From e7c5defe64e4c4f7b26f17a0ae63ec7264de2508 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Thu, 20 Aug 2026 11:41:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(live):=20runtime=E5=89=AF=E6=9C=AC=E5=AD=A4?= =?UTF-8?q?=E7=AB=8B=E5=8A=A0=E8=BD=BDImportError=E6=A0=B9=E6=B2=BB?= =?UTF-8?q?=E2=80=94=E2=80=942026-08-20=E6=99=A812=E5=BC=95=E6=93=8E?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E5=BE=AA=E7=8E=AF=E4=BA=8B=E6=95=85(6?= =?UTF-8?q?=E5=AE=9E=E7=9B=98+6=E5=BD=B1=E5=AD=90=E8=87=AA=E5=BB=BA?= =?UTF-8?q?=E9=98=9F07:10=E8=B5=B7=E6=AF=8F60s=E5=B4=A9,=E5=BD=93=E6=97=A5?= =?UTF-8?q?0=E6=88=90=E4=BA=A4,9:05/9:30/9:35=E5=85=A8=E6=9C=AA=E8=A7=A6?= =?UTF-8?q?=E5=8F=91)=E2=80=94=E2=80=94=E6=A0=B9=E5=9B=A0=3Dlive=5Fstrateg?= =?UTF-8?q?y.py=E5=8F=B0=E8=B4=A6/B2=E4=B8=A4=E8=A1=8C=E7=9B=B8=E5=AF=B9?= =?UTF-8?q?=E5=AF=BC=E5=85=A5(from=20.=20import=20live=5Finstance=5Fledger?= =?UTF-8?q?/from=20.live=5Fportfolio=5Fproxy):=E6=96=87=E4=BB=B6=E8=A2=AB?= =?UTF-8?q?=5Finstance=5Fadapter=E9=80=90=E5=AD=97=E8=8A=82=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E5=88=B0runtime/<=E5=AE=9E=E4=BE=8B>/=E5=90=8Ebullet?= =?UTF-8?q?=5Ftrade=E4=BB=A5=E9=A1=B6=E5=B1=82=E6=A8=A1=E5=9D=97=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD(=E6=97=A0=E5=8C=85=E4=B8=8A=E4=B8=8B=E6=96=87)?= =?UTF-8?q?=E2=86=92ImportError;=E6=B5=8B=E8=AF=95=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=E7=9B=B2=E5=8C=BA=3D=E6=B5=8B=E8=AF=95=E4=BB=A5=E5=8C=85?= =?UTF-8?q?=E6=88=90=E5=91=98=E5=AF=BC=E5=85=A5=E7=9B=B8=E5=AF=B9=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=90=88=E6=B3=95,=E7=94=9F=E4=BA=A7=E5=AD=A4?= =?UTF-8?q?=E7=AB=8B=E5=8A=A0=E8=BD=BD=E6=89=8D=E5=B4=A9,nas-verify?= =?UTF-8?q?=E4=B8=8D=E5=90=AF=E5=BC=95=E6=93=8E=E2=80=94=E2=80=94=E4=BF=AE?= =?UTF-8?q?=3D=E6=94=B9=E7=BB=9D=E5=AF=B9=E5=AF=BC=E5=85=A5from=20sanguo?= =?UTF-8?q?=5Fportfolio=20import(=E5=BC=95=E6=93=8E=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=A0=B9=E5=9C=A8sys.path,=E5=90=8C=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=85=B6=E5=AE=83=E7=BB=9D=E5=AF=B9=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E5=B7=B2=E9=AA=8C=E8=AF=81=E5=8F=AF=E8=B7=91?= =?UTF-8?q?);=E6=8A=BD=5Fledger=5Ftools()=E5=8D=95=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E4=B8=A4=E7=A7=8D=E5=8A=A0=E8=BD=BD=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=E7=9B=B4=E6=89=93;+3=E6=B5=8B=E8=AF=95(?= =?UTF-8?q?=E5=AD=A4=E7=AB=8B=E5=89=AF=E6=9C=AC=E9=A1=B6=E5=B1=82import=5F?= =?UTF-8?q?module=E5=A4=8D=E7=8E=B0=E4=BA=8B=E6=95=85=E7=82=B9/=E5=8C=85?= =?UTF-8?q?=E5=86=85=E4=B8=8A=E4=B8=8B=E6=96=87/=E9=9D=99=E6=80=81?= =?UTF-8?q?=E7=A6=81from=20.=E5=9B=9E=E6=BD=AE);=5Finstance=5Fadapter?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=97=B6=E5=86=85=E5=AE=B9=E6=AF=94=E5=AF=B9?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=87=8D=E6=8B=B7=E2=86=92VPS=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=E5=90=8E=E4=B8=8B=E8=BD=AE=E5=BC=95=E6=93=8E=E9=87=8D?= =?UTF-8?q?=E5=90=AF=E8=87=AA=E6=84=88=E5=85=8D=E6=89=8B=E5=B7=A5=E6=B8=85?= =?UTF-8?q?runtime;=E9=A1=BA=E6=89=8B=E4=BF=AE2=E4=B8=AA=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E6=B5=8B=E8=AF=95(proxy=20fixture?= =?UTF-8?q?=E4=B8=8Esnapshot=E7=9A=84trade=5Fdate=3D2026-08-19=E2=86=92?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E4=BB=8A=E5=A4=A9,=E5=BD=93=E6=97=A5?= =?UTF-8?q?=E4=B9=B0=E5=85=A5=E8=B7=A8=E6=97=A5T+1=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E5=BF=85=E7=BF=BB=E7=BA=A2,CI=E4=B8=8D=E8=B7=91=E6=AD=A4?= =?UTF-8?q?=E5=A5=97=E4=BB=B6=E6=95=85=E6=98=A8=E6=97=A5=E6=9C=AA=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2);portfolio=20398=E7=BB=BF=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_portfolio/live_strategy.py | 19 ++++++- tests/portfolio/test_live_instance_ledger.py | 4 +- tests/portfolio/test_live_portfolio_proxy.py | 5 +- .../test_live_strategy_standalone_load.py | 54 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/portfolio/test_live_strategy_standalone_load.py 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}"