tradingSystem/app/services/plan_recheck.py

113 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""采纳前核验;复用原扫描、合议、研判与规则闸,不解释自然语言。"""
from datetime import datetime
import re
from app.core.plan_revision import compare, fingerprint, portfolio_context
def current_basis(hard, code, proposal_id=None):
from app.services import plan_feed, portfolio, command_service, proposal_service as ps
view = portfolio.positions_view()
stock = command_service.effective_stock_params()
params = ps._scan_params(view)
selected = plan_feed.candidates(held=[x['ts_code'] for x in view['held']],
black=[c for c, v in stock.items() if v.get('black')], force=True,
route_by_verdict=bool(params.get('open_route_by_verdict')), recheck_code=code)
current = next((r for r in selected['items'] if r['ts_code'] == code), None)
if not (selected.get('decision') or {}).get('id'):
return {'ok': False, 'error': '当前计划没有可核验版本'}
if current is None:
return {'ok': False, 'error': '该票已不满足当前候选资格', 'material': True}
result = compare(hard.get('plan_basis'), current, code)
if result == 'unavailable':
return {'ok': False, 'error': '当前计划修订或消费留痕尚未完成'}
ph = fingerprint({k: v for k, v in params.items() if not k.startswith('_')})
commitments = ps._inflight_open_commitments(view, exclude_proposal=proposal_id)
context = portfolio_context(view, commitments, stock.get(code))
material = (result == 'material' or hard.get('scan_params_hash') != ph
or hard.get('portfolio_context_hash') != context)
return {'ok': not material, 'material': material,
'error': '研究、资格或仓位参数已变化,须重新评估' if material else None,
'view': view, 'stock': stock, 'params': params,
'provenance': {'decision': current['decision'], 'consumption_id': current['consumption_id'],
'version_check': {'status': 'passed', 'at': datetime.now().astimezone().isoformat(),
'reason': '依据相同,名次变化不撤销'}}}
def _failed_recheck(p, out):
"""保留失败原因供页面和操作日志读取;不更新原批准依据或改变提议状态。"""
from app.repo import pms_repo
rows = [r for k in ('rejected', 'skipped') for r in out.get(k, [])
if not r.get('ts_code') or r['ts_code'] == p['ts_code']]
reasons = []
for row in rows:
values = row.get('failed') or [row.get('why')]
if isinstance(values, str):
values = [values]
for value in values:
text = re.sub(r'^[A-Z][A-Z0-9_]*:\s*', '', str(value or '')).strip()
if text and text not in reasons:
reasons.append(text)
stage = rows[0].get('by', 'scan') if rows else 'scan'
title = {'rule': '交易条件不满足', 'judge': '最新研判不建议建仓',
'system': '当前策略不建议建仓'}.get(stage, '暂不满足建仓条件')
message = title + (':' + ';'.join(reasons) if reasons else ',请稍后查看更新的建议')
check = {'status': 'blocked', 'at': datetime.now().astimezone().isoformat(),
'stage': stage, 'message': message, 'reasons': reasons, 'details': out}
hard = dict(p.get('hard_numbers') or {}, last_accept_check=check)
if not pms_repo.record_proposal_check(p, hard):
return {'ok': False, 'error': '提议已被更新,请刷新后查看当前结果',
'proposal_id': p['proposal_id']}
return {'ok': False, 'error': message, 'details': out, 'proposal_id': p['proposal_id'],
'proposal': dict(p, hard_numbers=hard)}
def before_accept(p, expected_revision):
from app.services import proposal_service as ps
from app.repo import pms_repo
hard = p.get('hard_numbers') or {}
if p.get('action') != 'OPEN' or hard.get('kind') == 'strategy':
return {'ok': True, 'proposal': p}
try:
check = current_basis(hard, p['ts_code'], proposal_id=p.get('proposal_id'))
# 已有失败结论不能因仓位后来恰好回到旧值而沿用旧批准。下一次仍完整核验。
if check.get('ok') and hard.get('last_accept_check'):
check.update(ok=False, material=True)
if check.get('ok'):
if hard.get('proposal_revision') and expected_revision != hard['proposal_revision']:
return {'ok': False, 'error': '提议内容已更新,请刷新后再次确认', 'proposal': p}
return {'ok': True, 'proposal': dict(p, hard_numbers=dict(hard, **check['provenance']))}
if not check.get('material') or not check.get('view'):
return check
now = datetime.now()
out = {'skipped': [], 'rejected': [], 'queued': [], 'executed': []}
params, view = check['params'], check['view']
mkt = {}
params['_mkt'] = mkt
fresh = ps._scan_open(view, params, check['stock'], {}, mkt, out, now=now,
review_code=p['ts_code'], exclude_proposal=p['proposal_id'])
if not fresh:
return _failed_recheck(p, out)
c = fresh[0]
c['_plan_recheck'] = True
from app.services import param_store
from app.core import tradedays as td
brake = td.ymd() < param_store.get_int('PMS_BRAKE_UNTIL', 0)
result = ps._route_one(c, view, params, check['stock'], brake, now, True, out, review_only=True)
if not result:
return _failed_recheck(p, out)
hn = c['hard_numbers']
hn['version_check'] = {'status': 'reconfirmed_required', 'at': now.astimezone().isoformat(),
'reason': '依据变化,已重跑合议、研判、数量及规则闸'}
hn['proposal_revision'] = fingerprint({'qty': c['qty'], 'hard_numbers': hn})
if not pms_repo.revise_pending_proposal(p, c, result['verdict']):
return {'ok': False, 'error': '提议被并发更新,请刷新'}
pms_repo.insert_ledger(ts_code=p['ts_code'], action=p['action'], arbiter='rule',
verdict='WARN', price_at=hn.get('price') or 0, hard_numbers=hn,
ref_id=p['proposal_id'], reason='新版依据变化,更新提议并要求重新确认')
return {'ok': False, 'error': '提议已按新版重新评估,请查看更新内容后再次确认',
'proposal': dict(p, qty=c['qty'], hard_numbers=hn,
judge_verdict=result['verdict'].get('verdict'),
judge_reason=result['verdict'].get('reason'))}
except Exception as e:
return {'ok': False, 'error': f'版本核验暂不可用,保留提议:{type(e).__name__}'}