From 5d0f1cba79f96f80be39dd7c12d9fe7e386c780f Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:56:08 +0900 Subject: [PATCH] =?UTF-8?q?fix(indicators):=20=E7=BB=B4=E8=A1=A8=E6=B6=A8?= =?UTF-8?q?=E8=B7=8C=E5=81=9C=E4=BB=B7=E4=B8=BA=200=20=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=8A=8A=E5=85=A8=E9=83=A8=E6=A0=87=E7=9A=84=E5=88=A4?= =?UTF-8?q?=E6=88=90=E6=B6=A8=E5=81=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 冷路径只校验了「非空且 < 哨兵」, 维表 limit_up 为 0 (数据源未提供该字段 的占位值) 会被当成权威涨停价, 使「raw_close >= 0 - 0.005」恒成立 —— 当日 所有标的进涨停名单、连板数一路累加; 跌停侧反过来永远判不出跌停。实时路径 _compute_limit_signals_today 已有 >0 守卫, 冷路径补齐同一守卫。 --- backend/app/indicators/pipeline.py | 5 +++ backend/tests/test_price_limits.py | 62 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/backend/app/indicators/pipeline.py b/backend/app/indicators/pipeline.py index 25917d0..f5c85c9 100644 --- a/backend/app/indicators/pipeline.py +++ b/backend/app/indicators/pipeline.py @@ -803,9 +803,13 @@ def compute_limit_signals( else: authoritative_date = pl.col("date") == pl.col("date").max() if "limit_up" in df.columns: + # >0 与实时路径 (_compute_limit_signals_today) 同守卫: 维表 limit_up 为 0 + # (数据源未提供该字段的占位值) 不是权威价, 直接采用会让 raw_close >= -0.005 + # 恒成立, 全部标的被判涨停。 effective_limit_up = pl.when( authoritative_date & pl.col("limit_up").is_not_null() + & (pl.col("limit_up") > 0) & (pl.col("limit_up") < _SENTINEL) ).then(pl.col("limit_up")).otherwise(pl.col("_theoretical_limit_up")) else: @@ -814,6 +818,7 @@ def compute_limit_signals( effective_limit_down = pl.when( authoritative_date & pl.col("limit_down").is_not_null() + & (pl.col("limit_down") > 0) & (pl.col("limit_down") < _SENTINEL) ).then(pl.col("limit_down")).otherwise(pl.col("_theoretical_limit_down")) else: diff --git a/backend/tests/test_price_limits.py b/backend/tests/test_price_limits.py index 8857621..34a3a41 100644 --- a/backend/tests/test_price_limits.py +++ b/backend/tests/test_price_limits.py @@ -189,6 +189,68 @@ def test_daily_limit_prices_require_matching_instrument_date(instrument_as_of, e assert "_instrument_as_of" not in result.columns +def test_daily_limit_prices_ignore_zero_placeholder_and_match_realtime(): + """维表涨跌停价为 0 (数据源未提供该字段的占位值) 时必须回退理论价。 + + 直接采用 0 会让「raw_close >= 0 - 0.005」恒成立, 当日所有标的被判涨停, + 连板数一路累加; 跌停侧反过来永远判不出跌停。实时路径 + (_compute_limit_signals_today) 已有 >0 守卫, 冷路径必须同口径。 + """ + instruments = pl.DataFrame({ + "symbol": ["600001.SH"], + "name": ["普通股"], + "limit_up": [0.0], + "limit_down": [0.0], + "as_of": [date(2026, 7, 20)], + }) + + # 只涨 0.5%: 不是涨停 + mild = pipeline.compute_limit_signals( + _daily_limit_rows(10.05), + instruments, + needed={"signal_limit_up", "consecutive_limit_ups"}, + ) + assert mild["signal_limit_up"][-1] is False + assert mild["consecutive_limit_ups"][-1] == 0 + + # 真涨停 11.00 = 10.00 x 1.1: 理论价兜底后仍须判出 + sealed = pipeline.compute_limit_signals( + _daily_limit_rows(11.00), + instruments, + needed={"signal_limit_up", "consecutive_limit_ups"}, + ) + assert sealed["signal_limit_up"][-1] is True + assert sealed["consecutive_limit_ups"][-1] == 1 + + # 真跌停 9.00 = 10.00 x 0.9: 占位 0 不得让跌停漏判 + floored = pipeline.compute_limit_signals( + _daily_limit_rows(9.00), + instruments, + needed={"signal_limit_down"}, + ) + assert floored["signal_limit_down"][-1] is True + + # 与实时路径同一份维表同一结论 + realtime = pipeline._compute_limit_signals_today( + pl.DataFrame({ + "symbol": ["600001.SH"], + "date": [date(2026, 7, 20)], + "open": [10.05], + "high": [10.05], + "low": [10.05], + "close": [10.05], + "raw_close": [10.05], + "raw_high": [10.05], + "raw_low": [10.05], + "_prev_close_raw": [10.0], + "volume": [1000.0], + }), + instruments, + ) + assert realtime["signal_limit_up"][0] is False + assert mild["signal_limit_up"][-1] is realtime["signal_limit_up"][0] + + def test_realtime_limit_prices_ignore_stale_instrument_date(): today = date(2026, 7, 20) rows = pl.DataFrame({