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: