109 lines
5.4 KiB
Python
109 lines
5.4 KiB
Python
|
|
"""统一执行方向检查。只消解既有指令冲突,不重做研究或择时。"""
|
||
|
|
from app.repo import pms_repo, qmt_repo
|
||
|
|
|
||
|
|
LIVE = ('PROPOSED', 'RULE_PASSED', 'JUDGE_PASSED', 'DISPATCHED')
|
||
|
|
BUY_ACTIONS = ('OPEN', 'FILL', 'ADD', 'DCA')
|
||
|
|
|
||
|
|
|
||
|
|
def buy_blocker(ts_code):
|
||
|
|
# 查询失败必须抛给调用者,不能当作无冲突。按票查,不受全局列表裁剪影响。
|
||
|
|
rows = pms_repo.list_instructions(statuses=list(LIVE), side='sell', ts_code=ts_code, limit=500)
|
||
|
|
for row in rows:
|
||
|
|
if row.get('action') in ('EXIT', 'TRIM') and int(row.get('qty') or 0) > int(row.get('exec_qty') or 0):
|
||
|
|
verb = '清仓' if row['action'] == 'EXIT' else '减仓'
|
||
|
|
return f'该股正在{verb},暂停买入'
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def queued_buy_blocker(row):
|
||
|
|
"""发出前再次检查。异常由通道保留队列等待,不按无冲突继续。"""
|
||
|
|
if row.get('side') != 'buy':
|
||
|
|
return None
|
||
|
|
parent = pms_repo.get_instruction(row.get('parent_id'))
|
||
|
|
if not parent:
|
||
|
|
raise RuntimeError('买入来源指令无法核实,暂不发送')
|
||
|
|
if pms_repo.instruction_buy_stop(row.get('parent_id')) or (parent.get('progress') or {}).get('stopped_by_exit'):
|
||
|
|
return '清仓已终止原买入安排'
|
||
|
|
if parent.get('status') not in LIVE:
|
||
|
|
return '来源买入指令已结束,未发出的委托作废'
|
||
|
|
return buy_blocker(row['ts_code'])
|
||
|
|
|
||
|
|
|
||
|
|
def enforce_exit(ins):
|
||
|
|
"""完整清仓停止旧买入。每跳重试,撤单失败不会阻断风控卖出。"""
|
||
|
|
out = {'ok': True, 'instructions': [], 'plans': [], 'errors': []}
|
||
|
|
if ins.get('action') != 'EXIT' or ins.get('side') != 'sell':
|
||
|
|
return out
|
||
|
|
from app.services import strategy_service
|
||
|
|
why = '清仓指令已生效,停止该股原有买入安排'
|
||
|
|
# 先持久化停止依据。即使下游撤单暂时失败、清仓随后成交,也不能恢复原买入。
|
||
|
|
buys = pms_repo.list_instructions(statuses=list(LIVE), side='buy', ts_code=ins['ts_code'], limit=500)
|
||
|
|
for buy in buys:
|
||
|
|
if not pms_repo.instruction_buy_stop(buy['instruction_id']):
|
||
|
|
if not pms_repo.insert_ledger(ts_code=ins['ts_code'], action='BUY_STOP', arbiter='system',
|
||
|
|
verdict='PASS', price_at=0, ref_id=buy['instruction_id'], reason=why,
|
||
|
|
hard_numbers={'exit_instruction_id': ins['instruction_id'],
|
||
|
|
'qty': buy.get('qty'), 'exec_qty': buy.get('exec_qty')}):
|
||
|
|
raise RuntimeError('清仓停止依据未写入动作账本')
|
||
|
|
progress = dict(buy.get('progress') or {})
|
||
|
|
if not progress.get('stopped_by_exit'):
|
||
|
|
progress['stopped_by_exit'] = ins['instruction_id']
|
||
|
|
progress['last_decision'] = {'action': 'WAIT', 'reason': why}
|
||
|
|
if not pms_repo.update_instruction(buy['instruction_id'], progress=progress):
|
||
|
|
raise RuntimeError('清仓停止买入依据未写入')
|
||
|
|
result = strategy_service.cancel_by_code(ins['ts_code'], reason=why)
|
||
|
|
out.update(result)
|
||
|
|
# 未解锁与尚未转指令的命令批也要收尾,否则下一跳又生出买单。
|
||
|
|
try:
|
||
|
|
plans = pms_repo.list_plans(ts_code=ins['ts_code'], statuses=['PENDING', 'GATED', 'EXECUTING'])
|
||
|
|
for plan in plans:
|
||
|
|
if plan.get('action') not in BUY_ACTIONS:
|
||
|
|
continue
|
||
|
|
pms_repo.insert_ledger(ts_code=ins['ts_code'], action='CLEANUP', arbiter='system',
|
||
|
|
verdict='PASS', price_at=0, ref_id=plan['plan_id'],
|
||
|
|
reason=why, hard_numbers={'exit_instruction_id': ins['instruction_id']})
|
||
|
|
if not pms_repo.update_plan(plan['plan_id'], status='CANCELLED'):
|
||
|
|
raise RuntimeError('停止买入批次未写入')
|
||
|
|
out['plans'].append(plan['plan_id'])
|
||
|
|
except Exception as exc:
|
||
|
|
out['ok'] = False
|
||
|
|
out['errors'].append(str(exc))
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def instruction_progress(rows):
|
||
|
|
"""只读展示。父指令发过单,不代表当前还有未成交委托。"""
|
||
|
|
from app.services import dispatcher
|
||
|
|
ws = dispatcher.mode() == 'ws'
|
||
|
|
for row in rows:
|
||
|
|
if row.get('status') not in LIVE and row.get('status') != 'CANCELLED':
|
||
|
|
continue
|
||
|
|
if not ws:
|
||
|
|
row['execution_label'] = '影子执行待核对'
|
||
|
|
continue
|
||
|
|
try:
|
||
|
|
orders = qmt_repo.orders_for_parent(row['instruction_id'])
|
||
|
|
active = [o for o in orders if o.get('status') in qmt_repo.LIVE]
|
||
|
|
if any(o.get('cancel_state') in ('REQUESTED', 'SENT') for o in active):
|
||
|
|
label = '撤单处理中'
|
||
|
|
elif any(o.get('status') not in ('QUEUED', 'SENDING') for o in active):
|
||
|
|
label = '委托待成交'
|
||
|
|
elif active:
|
||
|
|
label = '委托待发送'
|
||
|
|
else:
|
||
|
|
reason = ((row.get('progress') or {}).get('last_decision') or {}).get('reason') or ''
|
||
|
|
if row.get('status') == 'CANCELLED':
|
||
|
|
label = '已撤销'
|
||
|
|
elif '可卖量不足' in reason:
|
||
|
|
label = '等待可卖'
|
||
|
|
elif '当日配额' in reason:
|
||
|
|
label = '等待下一批'
|
||
|
|
elif '暂停买入' in reason:
|
||
|
|
label = '买入已暂停'
|
||
|
|
else:
|
||
|
|
label = '等待执行'
|
||
|
|
row['execution_label'] = label
|
||
|
|
except Exception:
|
||
|
|
row['execution_label'] = '委托状态待核实'
|
||
|
|
return rows
|