# -*- coding: utf-8 -*- """ 指令下发通道 (设计 §9 权限移交的落点) ====================================== 三个适配器, 由参数 `PMS_DISPATCH_MODE` 切换, **默认 shadow**: shadow 影子运行 —— 只记账不下发。指令照常过规则闸、照常置 DISPATCHED, 等用户人工在 QMT 侧执行, 成交由回放按 FIFO 认领回来。 这是设计 §9 的一期口径:「通道未通前由用户人工执行、PMS 记账跟踪」。 plan_x 过渡兼容 —— 买入沿用 `trading_buy_plan` (置 is_active=6 待挂单, 署名 approved_by='pms')。**卖出无对应通道**, 自动退回 shadow。 列清单按下游现表推断, QMT 侧确认前请勿在实盘开启。 channel_y 推荐方案 —— 写统一指令表 `pms_order_request` (DDL 见需求清单 B1)。 该表归属与形态仍在协商 (B1.1/B1.7), 表未建时会明确报错而非静默吞掉。 无论哪种模式, **指令先落 pms_instruction 再下发** (先记账后动作), 本模块只负责 「往下游递一手」, 不改指令状态 —— 状态由 executor 统一推进。 """ from __future__ import annotations import logging from datetime import datetime from app.db.session import execute from app.services import param_store logger = logging.getLogger("pms.dispatch") MODE_SHADOW, MODE_PLAN_X, MODE_CHANNEL_Y = "shadow", "plan_x", "channel_y" MODES = (MODE_SHADOW, MODE_PLAN_X, MODE_CHANNEL_Y) def mode() -> str: m = (param_store.get("PMS_DISPATCH_MODE", MODE_SHADOW) or MODE_SHADOW).strip() return m if m in MODES else MODE_SHADOW def describe() -> dict: m = mode() return {"mode": m, "shadow": m == MODE_SHADOW, "modes": list(MODES), "hint": { MODE_SHADOW: "影子运行: 指令只记账不下发, 请在 QMT 侧人工执行, " "成交由回放自动认领回账本", MODE_PLAN_X: "过渡通道: 买入写 trading_buy_plan(is_active=6), 卖出退回影子", MODE_CHANNEL_Y: "统一通道: 写 pms_order_request, 由下游轮询执行", }[m]} def dispatch(*, instruction_id: str, ts_code: str, side: str, qty: int, limit_price=None, valid_until=None, stock_name=None) -> dict: """递一手给下游。返回 {ok, ref, mode, note, error}; ok=False 时 executor 不改指令状态。""" m = mode() try: if m == MODE_CHANNEL_Y: return _channel_y(instruction_id, ts_code, side, qty, limit_price, valid_until) if m == MODE_PLAN_X and str(side).lower() == "buy": return _plan_x(instruction_id, ts_code, qty, limit_price, stock_name) if m == MODE_PLAN_X: r = _shadow(instruction_id, side) r["note"] = "plan_x 无卖出通道, 本单退回影子运行 (待 QMT B1 落地)" return r return _shadow(instruction_id, side) except Exception as e: logger.exception("下发失败 %s", instruction_id) return {"ok": False, "ref": None, "mode": m, "note": "", "error": f"{type(e).__name__}: {e}"} def _shadow(instruction_id: str, side: str) -> dict: return {"ok": True, "ref": f"manual:{instruction_id}", "mode": MODE_SHADOW, "note": f"影子运行: 请在 QMT 侧人工{'买入' if side == 'buy' else '卖出'}, " f"成交由回放认领", "error": None} def _channel_y(instruction_id, ts_code, side, qty, limit_price, valid_until) -> dict: now = datetime.now() execute( "INSERT INTO pms_order_request (instruction_id, ts_code, side, qty, limit_price, " "valid_until, source, status, cancel_flag, create_time, update_time) VALUES " "(:iid, :code, :side, :qty, :lp, :vu, 'pms', 'NEW', 0, :ts, :ts)", {"iid": instruction_id, "code": ts_code, "side": str(side).lower(), "qty": int(qty), "lp": limit_price, "vu": valid_until or now, "ts": now}) return {"ok": True, "ref": instruction_id, "mode": MODE_CHANNEL_Y, "note": "已写入 pms_order_request, 等下游轮询执行", "error": None} def _plan_x(instruction_id, ts_code, qty, limit_price, stock_name) -> dict: """买入走上游既有计划表。amount 由 数量×限价 反算 (下游按 buy_amount 挂单)。""" px = float(limit_price or 0) if px <= 0: return {"ok": False, "ref": None, "mode": MODE_PLAN_X, "note": "", "error": "plan_x 通道要求限价 (下游按 target_price 挂单)"} now = datetime.now() execute( "INSERT INTO trading_buy_plan (stock_code, stock_name, target_price, buy_amount, " "is_active, trading_time, create_time, update_time, approved_by, change_reason) " "VALUES (:code, :name, :px, :amt, 6, :ts, :ts, :ts, 'pms', :rsn)", {"code": ts_code, "name": stock_name or ts_code, "px": px, "amt": round(qty * px, 2), "ts": now, "rsn": f"PMS 指令 {instruction_id}"}) return {"ok": True, "ref": f"buy_plan:{instruction_id}", "mode": MODE_PLAN_X, "note": "已写 trading_buy_plan(is_active=6) 待下游挂单", "error": None} def cancel(*, instruction_id: str, dispatch_ref=None) -> dict: """请求撤单。shadow/plan_x 无撤单语义, 只回执由 executor 置本地状态。""" m = mode() if m != MODE_CHANNEL_Y: return {"ok": True, "mode": m, "note": "本模式无下游撤单动作, 仅本地置撤销"} try: n = execute("UPDATE pms_order_request SET cancel_flag = 1, update_time = :ts " "WHERE instruction_id = :iid AND status IN ('NEW', 'ACCEPTED', 'EXECUTING')", {"iid": instruction_id, "ts": datetime.now()}) return {"ok": True, "mode": m, "note": f"已置 cancel_flag, 影响 {n} 行"} except Exception as e: return {"ok": False, "mode": m, "error": f"{type(e).__name__}: {e}"}