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: