From b9315ef7e7dbdb08c5233842dc34f4795c5921a6 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sat, 15 Aug 2026 00:02:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(tests):=20Mac=E6=9C=AC=E5=9C=B09=E4=B8=AA?= =?UTF-8?q?=E6=97=A2=E6=9C=89=E6=B5=8B=E8=AF=95=E5=A4=B1=E8=B4=A5=E6=A0=B9?= =?UTF-8?q?=E6=B2=BB=E2=80=94=E2=80=94test=5Fcta=5Fengine=E7=9A=84sys.modu?= =?UTF-8?q?les=20mock=E6=B3=A8=E5=85=A5=E4=BB=8Eimport=E6=9C=9F=E6=8C=AA?= =?UTF-8?q?=E8=BF=9Bmodule-scoped=20fixture(pytest=E5=85=A8=E9=87=8Fcollec?= =?UTF-8?q?tion=E5=85=88=E4=BA=8E=E6=89=A7=E8=A1=8C,import=E6=9C=9F?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E6=B1=A1=E6=9F=93=E5=90=8E=E6=94=B6=E9=9B=86?= =?UTF-8?q?=E7=9A=84metrics/datareader/factor)+venv310=E6=8C=89lock?= =?UTF-8?q?=E8=A1=A5=E8=A3=85empyrical-reloaded=3D=3D0.5.12;=E5=BD=B1?= =?UTF-8?q?=E5=AD=90=E6=9F=9C=E5=8F=B0=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=A4=B4=E6=9B=B4=E6=96=B0(P1=E5=B7=B2=E4=B8=8A=E7=BA=BF/P3?= =?UTF-8?q?=E5=89=8D=E5=8D=8A=E5=B7=B2=E5=AE=9E=E7=8E=B0)=20[nas]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/paper-shadow-desk-design.md | 11 ++-- tests/backtest/test_cta_engine.py | 74 +++++++++++++------------ 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/docs/design/paper-shadow-desk-design.md b/docs/design/paper-shadow-desk-design.md index 5a1a1e1..1d8f9ce 100644 --- a/docs/design/paper-shadow-desk-design.md +++ b/docs/design/paper-shadow-desk-design.md @@ -1,7 +1,8 @@ # 模拟盘影子交易柜台设计 v2(验收第四轮 #4) -> 状态:设计稿 v2(2026-08-14,按用户反馈重写驱动方式)。**P0 对账已通过(2026-08-14), -> P1 未动工**。 +> 状态:设计稿 v2(2026-08-14,按用户反馈重写驱动方式)。**P0 对账已通过(2026-08-14); +> P1 影子柜台已上线(2026-08-14 晚,shadow#39 实跑)**;P3 前半(双轨对账报表)已实现(534a06a)。 +> v1 的「schtask 每 15 分钟定时拉数据」已废弃——用户明确:数据要与实盘完全一样 > v1 的「schtask 每 15 分钟定时拉数据」已废弃——用户明确:数据要与实盘完全一样 > 由 miniQMT 实时提供,步长由策略自己的周期决定(tick 到月线),15 分钟只是举例。 @@ -97,9 +98,9 @@ VPS(miniQMT 客户端保持登录,唯一有实时行情的机器) | 期 | 内容 | 验收 | |----|------|------| | P0 | 对账 spike:checkpoint 续跑 vs 全量重放,末持仓/净值一致(含跨除权用例) | ✅ **已通过(2026-08-14,VPS 真数据)**:all_weather 2024 全年(242 交易日)全量重放 vs 3 段续跑(5月初/9月初分段,跨多次除权),期末 9 只持仓逐只一致、现金 2,256.29/总值 1,335,875.29 相对偏差 <1e-6、净值曲线 <1e-6。脚本 `scripts/shadow_desk/spike_p0_checkpoint_replay.py`,runner 通道 `--initial-positions`+`final_portfolio`(1e7d696) | -| P1 | 影子柜台常驻进程:bar 级订阅(分钟/日线)+ 实时价撮合 + 虚拟账户 + 落库 | 盘中下单成交可见,前端实时刷新 | -| P2 | tick 推送接入(tick 级策略)+ 月线/周线合成 | tick 策略可跑 | -| P3 | 日终对账自动化 + NAS 交叉对照;影子单同步 QMT(真实现盘前置) | — | +| P1 | 影子柜台常驻进程:bar 级订阅(分钟/日线)+ 实时价撮合 + 虚拟账户 + 落库 | ✅ **已上线(2026-08-14 晚)**:supervisor schtask 拉起 shadow 进程,shadow#39(all_weather 影子)实跑;ShadowBroker 5 协议方法补齐、bullet_trade 两层实例锁(b27c3b4/e1cafac),四实例齐跑 live3/4/5+shadow39。涨跌停/停牌拒单 61cc01e | +| P2 | tick 推送接入(tick 级策略)+ 月线/周线合成 | tick 策略可跑(挂起:等双轨验证稳) | +| P3 | 日终对账自动化 + NAS 交叉对照;影子单同步 QMT(真实现盘前置) | 前半 ✅ **双轨对账报表(534a06a,2026-08-15)**:reconcile 四指标+自动配对 live5↔shadow39+API/CLI/supervisor 日终兜底;后半(影子单同步 QMT)挂起 | 工程分期不影响使用语义:策略侧从第一天就按「声明周期」配置, tick 级在 P2 放开。 diff --git a/tests/backtest/test_cta_engine.py b/tests/backtest/test_cta_engine.py index c70310f..2327a8f 100644 --- a/tests/backtest/test_cta_engine.py +++ b/tests/backtest/test_cta_engine.py @@ -1,14 +1,23 @@ """Tests for sanguo_backtest.cta_engine module.""" -# Mock vnpy and tzlocal modules before importing anything that depends on them +# Mock vnpy and tzlocal modules before importing anything that depends on them。 +# ⚠️注入必须在 fixture 内(测试期)而非模块 import 期:pytest 全量 collection 先于任何 +# 测试执行,import 期注入的 sys.modules mock 会污染后收集的 datareader/metrics/factor +# 模块(它们 import 到的是 MagicMock)——2026-08-15 修,见 tests 全量 9 failed 根因。 import sys +import importlib import importlib.util from unittest.mock import MagicMock +import pytest +import json +from unittest.mock import Mock, patch, MagicMock +from datetime import datetime +from pathlib import Path +import pandas as pd + # Only mock these when the real module is NOT importable (e.g. local env missing empyrical). # In the container (authoritative test env) everything imports fine, so NO mock is installed -# → zero sys.modules pollution leaking into other test modules at collection time. -# (Previous unconditional sys.modules[...]=MagicMock() here broke datareader/spike/factor/metrics -# tests because it ran at collection time and never restored.) +# → zero sys.modules pollution leaking into other test modules. mock_tzlocal = MagicMock() mock_tzlocal.get_localzone_name = MagicMock(return_value="UTC") _MOCK_FACTORIES = { @@ -18,41 +27,36 @@ _MOCK_FACTORIES = { "vnpy.trader.object": MagicMock, "vnpy.trader.database": MagicMock, "vnpy_ctastrategy.backtesting": MagicMock, - "empyrical": MagicMock, } -_SAVED_MODULES = {} -for _name, _factory in _MOCK_FACTORIES.items(): - try: - _found = importlib.util.find_spec(_name) - except ModuleNotFoundError: - _found = None - if _found is None: - _SAVED_MODULES[_name] = sys.modules.get(_name) - sys.modules[_name] = _factory() - -import pytest -import json -from unittest.mock import Mock, patch, MagicMock -from datetime import datetime -from pathlib import Path -import pandas as pd -from sanguo_backtest.cta_engine import run_cta_backtest @pytest.fixture(scope="module", autouse=True) -def _restore_modules_after(): - """Restore sys.modules after this module's tests, so downstream tests import the - REAL vnpy/empyrical instead of the mocks we installed above. Also drop cta_engine/ - metrics from the module cache (they were imported while mocks were active) so they - re-import fresh with real dependencies.""" - yield - for cached in ("sanguo_backtest.cta_engine", "sanguo_backtest.metrics", "sanguo_backtest.ashare_engine"): - sys.modules.pop(cached, None) - for k, orig in _SAVED_MODULES.items(): - if orig is None: - sys.modules.pop(k, None) - else: - sys.modules[k] = orig +def _mock_deps_and_import_cta_engine(): + """module 级唯一 mock 窗口:装 mock(仅当真模块不可导入)→ import cta_engine → + 测试 → teardown 恢复 sys.modules 并清 sanguo_backtest 模块缓存(它们是在 mock + 生效期间 import 的,须弹出让后续测试用真依赖重新 import)。""" + saved = {} + for name, factory in _MOCK_FACTORIES.items(): + try: + found = importlib.util.find_spec(name) + except ModuleNotFoundError: + found = None + if found is None: + saved[name] = sys.modules.get(name) + sys.modules[name] = factory() + try: + cta_engine = importlib.import_module("sanguo_backtest.cta_engine") + globals()["run_cta_backtest"] = cta_engine.run_cta_backtest + yield + finally: + for cached in ("sanguo_backtest.cta_engine", "sanguo_backtest.metrics", "sanguo_backtest.ashare_engine"): + sys.modules.pop(cached, None) + for k, orig in saved.items(): + if orig is None: + sys.modules.pop(k, None) + else: + sys.modules[k] = orig + globals().pop("run_cta_backtest", None) class TestRunCtaBacktest: