Files
sanguo_vnpy_v2/sanguo_live/engine.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

155 lines
5.5 KiB
Python

"""LiveTradingEngine:vnpy 原生实盘链路封装。
参考 ``vnpy_v4.4.0/examples/no_ui/run.py`` 和 ``examples/no_ui/run.py``,
按 A 股 + QMT 场景精简:
- EventEngine + MainEngine
- add_gateway(QmtGateway) 连 miniQMT
- add_app(CtaStrategyApp) 挂 CTA 引擎
- 透出 connect / add_strategy / subscribe / init_all / start_all / stop_all / 查询
本机未装 vnpy_ctastrategy / vnpy_qmt 时 import 容错,实例化才报错。
"""
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger(__name__)
try:
from vnpy.event import EventEngine # type: ignore
from vnpy.trader.engine import MainEngine # type: ignore
from vnpy_qmt import QmtGateway # type: ignore
from vnpy_ctastrategy import CtaStrategyApp, CtaEngine # type: ignore
_DEPS_OK = True
_IMPORT_ERROR: Exception | None = None
except ImportError as _e:
EventEngine = None # type: ignore
MainEngine = None # type: ignore
QmtGateway = None # type: ignore
CtaStrategyApp = None # type: ignore
CtaEngine = None # type: ignore
_DEPS_OK = False
_IMPORT_ERROR = _e
class LiveTradingEngine:
"""vnpy 原生实盘引擎封装。单实例持有 MainEngine 生命周期。
使用:
eng = LiveTradingEngine()
eng.connect({"交易账号": "<QMT账号>", "mini路径": "C:\\\\..."})
eng.add_strategy(AShareDoubleMaStrategy, "dm1", "600000.SSE", {...})
eng.subscribe(["600000.SSE"])
eng.init_all(); eng.start_all()
# ... 常驻 ...
eng.stop_all(); eng.close()
"""
def __init__(self) -> None:
if not _DEPS_OK:
raise RuntimeError(
f"vnpy 依赖缺失(vnpy/vnpy_qmt/vnpy_ctastrategy),"
f"无法初始化 LiveTradingEngine: {_IMPORT_ERROR}"
)
self.event_engine: EventEngine = EventEngine()
self.main_engine: MainEngine = MainEngine(self.event_engine)
self.main_engine.add_gateway(QmtGateway)
self.cta_engine: CtaEngine = self.main_engine.add_app(CtaStrategyApp)
logger.info("LiveTradingEngine 初始化完成(MainEngine + QMT + CTA)")
# ---------------------- 连接 / 策略 / 行情 ----------------------
def connect(self, setting: dict[str, str]) -> None:
"""连接 miniQMT。``setting`` = ``{"交易账号":..., "mini路径":...}``。"""
self.main_engine.connect(setting, "QMT")
logger.info("已请求连接 QMT: 账号=%s", setting.get("交易账号"))
def add_strategy(
self,
strategy_class: type,
strategy_name: str,
vt_symbol: str,
setting: dict[str, Any],
) -> None:
"""注册策略实例到 CTA 引擎。"""
self.cta_engine.add_strategy(
strategy_class, strategy_name, vt_symbol, setting
)
logger.info("已添加策略 %s @ %s (class=%s)", strategy_name, vt_symbol,
strategy_class.__name__)
def subscribe(self, vt_symbols: list[str]) -> None:
"""订阅 ``vt_symbol`` 列表(格式 ``SYMBOL.EXCHANGE``,如 ``600000.SSE``)。
订阅是行情驱动策略的前提:QmtGateway.on_tick → EVENT_TICK →
CtaEngine → strategy.on_tick → BarGenerator 合成 15min bar。
"""
from vnpy.trader.object import SubscribeRequest # type: ignore
from vnpy.trader.constant import Exchange # type: ignore
ok = 0
for vt_symbol in vt_symbols:
try:
symbol, exch_code = vt_symbol.split(".", 1)
exchange = Exchange(exch_code)
except (ValueError, KeyError):
logger.warning("vt_symbol 无法解析,跳过: %r", vt_symbol)
continue
req = SubscribeRequest(symbol=symbol, exchange=exchange)
self.main_engine.subscribe(req, "QMT")
ok += 1
logger.info("已请求订阅 %d / %d 个标的", ok, len(vt_symbols))
# ---------------------- 生命周期 ----------------------
def init_all(self) -> None:
self.cta_engine.init_all_strategies()
logger.info("所有策略初始化完成")
def start_all(self) -> None:
self.cta_engine.start_all_strategies()
logger.info("所有策略已启动")
def stop_all(self) -> None:
try:
self.cta_engine.stop_all_strategies()
logger.info("所有策略已停止")
except Exception as e: # noqa: BLE001
logger.warning("stop_all_strategies 异常: %s", e)
def close(self) -> None:
try:
self.main_engine.close()
finally:
logger.info("LiveTradingEngine 已关闭")
# ---------------------- 查询(OMS 缓存) ----------------------
def _oms(self) -> Any:
return self.main_engine.get_engine("oms")
def get_all_accounts(self) -> list:
"""返回 QMT 网关所有账户(AccountData 列表)。连接前可能为空。"""
oms = self._oms()
if oms is None:
return []
return [a for a in oms.get_all_accounts() if a.gateway_name == "QMT"]
def get_positions(self) -> list:
"""返回 QMT 持仓(PositionData 列表)。"""
oms = self._oms()
if oms is None:
return []
return [p for p in oms.get_all_positions() if p.gateway_name == "QMT"]
def get_orders(self) -> list:
"""返回 QMT 当日委托(OrderData 列表)。"""
oms = self._oms()
if oms is None:
return []
return [o for o in oms.get_all_orders() if o.gateway_name == "QMT"]
__all__ = ["LiveTradingEngine"]