Files
sanguo_vnpy_v2/sanguo_trader/qmt_gateway_client.py
T
claude_dev 434d7cb357
CI/CD / test (push) Failing after 7s
CI/CD / nas-deploy (push) Has been cancelled
CI/CD / nas-verify (push) Has been cancelled
feat(config): QMT 部署身份单一权威源 config/qmt_identity.json——P2-18 账号硬编码根治(⚖️-2 用户拍板:确认永为模拟号+令根治硬编码)
根因三层(全案=audit/20261005_qmt_identity_env_plan.md): schtask 无 per-task env+机器级 env 未设→wrapper 被迫字面量;「部署身份」无单一权威源(env 名分裂三个);模拟号零安全压力致 32 文件 88 处 copy-paste 扩散。

- qmt_gate_common.load_identity 正典装载器(:14-59): env SANGUO_QMT_ACCOUNT/BIGQMT_ACCOUNT_ID(兼容别名)>json>RuntimeError fail-loud;SANGUO_IDENTITY_JSON 显式指路不存在=立即报错;查找=repo 相对>C:\sanguo_vnpy_v2\config\(生产副本回退位)
- 灾备腿同源: relogin/qmt_bridge_probe/setclip 改 from qmt_gate_common import ACCOUNT
- 桥腿镜像同链: xt_gateway._qmt_account/qmt_gateway_client._identity_default_account/is_price_source_vps/bridge_switch_ops(3 处 env 注入+1 处内嵌探针串)/check_xtquant——env 带默认的硬编码默认值全数退役
- xt_eod_wrapper.ps1 改 Get-Content identity json(保持 ASCII);码内 docstring 字面量→占位符(engine/runner/runner_live/live.yaml 注释/README)
- .gitignore 白名单 !config/qmt_identity.json(全局 *.json 曾吞之,git check-ignore 实证)
- tests/trader/test_qmt_identity.py 五钉: 装载链语义(env 双名覆盖/显式指路 fail)+单源一致性(identity≡live.yaml≡watch_accounts≡gate_common≡各腿镜像,休市表 CI 同步测先例)+码面字面量 grep-pin(生产码面零 66639661,只 config 三件套可含)
- 监控 spec: §3.2 账号来源 bullet/§9.A-18 坑条/§12 行数+行号勘正(插入块致 bridge_ping/calendar_probe/relogin_running 引用行号推移);runbook: 非代码资产两行(副本回退路径+生产 wrapper 待改清单=10-08 窗口后 §五③)

生产 wrapper(C:\sanguo_bigqmt 库外)字面量收尾=10-08 复市验证窗口后非代码资产流程;VPS 班车同窗口(护 10-08 07:50 relogin 首班旧码纯净)。trader+api 620 绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-05 17:32:42 +08:00

194 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""vnpy_qmt 进程内执行客户端(Phase 2:替掉 HTTP bridge)。
与 ``bridge_client.BridgeClient`` 同接口(place_order/get_account/get_positions/
get_orders/cancel_order),但底层是**进程内 QmtGateway 直连 miniQMT**(同机同会话,
xtquant 进程内调用),不经 HTTP bridge。
设计:
- ``_QmtExec`` 单例持有长生命周期的 EventEngine + QmtGateway,懒连接(首次用即连),
跨 live_step 调用复用。gateway 的定时器周期性 query account/position/order 并经事件回调
推入缓存,故 get_account/get_positions 读缓存即同步返回。
- ``QmtGatewayClient`` 包装单例 + 缓存,方法签名/返回与 BridgeClient 对齐,live_orchestrator
可零改调用方代码替换。
配置(env):SANGUO_QMT_ACCOUNT(交易账号,不设则读部署身份单一权威源
config/qmt_identity.json,P2-18)、SANGUO_QMT_PATH(userdata_mini
路径,不设则扫 C:\\ 自动发现,避免中文路径字面量)。
依赖:vnpy 4.4.0 源码(sys.path)+ vnpy_qmt 0.3.3(pip)+ miniQMT 同机运行。
"""
from __future__ import annotations
import logging
import json
import os
import threading
from typing import Any
logger = logging.getLogger(__name__)
_singleton: _QmtExec | None = None
_lock = threading.Lock()
def _parse_code(code: str) -> tuple[str, Any]:
"""bridge code(sh600000/sz000001)→ (symbol, Exchange)。兼容纯数字码。"""
from vnpy.trader.constant import Exchange
c = code.lower()
if c.startswith("sh"):
return code[2:], Exchange.SSE
if c.startswith("sz"):
return code[2:], Exchange.SZSE
# 纯数字码兜底(与 bridge_client.to_bridge_code 一致)
if code.startswith(("60", "68", "51", "56", "58")):
return code, Exchange.SSE
return code, Exchange.SZSE
def _to_bridge_code(symbol: str, exchange: Any) -> str:
"""(symbol, Exchange) → bridge code(sh600000),get_positions 输出对齐 bridge。"""
from vnpy.trader.constant import Exchange
return f"sh{symbol}" if exchange == Exchange.SSE else f"sz{symbol}"
class _QmtExec:
"""长生命周期 vnpy_qmt 执行器单例:EventEngine + QmtGateway + 行情/账本缓存。"""
def __init__(self) -> None:
from vnpy.event import EventEngine
from vnpy.trader.event import EVENT_ACCOUNT, EVENT_POSITION, EVENT_ORDER
from vnpy_qmt import QmtGateway
self.account: Any = None # 最新 AccountData
self.positions: dict[str, Any] = {} # vt_symbol -> PositionData
self.orders: dict[str, Any] = {} # vt_orderid -> OrderData
self.ee = EventEngine()
self.ee.register(EVENT_ACCOUNT, self._on_account)
self.ee.register(EVENT_POSITION, self._on_position)
self.ee.register(EVENT_ORDER, self._on_order)
self.ee.start()
self.gateway = QmtGateway(self.ee)
self.gateway.connect(self._setting())
logger.info("QmtGatewayClient 连接 miniQMT: %s", self._setting().get("交易账号"))
@staticmethod
def _setting() -> dict[str, str]:
account = (os.environ.get("SANGUO_QMT_ACCOUNT")
or os.environ.get("BIGQMT_ACCOUNT_ID")
or _identity_default_account())
path = os.environ.get("SANGUO_QMT_PATH") or _find_userdata_mini()
return {"交易账号": account, "mini路径": path}
def _on_account(self, event: Any) -> None:
self.account = event.data
def _on_position(self, event: Any) -> None:
p = event.data
self.positions[p.vt_symbol] = p
def _on_order(self, event: Any) -> None:
o = event.data
self.orders[o.vt_orderid] = o
def _identity_default_account() -> str:
"""部署身份单一权威源 config/qmt_identity.json(P2-18 根治)——env 不设时
的账号兜底; 镜像自 scripts/qmt_relogin/qmt_gate_common(灾备腿正典),
CI 同步测钉一致; 文件缺失=FileNotFoundError fail-loud 不猜默认。"""
p = os.path.normpath(os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "config",
"qmt_identity.json"))
with open(p, encoding="utf-8") as f:
return json.load(f)["default_account"]
def _find_userdata_mini() -> str:
"""扫 C:\\ 找 userdata_mini 目录(避免中文路径字面量编码问题)。"""
try:
for name in os.listdir("C:\\"):
cand = os.path.join("C:\\", name, "userdata_mini")
if os.path.isdir(cand):
return cand
except OSError:
pass
return ""
def get_qmt_exec() -> _QmtExec:
"""懒加载单例(线程安全)。首次调用连 miniQMT,之后复用。"""
global _singleton
if _singleton is None:
with _lock:
if _singleton is None:
_singleton = _QmtExec()
return _singleton
class QmtGatewayClient:
"""进程内 vnpy_qmt 执行客户端,接口对齐 BridgeClient。
构造签名兼容 BridgeClient(url, token)——url/token 被忽略(进程内无需),
便于 live_orchestrator 零改替换。
"""
def __init__(self, url: str = "", token: str = "") -> None:
self._exec = get_qmt_exec()
def place_order(self, code: str, action: str, price: float, volume: int,
price_type: str = "limit", reason: str = "") -> dict | None:
"""下单 → {ok: True, order_id: vt_orderid} 或 None。"""
try:
from vnpy.trader.constant import Direction, OrderType, Offset
from vnpy.trader.object import OrderRequest
symbol, exchange = _parse_code(code)
direction = Direction.LONG if action == "buy" else Direction.SHORT
otype = OrderType.LIMIT if price_type == "limit" else OrderType.MARKET
req = OrderRequest(symbol=symbol, exchange=exchange, direction=direction,
type=otype, volume=volume, price=price,
offset=Offset.NONE, reference=reason or "")
vt_orderid = self._exec.gateway.send_order(req)
return {"ok": True, "order_id": vt_orderid}
except Exception as e: # noqa: BLE001 影子下单绝不阻断 live_step
logger.warning("QmtGatewayClient.place_order 失败 code=%s: %s", code, e)
return None
def get_account(self) -> dict | None:
a = self._exec.account
if a is None:
return None
mv = sum(p.volume * p.price for p in self._exec.positions.values())
return {"ok": True, "cash": a.balance - mv, "frozen": a.frozen,
"market_value": mv, "total": a.balance}
def get_positions(self) -> list | None:
return [{"code": _to_bridge_code(p.symbol, p.exchange), "volume": int(p.volume),
"can_use": int(p.yd_volume), "avg_price": float(p.price)}
for p in self._exec.positions.values() if p.volume > 0]
def get_orders(self) -> list | None:
from vnpy.trader.constant import Status
return [{"vt_orderid": o.vt_orderid, "code": _to_bridge_code(o.symbol, o.exchange),
"direction": "buy" if o.direction.value == "多" else "sell",
"price": o.price, "volume": o.volume, "traded": o.traded,
"status": o.status.name, "reference": o.reference}
for o in self._exec.orders.values()]
def cancel_order(self, order_id) -> dict | None:
try:
from vnpy.trader.object import CancelRequest
oid = str(order_id)
vt = oid if oid.startswith("QMT.") else f"QMT.{oid}"
o = self._exec.orders.get(vt)
if o is None:
return {"ok": False, "error": f"order {oid} 不在缓存(gateway 未收到该单事件)"}
remark = vt[4:] # vt_orderid(QMT.184246#1)→ remark(184246#1),vnpy_qmt 按 remark 建 key
# vnpy 4.4.0 的 CancelRequest 必填 (orderid, symbol, exchange),缺 symbol/exchange 会 TypeError
self._exec.gateway.cancel_order(
CancelRequest(orderid=remark, symbol=o.symbol, exchange=o.exchange))
return {"ok": True, "order_id": order_id}
except Exception as e: # noqa: BLE001
logger.warning("QmtGatewayClient.cancel_order 失败 %s: %s", order_id, e)
return {"ok": False, "error": str(e)}