Files
sanguo_vnpy_v2/sanguo_portfolio/live_strategy.py
T

169 lines
8.2 KiB
Python

"""组合策略实盘适配文件(bullet_trade LiveEngine 加载的聚宽风格 strategy_file)。
bullet_trade 0.9.x LiveEngine 只认策略文件:本文件 ``initialize(context)`` 里把
sanguo_portfolio 的 StrategyTemplate 策略挂到 run_daily/run_monthly 定时器,
下单走 bullet_trade 顶层 API(live 模式自动路由 LiveEngine → QmtBroker)。
配置从 env 读(supervisor 注入,或手动 set 后直跑 ``python -m sanguo_portfolio.runner_live``):
SANGUO_LIVE_STRATEGY all_weather / momentum_timing / value_selection / small_cap
SANGUO_LIVE_MAX_POOL 选股池上限(默认 30)
数据 provider 由 runner_live ``set_data_provider`` 先行注入(miniQMT live 模式)。
⚠️ **process_initialize 是定时任务的生命线**(2026-08-17 VPS 16 引擎空转事故):
LiveEngine 重启时若 runtime 里恢复了 g(live_state.json/g.pkl),会**跳过 initialize**
"断点续跑"路径,并把持久化的旧任务按 ``module+func`` 反射恢复——而我们的任务
是策略实例的 bound method,恢复必然失败(``无法恢复调度任务``)。结果:进程活着、
分钟心跳正常,但调度任务列表为空,开盘后零成交零日志。
聚宽语义的正解是 ``process_initialize``(每次进程启动必调,含 resume):任务注册
放这里,resume 重启后才能补挂。initialize 钩子保留(新策略首启由引擎调用)。
"""
from __future__ import annotations
import logging
import os
logger = logging.getLogger(__name__)
# 进程内单例:initialize/process_initialize 双钩子共用一份策略实例与装配状态
_STATE: dict = {"strategy": None, "wired": False}
def _build_live_strategy(provider):
"""env 配置 → StrategyTemplate 实例(对齐 runner_backtest._build_strategy)。"""
from sanguo_portfolio.strategies import (
AllWeatherConfig, AllWeatherStrategy,
AllWeatherExConfig, AllWeatherExStrategy,
ChannelTestConfig, ChannelTestStrategy,
MomentumTimingConfig, MomentumTimingStrategy,
MomentumTimingExConfig, MomentumTimingExStrategy,
SmallCapConfig, SmallCapStrategy,
SmallCapExConfig, SmallCapExStrategy,
ValueSelectionConfig, ValueSelectionStrategy,
ValueSelectionExConfig, ValueSelectionExStrategy,
)
name = os.environ.get("SANGUO_LIVE_STRATEGY", "all_weather")
max_pool = int(os.environ.get("SANGUO_LIVE_MAX_POOL", "30") or 30)
factories = {
"all_weather": lambda: AllWeatherStrategy(
provider=provider, config=AllWeatherConfig(max_pool=max_pool)),
"momentum_timing": lambda: MomentumTimingStrategy(
provider=provider, config=MomentumTimingConfig(max_pool=max_pool)),
"value_selection": lambda: ValueSelectionStrategy(
provider=provider, config=ValueSelectionConfig(max_pool=max_pool)),
"small_cap": lambda: SmallCapStrategy(
provider=provider, config=SmallCapConfig(max_pool=max_pool)),
"channel_test": lambda: ChannelTestStrategy(
provider=provider, config=ChannelTestConfig()),
# TET Phase2 副本(issue#19):影子/实盘账户可用 _ex 副本发起
# (2026-08-16 VPS 实况:shadow#42=all_weather_ex 因工厂表缺项拉起即崩)
"all_weather_ex": lambda: AllWeatherExStrategy(
provider=provider, config=AllWeatherExConfig(max_pool=max_pool)),
"momentum_timing_ex": lambda: MomentumTimingExStrategy(
provider=provider, config=MomentumTimingExConfig(max_pool=max_pool)),
"value_selection_ex": lambda: ValueSelectionExStrategy(
provider=provider, config=ValueSelectionExConfig(max_pool=max_pool)),
"small_cap_ex": lambda: SmallCapExStrategy(
provider=provider, config=SmallCapExConfig(max_pool=max_pool)),
}
if name not in factories:
raise ValueError(
f"未知 SANGUO_LIVE_STRATEGY: {name}"
f"(支持: {' / '.join(factories)})"
)
return factories[name]()
def _ledger_tools():
"""账本通道依赖(2026-08-20 事故修复):必须绝对导入。
本文件会被逐字节复制到 runtime/<实例>/live_strategy.py(runner_live 与
shadow/runner 的 _instance_adapter),bullet_trade 以**顶层模块**加载副本
(无包上下文)——历史上这里用 ``from . import`` 相对导入,包内(测试/回测)
合法,孤立副本上直接 ImportError → 2026-08-20 晨 12 引擎(6实盘+6影子)
崩溃循环 4 小时、当日 0 成交。绝对导入两种上下文都成立(引擎进程的项目根
在 sys.path,同文件其它 ``from sanguo_portfolio...`` 导入生产已验证可跑)。
单独成函数 = 两种加载上下文的单测都能直接打到这一行。
"""
from sanguo_portfolio import live_instance_ledger
from sanguo_portfolio.live_portfolio_proxy import wrap_scheduler
return live_instance_ledger, wrap_scheduler
def _setup(context):
"""装配策略(幂等,每进程一次):建实例 + broker facade + 定时任务。
initialize(新策略首启)与 process_initialize(每次进程启动)共用;第二个
钩子进来时直接返回,避免重复装配。各策略的 initialize 均为幂等配置,且定时
任务由策略自身经注入的 facade.run_daily/run_monthly 注册(单一事实源,无需
再调 runner_backtest._register_schedule 代注册)。
"""
from bullet_trade.core.api import ( # type: ignore
order_target_value as bt_otv,
order_value as bt_ov,
set_order_cost, set_slippage,
)
from bullet_trade.core import run_daily as bt_run_daily # type: ignore
from bullet_trade.core import run_monthly as bt_run_monthly # type: ignore
from bullet_trade.core.settings import ( # type: ignore
OrderCost, FixedSlippage, set_option as bt_set_option,
)
from bullet_trade.data.api import get_data_provider # type: ignore
from sanguo_portfolio.strategies.all_weather import BrokerFacade
if _STATE["wired"]:
return
strategy = _build_live_strategy(get_data_provider())
_STATE["strategy"] = strategy
# broker 注入(与回测同构):下单委托 bullet_trade 顶层 API,live 下路由 engine;
# run_daily/run_monthly 同步注入——策略在自身 initialize 里经 facade 自挂定时
# 任务,facade 缺注入时静默 no-op(2026-08-17 前 channel_test 等永不开仓的根因之一)
strategy.broker = BrokerFacade(
order_target_value=lambda c, v: bt_otv(c, v),
order_value=lambda c, v: bt_ov(c, v),
set_option=lambda k, v: bt_set_option(k, v),
run_daily=bt_run_daily,
run_monthly=bt_run_monthly,
)
# 实例持仓通道(2026-08-19 共享QMT账户互卖根治):runner_live 已 set_active
# 时注入,策略侧 getattr(broker,'get_instance_positions',None) 消费;
# 回测/测试无账本 → 保持 None,策略回退 context.portfolio
_lil, _wrap_sched = _ledger_tools()
_ledger = _lil.get_active()
if _ledger is not None:
strategy.broker.get_instance_positions = _ledger.positions_view
logger.info("instance 台账通道已注入: get_instance_positions (cash=%.2f 持仓 %d 只)",
_ledger.cash, len(_ledger.positions))
# B2 定寸虚拟化:经 facade 注册的定时回调收到代理 context(portfolio=实例
# 账本视图),策略定寸/资金守卫不再被全账户污染;引擎内部仍看真实账户。
# 无账本(回测/影子) → wrap_scheduler 原样返回,行为零改动。
strategy.broker.run_daily = _wrap_sched(bt_run_daily, _ledger)
strategy.broker.run_monthly = _wrap_sched(bt_run_monthly, _ledger)
# A 股费用 + 滑点(与回测默认一致)
set_order_cost(
OrderCost(
open_tax=0.0, close_tax=0.001,
open_commission=0.0003, close_commission=0.0003,
min_commission=5.0,
),
type="stock",
)
set_slippage(FixedSlippage(value=0.001))
strategy.initialize(context)
_STATE["wired"] = True
logger.info("live strategy 已挂载: %s", type(strategy).__name__)
def initialize(context):
"""LiveEngine 新策略首启回调(策略文件级,g 未恢复时才被调)。"""
_setup(context)
def process_initialize(context):
"""LiveEngine 每次进程启动必调(resume 重启含)——定时任务在这里补挂。"""
_setup(context)