117 lines
5.8 KiB
Python
117 lines
5.8 KiB
Python
# -*- 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}"}
|