96b1924fd5
[live] 实盘模拟 vnpy+miniQMT 直连(supervisor 轮询, 前后端): - sanguo_live: LiveTradingEngine + AShareCtaTemplate(定寸/禁做空) + runner_supervisor(DB驱动) + persistence(4表WAL) - sanguo_api/routes_live: 9路由(create/start/stop/positions/trades/account/status) - frontend live: New/List/Monitor + api/live.ts; config/live.yaml [portfolio] 组合回测 MVP(BulletTrade, 链路代码完成待验证): - runner_backtest 加 JSON 入口(--json, BacktestEngine 顶层 import) - sanguo_api/routes_portfolio: POST /portfolio/backtest SSH 触发 VPS 跑 - frontend PortfolioBacktest.vue + api/portfolio.ts: 表单+结果+净值曲线 - 路由/菜单注册(/backtest/portfolio 组合回测) - 已知: MVP 链路未端到端验证, agent 改至中途被停; 待 Mac 起服务联调
155 lines
5.5 KiB
Python
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({"交易账号": "66639661", "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"]
|