From 384bcc56d738527cbce83a56ac023c12c844a18c Mon Sep 17 00:00:00 2001 From: claude_dev Date: Wed, 29 Jul 2026 21:17:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(data):=20=E4=BF=AE=E4=B8=89=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=20session=20=E5=8F=8D=E9=A6=88=E7=9A=84=203=20?= =?UTF-8?q?=E4=B8=AA=E6=95=B0=E6=8D=AE=E5=B1=82=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D1: 删 test_circuit_breaker.py(测已归档 raw_redownload.check_circuit_breaker 死代码,全仓零活跃引用,致 data_platform 套件 collection error) D2: datareader.py read_db_daily/read_index_daily 两处 vnpy_db 硬访问→.get()+清晰报错防崩溃(根治切 dbbardata 读指数列待办) D3: high_limit/low_limit close±10% 兜底是有意设计非 bug(填 NaN 会复活 bullet_trade 误判停牌)—get_price 加 round(.,2) 对齐 get_current_tick 口径;测试期望从 NaN 改兜底估算 --- sanguo_data/datareader.py | 10 +- .../providers/local_unified_provider.py | 4 +- tests/data_platform/test_circuit_breaker.py | 155 ------------------ .../portfolio/test_local_unified_provider.py | 9 +- 4 files changed, 15 insertions(+), 163 deletions(-) delete mode 100644 tests/data_platform/test_circuit_breaker.py diff --git a/sanguo_data/datareader.py b/sanguo_data/datareader.py index 3d64853..8abae60 100644 --- a/sanguo_data/datareader.py +++ b/sanguo_data/datareader.py @@ -59,7 +59,10 @@ def read_db_daily(symbol: str, start: str, end: str, cfg) -> list[BarData]: from vnpy.trader.database import get_database # lazy:避免模块 import 依赖数据库驱动 # Configure vnpy database SETTINGS before calling get_database() SETTINGS["database.name"] = "sqlite" - SETTINGS["database.database"] = cfg.data_paths["vnpy_db"] + vnpy_db = getattr(cfg, "data_paths", {}).get("vnpy_db") + if not vnpy_db: + raise RuntimeError("config 缺 data_paths.vnpy_db — 检查 backtest.yaml 初始化") + SETTINGS["database.database"] = vnpy_db db = get_database() start_dt = datetime.strptime(start, "%Y-%m-%d") @@ -139,7 +142,10 @@ def read_index_daily(code: str, start, end, cfg) -> pd.DataFrame: # 配置 vnpy DB(与 read_db_daily 同模式) SETTINGS["database.name"] = "sqlite" - SETTINGS["database.database"] = cfg.data_paths["vnpy_db"] + vnpy_db = getattr(cfg, "data_paths", {}).get("vnpy_db") + if not vnpy_db: + raise RuntimeError("config 缺 data_paths.vnpy_db — 检查 backtest.yaml 初始化") + SETTINGS["database.database"] = vnpy_db db = get_database() bars = db.load_bar_data( diff --git a/sanguo_portfolio/providers/local_unified_provider.py b/sanguo_portfolio/providers/local_unified_provider.py index ca670d0..bf1ab8e 100644 --- a/sanguo_portfolio/providers/local_unified_provider.py +++ b/sanguo_portfolio/providers/local_unified_provider.py @@ -240,9 +240,9 @@ class LocalUnifiedProvider(DataProvider): # type: ignore[misc] if f == "paused": df[f] = False elif f == "high_limit" and "close" in df.columns: - df[f] = df["close"] * 1.1 + df[f] = (df["close"] * 1.1).round(2) elif f == "low_limit" and "close" in df.columns: - df[f] = df["close"] * 0.9 + df[f] = (df["close"] * 0.9).round(2) else: df[f] = float("nan") df = df[[f for f in fields if f in df.columns]] diff --git a/tests/data_platform/test_circuit_breaker.py b/tests/data_platform/test_circuit_breaker.py deleted file mode 100644 index 19ec481..0000000 --- a/tests/data_platform/test_circuit_breaker.py +++ /dev/null @@ -1,155 +0,0 @@ -"""断路器单元测试(task: Phase 2 可靠性增强)。 - -测试 check_circuit_breaker 纯函数——不启动整个下载流程, -直接构造 recent_results 列表验证触发逻辑。 - -覆盖场景: - 1. 35% 失败率 → 触发 - 2. 25% 失败率 → 不触发 - 3. 北交所 920xxx 的 fail 不计入 → 不触发 - 4. 不足窗口 → 不触发 - 5. 恰好 30% → 不触发(阈值是 > 0.30,不含等于) -""" -import os -import sys - -import pytest - -_SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "scripts", "data_platform") -_SCRIPT_DIR = os.path.abspath(_SCRIPT_DIR) -if _SCRIPT_DIR not in sys.path: - sys.path.insert(0, _SCRIPT_DIR) - -from raw_redownload import ( # noqa: E402 - CIRCUIT_BREAKER_FAIL_RATE, - CIRCUIT_BREAKER_WINDOW, - KNOWN_UNSUPPORTED_PREFIX, - check_circuit_breaker, -) - - -def _make_results(n_ok: int, n_fail: int, fail_prefix: str = "600") -> list: - """构造 recent_results 列表:n_ok 个 ok + n_fail 个 fail。 - - fail_prefix 控制失败码的前缀(用于测试北交所排除)。 - """ - ok_list = [("600001", True)] * n_ok - fail_list = [(f"{fail_prefix}9999", False)] * n_fail - return ok_list + fail_list - - -class TestCircuitBreakerTrigger: - """断路器触发阈值测试。""" - - def test_35_percent_fail_triggers(self): - """200 只里 70 只 fail(35%)→ 触发。""" - results = _make_results(n_ok=130, n_fail=70) - assert len(results) == 200 - assert check_circuit_breaker(results) is True - - def test_25_percent_fail_no_trigger(self): - """200 只里 50 只 fail(25%)→ 不触发。""" - results = _make_results(n_ok=150, n_fail=50) - assert len(results) == 200 - assert check_circuit_breaker(results) is False - - def test_exactly_30_percent_no_trigger(self): - """恰好 30%(60/200)→ 不触发(阈值是 > 0.30,不含等于)。""" - results = _make_results(n_ok=140, n_fail=60) - assert len(results) == 200 - fail_rate = 60 / 200 - assert fail_rate == pytest.approx(CIRCUIT_BREAKER_FAIL_RATE) - assert check_circuit_breaker(results) is False - - def test_31_percent_triggers(self): - """31% 失败率 → 触发(刚过阈值)。""" - results = _make_results(n_ok=138, n_fail=62) - assert len(results) == 200 - assert check_circuit_breaker(results) is True - - -class TestCircuitBreakerBjExclusion: - """北交所码不计入断路器分母测试。""" - - def test_bj_fails_excluded_from_denominator(self): - """北交所 920xxx 的 fail 不计入 → 不触发。 - - 200 只非北交所全部 ok + 100 只北交所全部 fail: - 有效分母=200,有效失败=0 → 0% → 不触发。 - """ - ok_list = [("600001", True)] * 200 - bj_fail_list = [("920001", False)] * 100 - results = ok_list + bj_fail_list - assert check_circuit_breaker(results) is False - - def test_bj_fails_do_not_inflate_rate(self): - """北交所 fail 混在 200 窗口内不抬高失败率。 - - 150 只非北交所 ok + 50 只北交所 fail = 200 只总数: - 有效分母=150(< 窗口 200)→ 不足窗口,不触发。 - """ - ok_list = [("600001", True)] * 150 - bj_fail_list = [("920001", False)] * 50 - results = ok_list + bj_fail_list - assert check_circuit_breaker(results) is False - - def test_mixed_bj_and_normal_fails(self): - """混合场景:200 非北交所(60 fail=30%)+ 50 北交所 fail。 - - 有效:200 非北交所,60 fail = 30%,恰好阈值(> 0.30 不含等于)→ 不触发。 - 北交所 50 fail 被排除,不影响计算。 - """ - ok_normal = [("600001", True)] * 140 - fail_normal = [("600002", False)] * 60 - fail_bj = [("920001", False)] * 50 - results = ok_normal + fail_normal + fail_bj - assert check_circuit_breaker(results) is False - - def test_all_bj_prefixes_excluded(self): - """所有已知不支持前缀都排除:920/921/83/87。""" - ok_list = [("600001", True)] * 200 - bj_fails = ( - [("920001", False)] * 30 - + [("921002", False)] * 30 - + [("830003", False)] * 30 - + [("870004", False)] * 30 - ) - results = ok_list + bj_fails - assert check_circuit_breaker(results) is False - - -class TestCircuitBreakerWindow: - """窗口大小边界测试。""" - - def test_insufficient_window_no_trigger(self): - """不足 200 只 → 不触发(即使全部 fail)。""" - results = [("600001", False)] * 199 - assert check_circuit_breaker(results) is False - - def test_exactly_window_triggers_if_high_fail(self): - """恰好 200 只且失败率超阈值 → 触发。""" - results = _make_results(n_ok=100, n_fail=100) - assert len(results) == 200 - assert check_circuit_breaker(results) is True - - def test_empty_results_no_trigger(self): - """空列表 → 不触发。""" - assert check_circuit_breaker([]) is False - - def test_all_ok_no_trigger(self): - """200 只全部 ok → 不触发。""" - results = [("600001", True)] * 200 - assert check_circuit_breaker(results) is False - - -class TestCircuitBreakerConstants: - """常量值校验(防止意外修改)。""" - - def test_window_is_200(self): - assert CIRCUIT_BREAKER_WINDOW == 200 - - def test_fail_rate_is_030(self): - assert CIRCUIT_BREAKER_FAIL_RATE == 0.30 - - def test_known_unsupported_prefixes(self): - assert KNOWN_UNSUPPORTED_PREFIX == ("920", "921", "83", "87") diff --git a/tests/portfolio/test_local_unified_provider.py b/tests/portfolio/test_local_unified_provider.py index b145861..c7040eb 100644 --- a/tests/portfolio/test_local_unified_provider.py +++ b/tests/portfolio/test_local_unified_provider.py @@ -227,8 +227,9 @@ class TestGetPrice: assert len(df) == 2 assert "600519.XSHG" in set(df["code"]) - def test_fields_with_missing_column_fills_nan(self, unified_provider): - # high_limit 不在 dbbardata → NaN 降级(策略 prepare_stock_list 涨停识别降级) + def test_fields_with_missing_column_fills_fallback(self, unified_provider): + # high_limit 不在 dbbardata → 按 close×1.1 兜底估算(round 2, 与 get_current_tick 同口径) + # 注:不填 NaN — 填 NaN 会被 bullet_trade 误判停牌导致订单全 cancel(unified-provider-paused-nan-bug) df = unified_provider.get_price( "600519.XSHG", end_date="2024-06-20", @@ -237,8 +238,8 @@ class TestGetPrice: fields=["close", "high_limit"], ) assert "high_limit" in df.columns - # high_limit NaN(不崩) - assert pd.isna(df.iloc[0]["high_limit"]) or df.iloc[0]["high_limit"] != df.iloc[0]["high_limit"] + expected = round(df.iloc[0]["close"] * 1.1, 2) + assert abs(df.iloc[0]["high_limit"] - expected) < 1e-6 def test_minute_frequency_returns_empty(self, unified_provider): # 1m 频率无数据 → 返空 DataFrame