diff --git a/sanguo_portfolio/live_strategy.py b/sanguo_portfolio/live_strategy.py index 574d808..08b4897 100644 --- a/sanguo_portfolio/live_strategy.py +++ b/sanguo_portfolio/live_strategy.py @@ -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 股费用 + 滑点(与回测默认一致) diff --git a/tests/portfolio/test_live_instance_ledger.py b/tests/portfolio/test_live_instance_ledger.py index a98b8a3..95a4e99 100644 --- a/tests/portfolio/test_live_instance_ledger.py +++ b/tests/portfolio/test_live_instance_ledger.py @@ -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) diff --git a/tests/portfolio/test_live_portfolio_proxy.py b/tests/portfolio/test_live_portfolio_proxy.py index d35d53e..a07619b 100644 --- a/tests/portfolio/test_live_portfolio_proxy.py +++ b/tests/portfolio/test_live_portfolio_proxy.py @@ -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 diff --git a/tests/portfolio/test_live_strategy_standalone_load.py b/tests/portfolio/test_live_strategy_standalone_load.py new file mode 100644 index 0000000..7e4955a --- /dev/null +++ b/tests/portfolio/test_live_strategy_standalone_load.py @@ -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}"