fix(portfolio): 实盘/影子引擎重启后定时任务全丢根治——process_initialize+facade注入run_daily [vps]
P0 根因(2026-08-17 VPS 16 引擎空转零成交事故):bullet_trade LiveEngine 重启时
恢复 g(live_state.json/g.pkl)则跳过 initialize 走断点续跑,持久化旧任务按
module+func 反射恢复,而我们的任务是策略实例 bound method,恢复必失败
('无法恢复调度任务')→进程活着、分钟心跳正常、调度任务列表为空,开盘零成交零日志
(shadow_43/47/49 日志三段实锤:首启'已注册定时任务'→重启'无法恢复'→末代零任务)。
修法:live_strategy 加 process_initialize(引擎每次进程启动必调,resume 含),
装配抽 _setup 幂等(每进程一次);+3 回归测试(resume 只调 process_initialize
仍注册/双钩子不重复/facade 注入)。
P1 顺带根治:BrokerFacade 补注入 bullet_trade 顶层 run_daily/run_monthly——
此前 live facade 缺注入,策略自身 initialize 里的 b.run_daily 全部静默 no-op
(channel_test 无 _register_schedule 分支,4 账户连首启都不可能开仓);注入后
定时注册回归策略自身 initialize 单一事实源,_setup 不再调 _register_schedule
代注册(回测路径不变,runner_backtest._register_schedule 保留并补
ChannelTestStrategy 自注册分支消误导告警)。
829 测试绿
This commit is contained in:
@@ -142,6 +142,7 @@ def test_live_strategy_adapter_builds_all_strategies(monkeypatch):
|
||||
("momentum_timing", "MomentumTimingStrategy"),
|
||||
("value_selection", "ValueSelectionStrategy"),
|
||||
("small_cap", "SmallCapStrategy"),
|
||||
("channel_test", "ChannelTestStrategy"),
|
||||
# TET Phase2 副本:影子/实盘同样可发起(2026-08-16 VPS shadow#42 因缺项拉起即崩)
|
||||
("all_weather_ex", "AllWeatherExStrategy"),
|
||||
("momentum_timing_ex", "MomentumTimingExStrategy"),
|
||||
@@ -156,6 +157,88 @@ def test_live_strategy_adapter_builds_all_strategies(monkeypatch):
|
||||
live_strategy._build_live_strategy(_FakeProvider())
|
||||
|
||||
|
||||
def _patch_live_wiring(monkeypatch):
|
||||
"""打桩 bullet_trade 顶层 API:记录定时注册,provider 给假件。"""
|
||||
import bullet_trade.core as bt_core
|
||||
import bullet_trade.data.api as bt_data_api
|
||||
|
||||
registered = []
|
||||
monkeypatch.setattr(
|
||||
bt_core, "run_daily",
|
||||
lambda f, t, **kw: registered.append(
|
||||
("daily", getattr(f, "__name__", str(f)), t)))
|
||||
monkeypatch.setattr(
|
||||
bt_core, "run_monthly",
|
||||
lambda f, d, t, **kw: registered.append(
|
||||
("monthly", getattr(f, "__name__", str(f)), d, t)))
|
||||
|
||||
class _FakeProvider:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(bt_data_api, "get_data_provider", lambda: _FakeProvider())
|
||||
return registered
|
||||
|
||||
|
||||
def _reset_live_state():
|
||||
from sanguo_portfolio import live_strategy
|
||||
live_strategy._STATE["strategy"] = None
|
||||
live_strategy._STATE["wired"] = False
|
||||
|
||||
|
||||
def test_process_initialize_registers_tasks_on_resume_boot(monkeypatch):
|
||||
"""P0 回归:resume 重启(g 已恢复,引擎跳过 initialize 只调 process_initialize)
|
||||
定时任务仍要注册——2026-08-17 VPS 16 引擎重启后零任务空转、开盘零成交。"""
|
||||
from sanguo_portfolio import live_strategy
|
||||
|
||||
registered = _patch_live_wiring(monkeypatch)
|
||||
monkeypatch.setenv("SANGUO_LIVE_STRATEGY", "all_weather")
|
||||
_reset_live_state()
|
||||
|
||||
# 只调 process_initialize,不调 initialize(复刻 resume 路径)
|
||||
live_strategy.process_initialize(object())
|
||||
|
||||
names = {r[1] for r in registered}
|
||||
assert "prepare_stock_list" in names
|
||||
assert "monthly_adjustment" in names
|
||||
assert "stop_loss" in names
|
||||
assert live_strategy._STATE["wired"] is True
|
||||
|
||||
|
||||
def test_setup_idempotent_across_both_hooks(monkeypatch):
|
||||
"""新策略首启:引擎先调 initialize 再调 process_initialize——任务不重复注册,
|
||||
策略实例复用同一份。"""
|
||||
from sanguo_portfolio import live_strategy
|
||||
|
||||
registered = _patch_live_wiring(monkeypatch)
|
||||
monkeypatch.setenv("SANGUO_LIVE_STRATEGY", "momentum_timing")
|
||||
_reset_live_state()
|
||||
|
||||
live_strategy.initialize(object())
|
||||
first = live_strategy._STATE["strategy"]
|
||||
live_strategy.process_initialize(object())
|
||||
|
||||
assert live_strategy._STATE["strategy"] is first
|
||||
assert len([r for r in registered if r[1] == "handle_data"]) == 1
|
||||
|
||||
|
||||
def test_facade_injects_run_daily_for_channel_test(monkeypatch):
|
||||
"""channel_test 自注册依赖 facade.run_daily(缺注入时静默 no-op 永不开仓):
|
||||
process_initialize 后 9:35/10:45/13:45/14:30 四时点应经注入的 run_daily 挂上。"""
|
||||
import bullet_trade.core as bt_core
|
||||
from sanguo_portfolio import live_strategy
|
||||
|
||||
registered = _patch_live_wiring(monkeypatch)
|
||||
monkeypatch.setenv("SANGUO_LIVE_STRATEGY", "channel_test")
|
||||
_reset_live_state()
|
||||
|
||||
live_strategy.process_initialize(object())
|
||||
|
||||
daily_times = sorted(r[2] for r in registered if r[0] == "daily")
|
||||
assert daily_times == ["10:45", "13:45", "14:30", "9:35"]
|
||||
# facade 拿到的是 bullet_trade 顶层 run_daily 本尊,不是默认空 lambda
|
||||
assert live_strategy._STATE["strategy"].broker.run_daily is bt_core.run_daily
|
||||
|
||||
|
||||
def test_normalize_vt_symbol_bare_code_gets_exchange_suffix():
|
||||
"""CTA 实盘标的:裸 6 位码自动补交易所后缀(6→SSE,0/3→SZSE)。"""
|
||||
from sanguo_api.routes_live import _normalize_vt_symbol
|
||||
|
||||
Reference in New Issue
Block a user