fix(live): runtime副本孤立加载ImportError根治——2026-08-20晨12引擎崩溃循环事故(6实盘+6影子自建队07:10起每60s崩,当日0成交,9:05/9:30/9:35全未触发)——根因=live_strategy.py台账/B2两行相对导入(from . import live_instance_ledger/from .live_portfolio_proxy):文件被_instance_adapter逐字节复制到runtime/<实例>/后bullet_trade以顶层模块加载(无包上下文)→ImportError;测试全绿盲区=测试以包成员导入相对导入合法,生产孤立加载才崩,nas-verify不启引擎——修=改绝对导入from sanguo_portfolio import(引擎进程项目根在sys.path,同文件其它绝对导入生产已验证可跑);抽_ledger_tools()单函数两种加载上下文单测直打;+3测试(孤立副本顶层import_module复现事故点/包内上下文/静态禁from .回潮);_instance_adapter启动时内容比对自动重拷→VPS落地后下轮引擎重启自愈免手工清runtime;顺手修2个日期硬编码测试(proxy fixture与snapshot的trade_date=2026-08-19→动态今天,当日买入跨日T+1断言必翻红,CI不跑此套件故昨日未暴露);portfolio 398绿 [vps]

This commit is contained in:
2026-08-20 11:41:47 +08:00
parent 9fa2f933ee
commit e7c5defe64
4 changed files with 78 additions and 4 deletions
+17 -2
View File
@@ -76,6 +76,22 @@ def _build_live_strategy(provider):
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 + 定时任务。
@@ -116,7 +132,7 @@ def _setup(context):
# 实例持仓通道(2026-08-19 共享QMT账户互卖根治):runner_live 已 set_active
# 时注入,策略侧 getattr(broker,'get_instance_positions',None) 消费;
# 回测/测试无账本 → 保持 None,策略回退 context.portfolio
from . import live_instance_ledger as _lil
_lil, _wrap_sched = _ledger_tools()
_ledger = _lil.get_active()
if _ledger is not None:
strategy.broker.get_instance_positions = _ledger.positions_view
@@ -125,7 +141,6 @@ def _setup(context):
# B2 定寸虚拟化:经 facade 注册的定时回调收到代理 context(portfolio=实例
# 账本视图),策略定寸/资金守卫不再被全账户污染;引擎内部仍看真实账户。
# 无账本(回测/影子) → wrap_scheduler 原样返回,行为零改动。
from .live_portfolio_proxy import wrap_scheduler as _wrap_sched
strategy.broker.run_daily = _wrap_sched(bt_run_daily, _ledger)
strategy.broker.run_monthly = _wrap_sched(bt_run_monthly, _ledger)
# A 股费用 + 滑点(与回测默认一致)
+3 -1
View File
@@ -167,7 +167,9 @@ class TestAttributionAndSnapshot:
db = str(tmp_path / "live.db")
init_db(db)
led = LiveInstanceLedger(initial_cash=1_000_000)
led.apply_trade(True, "000001.XSHE", 10.0, 100, "t1", "2026-08-19")
# trade_date=今天(动态):快照按 now 判 T+1,写死日期跨日必翻红(frozen 归 0)
led.apply_trade(True, "000001.XSHE", 10.0, 100, "t1",
datetime.now().strftime("%Y-%m-%d"))
engine = _fake_engine()
_snapshot_once(engine, db, 44, led)
+4 -1
View File
@@ -4,6 +4,7 @@
全账户污染);现价从真 portfolio 透传(市场数据非所有权);未覆盖属性
fail-fast;无账本(回测/影子)零改动。
"""
from datetime import date
from types import SimpleNamespace
import pytest
@@ -29,8 +30,10 @@ class _RealPos:
def ledger():
led = LiveInstanceLedger(initial_cash=1_000_000)
# 买入 510300@4.74×4200(费用=est) → cash 减,持仓进账本
# trade_date=今天(动态):T+1 断言语义=「当日买入锁定」,写死日期跨日必翻红
led.apply_trade(is_buy=True, symbol="510300.XSHG", price=4.74,
volume=4200, trade_id="t1", trade_date="2026-08-19")
volume=4200, trade_id="t1",
trade_date=date.today().isoformat())
return led
@@ -0,0 +1,54 @@
"""runtime 副本孤立加载回归(2026-08-20 事故)。
生产路径 = runner_live/_shadow 的 _instance_adapter 把
sanguo_portfolio/live_strategy.py 逐字节复制到 runtime/<实例>/live_strategy.py,
bullet_trade 以**顶层模块**加载(无包上下文)。历史上 ``from . import`` 相对导入
在该上下文直接 ImportError → 12 引擎(6实盘+6影子)自建队起崩溃循环 4 小时、
当日 0 成交;测试全绿是因为测试以包成员导入,相对导入合法——加载方式不一致
就是盲区。本测试按生产方式加载副本并直接调用 _ledger_tools(事故行),
相对导入复发会在本地测试就红(注:CI 只跑 tests/data_platform,本文件靠
本地 venv310 会话验收,属已知 CI 覆盖缺口)。
"""
from __future__ import annotations
import importlib
import shutil
import sys
from pathlib import Path
import pytest
ADAPTER = Path(__file__).resolve().parents[2] / "sanguo_portfolio" / "live_strategy.py"
@pytest.fixture
def standalone_module(tmp_path, monkeypatch):
"""按生产方式加载:拷副本到独立目录 → sys.path 指向该目录 → 顶层 import。"""
dst = tmp_path / "live_strategy.py"
shutil.copyfile(ADAPTER, dst)
monkeypatch.syspath_prepend(str(tmp_path))
monkeypatch.delitem(sys.modules, "live_strategy", raising=False)
return importlib.import_module("live_strategy")
def test_ledger_tools_under_standalone_load(standalone_module):
"""孤立加载(无包上下文)下 _ledger_tools 必须可用——2026-08-20 事故复现点。"""
lil, wrap = standalone_module._ledger_tools()
assert hasattr(lil, "get_active")
assert callable(wrap)
def test_ledger_tools_under_package_load():
"""包内上下文(测试/回测)同样可用——两种加载方式都必须成立。"""
from sanguo_portfolio import live_strategy as pkg_mod
lil, wrap = pkg_mod._ledger_tools()
assert hasattr(lil, "get_active")
assert callable(wrap)
def test_adapter_has_no_relative_imports():
"""适配文件禁止 ``from .`` 相对导入(复制出去必崩,静态兜底防回潮)。"""
for ln in ADAPTER.read_text(encoding="utf-8").splitlines():
s = ln.strip()
assert not s.startswith("from ."), f"相对导入残留: {s}"