From d61bd7024e8ea14b893aa2af1877c2d10e31bf1d Mon Sep 17 00:00:00 2001 From: hmo Date: Tue, 11 Aug 2026 15:20:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20predictive=5Foversold=5Fscanner?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E2=80=94=E2=80=94=E7=9C=9F=E5=AE=9E=E5=85=A8?= =?UTF-8?q?=E5=B8=82=E5=9C=BAPE/=E5=B8=82=E5=80=BC=E5=88=86=E4=BD=8D(get?= =?UTF-8?q?=5Fmarket=5Fpercentile)+=E8=A1=8C=E4=B8=9A20=E6=97=A5=E5=8A=A8?= =?UTF-8?q?=E9=87=8F(fetch=5Fsector=5Fmomentum),=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../predictive_oversold_scanner.py | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/deploy/profile-scripts/predictive_oversold_scanner.py b/deploy/profile-scripts/predictive_oversold_scanner.py index 49da2954..2b29468a 100644 --- a/deploy/profile-scripts/predictive_oversold_scanner.py +++ b/deploy/profile-scripts/predictive_oversold_scanner.py @@ -228,12 +228,14 @@ def main(): fund = fetch_fundamentals(code) if not fund: continue - # 简化分位(部署时可从全市场分位表读取) - mcap_q = 0.1 if fund.get("mcap_total") and fund["mcap_total"] < 50 else 0.5 - pe_q = 0.1 if fund.get("pe") and fund["pe"] < 20 else 0.5 - # 简化 news3/sec_ret20(部署时可从 stock_news/sector 数据读取) - news3 = 1 if fetch_news_count(code) > 0 else 0 - sec_ret20 = -5 # 简化,部署时用真实行业动量 + # 2026-08-11:真实分位(全市场 PE/市值分位,替代简化值) + mcap_q = get_market_percentile(code, "mcap_total") + pe_q = get_market_percentile(code, "pe") + if mcap_q is None or pe_q is None: + continue + # 2026-08-11:真实新闻数(3日)+ 行业20日动量 + news3 = fetch_news_count(code) + sec_ret20 = fetch_sector_momentum(code) ok_s, msg_s = check_stock(code, code, mcap_q, pe_q, news3, sec_ret20, klines) if ok_s: @@ -250,6 +252,57 @@ def main(): print(f" ✅ 完成: 新增{hits}只 p_oversold 候选", flush=True) +def get_market_percentile(code, field): + """计算个股在全市场的分位(0-1,越小越优)。 + field: mcap_total 或 pe。分位 = (比它小的数量 / 总数)。 + 返回 None 表示数据不可用。""" + try: + conn = sqlite3.connect(str(DB_PATH), timeout=5) + # 全市场分布 + total = conn.execute(f"SELECT COUNT(*) FROM stock_fundamentals WHERE {field} > 0").fetchone()[0] + if not total: + conn.close() + return None + mine = conn.execute(f"SELECT {field} FROM stock_fundamentals WHERE code=?", (code,)).fetchone() + if not mine or not mine[0] or mine[0] <= 0: + conn.close() + return None + val = mine[0] + # 分位:比我小的占比 + cnt = conn.execute(f"SELECT COUNT(*) FROM stock_fundamentals WHERE {field} > 0 AND {field} < ?", (val,)).fetchone()[0] + conn.close() + return cnt / total + except Exception: + return None + + +def fetch_sector_momentum(code): + """计算行业20日动量(%)。从 stock_sectors 拿行业,再算行业指数20日涨跌。 + 返回 None 表示无行业数据。""" + try: + conn = sqlite3.connect(str(DB_PATH), timeout=5) + sector = conn.execute( + "SELECT sector_name FROM stock_sectors WHERE code=? LIMIT 1", (code,)).fetchone() + if not sector or not sector[0]: + conn.close() + return None + sector_name = sector[0] + # 行业20日动量:查 sector_index_daily 该行业20日前 vs 最新 + rows = conn.execute( + "SELECT close FROM sector_index_daily WHERE sector=? ORDER BY date DESC LIMIT 21", + (sector_name,)).fetchall() + conn.close() + if len(rows) < 20: + return None + latest = rows[0][0] + past = rows[19][0] + if past <= 0: + return None + return round((latest - past) / past * 100, 2) + except Exception: + return None + + def fetch_news_count(code): """简化:查 stock_news 表近3日新闻数(部署时可完善)""" try: