From 09ec2a55c3d8233ef2308b664ebdfba041b67103 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Thu, 2 Jul 2026 12:28:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BD=93=E6=97=A5=E6=B6=A8=E8=B7=8C?= =?UTF-8?q?=E5=81=9C=E5=88=A4=E5=AE=9A=E6=94=B9=E7=94=A8=E7=BB=B4=E8=A1=A8?= =?UTF-8?q?=20limit=5Fup/limit=5Fdown=20=E6=9D=83=E5=A8=81=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原逻辑用 _limit_price(prev_close, 推断涨幅) 自算理论涨停价, 涨幅靠 symbol 前缀 + ST 名字匹配推断, 存在两类缺陷: 1. 浮点精度: 自算涨停价比真实价多 0.01, 超出 0.005 容差导致漏判 (12 只) 2. ST 匹配漏洞: 名字含 "S" 不含 "ST" (如 S佳通) 被漏判 ST 5% 档 (1 只) 改为优先读 instruments 维表的 limit_up/limit_down (交易所级别精确价), 仅新股 (limit_up 为 null 或哨兵 100000) 回退自算。输出 schema 不变, 下游 depth_service/screener/market_overview 零改动。 历史路径 compute_limit_signals 不动 (instruments 仅存当天 as_of)。 --- backend/app/indicators/pipeline.py | 32 +++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/app/indicators/pipeline.py b/backend/app/indicators/pipeline.py index dcab138..c753943 100644 --- a/backend/app/indicators/pipeline.py +++ b/backend/app/indicators/pipeline.py @@ -1381,8 +1381,9 @@ def compute_enriched_today( def _compute_limit_signals_today(df: pl.DataFrame, instruments: pl.DataFrame) -> pl.DataFrame: """盘中增量版的涨跌停/换手率/炸板/连板计算。""" inst_cols = ["symbol"] - if "float_shares" in instruments.columns: - inst_cols.append("float_shares") + for c in ["float_shares", "limit_up", "limit_down"]: + if c in instruments.columns: + inst_cols.append(c) inst_subset = instruments.select(inst_cols).unique(subset=["symbol"]) if "name" in instruments.columns: st_flag = ( @@ -1431,14 +1432,31 @@ def _compute_limit_signals_today(df: pl.DataFrame, instruments: pl.DataFrame) -> limit_up_price = _limit_price(prev_raw, limit_pct, up=True) limit_down_price = _limit_price(prev_raw, limit_pct, up=False) + # 生效涨跌停价: 优先用维表权威值 (instruments.limit_up/down, 交易所级别精确价), + # 维表缺失 (新股上市前 5 日: limit_up 为 null 或哨兵 100000) 回退自算理论价。 + # 哨兵阈值 10000 用于识别 "新股无涨跌停限制" 的占位值 (实际涨停价不可能上万)。 + _SENTINEL = 10000.0 + if "limit_up" in df.columns: + effective_limit_up = pl.when( + pl.col("limit_up").is_not_null() & (pl.col("limit_up") < _SENTINEL) + ).then(pl.col("limit_up")).otherwise(limit_up_price) + else: + effective_limit_up = limit_up_price + if "limit_down" in df.columns: + effective_limit_down = pl.when( + pl.col("limit_down").is_not_null() & (pl.col("limit_down") < _SENTINEL) + ).then(pl.col("limit_down")).otherwise(limit_down_price) + else: + effective_limit_down = limit_down_price + is_limit_up = ( pl.when((prev_raw > 0) & (pl.col("raw_close") > 0)) - .then((pl.col("raw_close") - limit_up_price).abs() < 0.005) + .then(pl.col("raw_close") >= (effective_limit_up - 0.005)) .otherwise(None).cast(pl.Boolean) ) is_limit_down = ( pl.when((prev_raw > 0) & (pl.col("raw_close") > 0)) - .then((pl.col("raw_close") - limit_down_price).abs() < 0.005) + .then(pl.col("raw_close") <= (effective_limit_down + 0.005)) .otherwise(None).cast(pl.Boolean) ) @@ -1449,7 +1467,7 @@ def _compute_limit_signals_today(df: pl.DataFrame, instruments: pl.DataFrame) -> pl.when(prev_raw > 0) .then( (~is_limit_down.fill_null(True)) - & (pl.col("low") <= limit_down_price + 0.005) + & (pl.col("low") <= effective_limit_down + 0.005) & (pl.col("close") > pl.col("open")) ).otherwise(None).cast(pl.Boolean) .alias("signal_limit_down_recovery"), @@ -1457,7 +1475,7 @@ def _compute_limit_signals_today(df: pl.DataFrame, instruments: pl.DataFrame) -> pl.when((prev_raw > 0) & (pl.col("raw_high") > 0)) .then( (~is_limit_up.fill_null(True)) - & (pl.col("raw_high") >= limit_up_price - 0.005) + & (pl.col("raw_high") >= effective_limit_up - 0.005) ).otherwise(None).cast(pl.Boolean) .alias("signal_broken_limit_up"), ]) @@ -1482,7 +1500,7 @@ def _compute_limit_signals_today(df: pl.DataFrame, instruments: pl.DataFrame) -> ]) # 清理 - cleanup = ["_limit_pct", "_is_st"] + cleanup = ["_limit_pct", "_is_st", "limit_up", "limit_down"] for c in df.columns: if c.endswith("_inst"): cleanup.append(c)