tradingSystem/app/services/dispatcher.py

117 lines
5.8 KiB
Python
Raw Normal View History

# -*- 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}"}