"""统一执行方向检查。只消解既有指令冲突,不重做研究或择时。""" 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