"""组合策略实盘入口(VPS Windows / miniQMT 直连)——bullet_trade 0.9.2 LiveEngine。 supervisor(``sanguo_live.runner.run_supervisor``) 对 strategy_type='portfolio' 的 live_accounts 行以**子进程**方式拉起本模块,env 传参: SANGUO_QMT_ACCOUNT / SANGUO_QMT_PATH miniQMT 交易账号 / userdata_mini 路径 SANGUO_LIVE_STRATEGY / _MAX_POOL / _BENCHMARK 组合策略配置 SANGUO_LIVE_CASH 初始资金(engine NAV 基准) SANGUO_LIVE_DB / SANGUO_LIVE_ACCOUNT_ID 快照落库目标(缺省不落) 手动用法(交易日 + miniQMT 连接下): set SANGUO_QMT_ACCOUNT=66639661 python -m sanguo_portfolio.runner_live 不在 Mac 跑(Mac 无 xtquant/miniQMT 客户端)。 历史注记:0.2 之前的 bullet_trade LiveEngine 接受 ``initialize=/broker=`` 直传, 0.9.x 改为 strategy_file + broker_factory——本模块即按新 API 装配,策略逻辑在 ``sanguo_portfolio/live_strategy.py``(适配文件)。 """ from __future__ import annotations # ENV GUARD 必须早于任何 bullet_trade import import os os.environ.setdefault("DEFAULT_DATA_PROVIDER", "miniqmt") import logging import threading import time from pathlib import Path from typing import Any, Dict logger = logging.getLogger(__name__) ADAPTER_FILE = Path(__file__).resolve().parent / "live_strategy.py" def build_provider(provider_config: Dict[str, Any] | None = None) -> Any: """构造 live 模式的 SanguoMiniQmtProvider。""" from .providers import SanguoMiniQmtProvider cfg = dict(provider_config or {}) cfg.setdefault("mode", "live") cfg.setdefault("auto_download", True) return SanguoMiniQmtProvider(cfg) def live_env() -> Dict[str, str]: """解析 env 实盘配置(带默认值)。独立出来便于单测。""" return { "strategy": os.environ.get("SANGUO_LIVE_STRATEGY", "all_weather"), "max_pool": os.environ.get("SANGUO_LIVE_MAX_POOL", "30"), "benchmark": os.environ.get("SANGUO_LIVE_BENCHMARK", "000300.XSHG"), "cash": os.environ.get("SANGUO_LIVE_CASH", "1000000"), "account": os.environ.get("SANGUO_QMT_ACCOUNT", ""), "mini_path": (os.environ.get("SANGUO_QMT_PATH") or r"C:\国金QMT交易端模拟\userdata_mini"), "db": os.environ.get("SANGUO_LIVE_DB", ""), "account_id": os.environ.get("SANGUO_LIVE_ACCOUNT_ID", ""), } def _snapshot_loop(engine: Any, db: str, account_id: int, interval_sec: float = 60.0) -> None: """后台线程:把 engine 组合快照落 live_positions/live_balance(供 API 读)。 LiveEngine 的账户/持仓由 broker 同步进 context.portfolio(LivePortfolioProxy), 这里只读转储;任何异常只 warning 不中断(engine 主循环不受影响)。 """ from datetime import datetime from sanguo_live.persistence import save_balance, save_positions while True: time.sleep(interval_sec) try: portfolio = engine.context.portfolio positions: Dict[str, Dict[str, Any]] = {} for sym, pos in (getattr(portfolio, "positions", None) or {}).items(): vol = int(getattr(pos, "total_amount", 0) or 0) if vol <= 0: continue positions[str(sym)] = { "volume": float(vol), "frozen": float(vol - int(getattr(pos, "closeable_amount", vol) or 0)), "avg_price": float(getattr(pos, "avg_cost", 0) or 0), } save_positions(db, account_id, positions) cash = float(getattr(portfolio, "available_cash", 0) or 0) total = float(getattr(portfolio, "total_value", 0) or 0) save_balance( db, account_id, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), cash, market_value=max(total - cash, 0.0), total=total, ) except Exception as e: # noqa: BLE001 logger.warning("[live-snapshot] 落库失败 (account=%s): %s", account_id, e) def run_live(provider_config: Dict[str, Any] | None = None) -> None: """装配 LiveEngine(strategy_file=适配文件 + QmtBroker)并 run(阻塞)。""" from bullet_trade.core.live_engine import LiveEngine # type: ignore from bullet_trade.data.api import set_data_provider # type: ignore from bullet_trade.broker.qmt import QmtBroker # type: ignore cfg = live_env() if not cfg["account"]: raise RuntimeError( "缺 SANGUO_QMT_ACCOUNT(miniQMT 交易账号),实盘无法启动。" "设 set SANGUO_QMT_ACCOUNT=66639661 后重试。" ) provider = build_provider(provider_config) set_data_provider(provider) broker = QmtBroker(account_id=cfg["account"], data_path=cfg["mini_path"]) logger.info("QmtBroker 装配 account=%s data_path=%s", cfg["account"], cfg["mini_path"]) engine = LiveEngine( ADAPTER_FILE, broker_factory=lambda: broker, ) logger.info( "组合 live engine 启动: strategy=%s max_pool=%s benchmark=%s cash=%s", cfg["strategy"], cfg["max_pool"], cfg["benchmark"], cfg["cash"], ) # 快照落库(supervisor 注入 db+account_id 时才开) if cfg["db"] and cfg["account_id"]: t = threading.Thread( target=_snapshot_loop, args=(engine, cfg["db"], int(cfg["account_id"])), daemon=True, name="live-snapshot", ) t.start() engine.run() def main() -> None: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") run_live() if __name__ == "__main__": main()