Files
sanguo_vnpy_v2/sanguo_portfolio/runner_live.py
T
claude_dev a68cf4905e feat(portfolio): sanguo_portfolio 组合策略框架(BulletTrade+miniQMT,不用jqdatasdk)
把聚宽"全天候轮动"(post48819)搬到 BulletTrade。融合=pip+扩展点注入
(SanguoMiniQmtProvider 继承 MiniQMTProvider 只 override get_fundamentals,
set_data_provider 公开 API 注入, BulletTrade 源码 0 改动)。

- providers: SanguoMiniQmtProvider 补 get_fundamentals(PershareIndex+自算PE/PS/PB/PCF/市值/ROIC)
- strategies/all_weather: 4选股函数+大小盘轮动+ETF兜底+涨停止损(聚宽风格翻译)
- factors(估值/ROIC自算) + filters(ST/涨跌停/次新/停牌)
- 88/88 测试 Mac+VPS 双过; VPS 回测 pipeline 跑通(修9bug:Capital单位/日期格式/百分数口径/11字段alias)
- 实盘 runner_live+runbook 就绪等交易日; DEFAULT_DATA_PROVIDER=miniqmt env 不装 jqdatasdk
- 文档: sanguo_portfolio_plan / portfolio_backtest_result / portfolio_live_runbook
2026-07-18 19:08:18 +08:00

111 lines
3.6 KiB
Python

"""全天候策略实盘入口(VPS Windows / miniQMT 直连)。
**实盘就绪,但需在交易日+miniQMT 连接下首次跑**。
用法:
set DEFAULT_DATA_PROVIDER=miniqmt
set MINIQMT_MARKET=SH
python -m sanguo_portfolio.runner_live
不在 Mac 跑(Mac 无 xtquant/miniQMT 客户端)。
"""
from __future__ import annotations
# ENV GUARD 必须早于任何 bullet_trade import
import os
os.environ.setdefault("DEFAULT_DATA_PROVIDER", "miniqmt")
import logging
from typing import Any, Dict
logger = logging.getLogger(__name__)
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 build_broker_facade() -> Any:
"""实盘 BrokerFacade:委托 bullet_trade 顶层聚宽风格 API + QmtBroker。"""
from .strategies.all_weather import BrokerFacade
from bullet_trade.core.api import ( # type: ignore
order_target_value as bt_otv,
order_value as bt_ov,
set_benchmark, set_option, set_slippage, set_order_cost,
run_daily, run_monthly,
)
return BrokerFacade(
order_target_value=lambda c, v: bt_otv(c, v),
order_value=lambda c, v: bt_ov(c, v),
set_benchmark=set_benchmark,
set_option=set_option,
set_slippage=set_slippage,
set_order_cost=set_order_cost,
run_daily=run_daily,
run_monthly=run_monthly,
)
def run_live(provider_config: Dict[str, Any] | None = None) -> None:
"""启动 LiveEngine + AllWeatherStrategy。
LiveEngine 负责驱动 scheduler(context.current_dt 推进)和下单路由。
我们负责把 provider/broker 注入 AllWeatherStrategy。
"""
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
from .strategies import AllWeatherConfig, AllWeatherStrategy
provider = build_provider(provider_config)
set_data_provider(provider)
broker_facade = build_broker_facade()
strategy = AllWeatherStrategy(
provider=provider,
broker=broker_facade,
config=AllWeatherConfig(),
)
def initialize(context):
strategy.initialize(context)
# QmtBroker 需要 account_id + data_path(miniQMT userdata_mini)。
# 从 env 读(与 sanguo_live 约定一致),缺 account 拒绝启动避免误下单。
account = os.environ.get("SANGUO_QMT_ACCOUNT", "")
mini_path = (os.environ.get("SANGUO_QMT_PATH")
or r"C:\国金QMT交易端模拟\userdata_mini")
if not account:
raise RuntimeError(
"缺 SANGUO_QMT_ACCOUNT(miniQMT 交易账号),实盘无法启动。"
"设 set SANGUO_QMT_ACCOUNT=66639661 后重试。"
)
broker = QmtBroker(account_id=account, data_path=mini_path)
logger.info("QmtBroker 装配 account=%s data_path=%s", account, mini_path)
# 小资金 1e6 起步,等交易日观察(9:05 prepare / 月初 9:30 monthly / 14:00 stop_loss)
engine = LiveEngine(
initialize=initialize,
initial_cash=1_000_000.0,
broker=broker,
)
logger.info("AllWeather live engine 启动,等交易日触发 monthly_adjustment")
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()