#!/usr/bin/env python3 # -*- coding: utf-8 -*- """b_td1_v3_scanner.py — B组超跌·原池优选 实盘扫描器(v4 纯 SQL,2026-08-17) 架构铁律(老莫):数据使用层不采集、不计算指标——指标是数据加工层的事。 采集层 daily_kline_collector → stock_daily(原始K线) 加工层 factor_engine(17:05 cron) → stock_indicators(bias60/rsi/dist_lo20/r5f/mcap_q/pe_q 全算好) 使用层 本扫描器 → 只 SQL 查询,零计算零网络 stock_indicators 覆盖 3972只(08-14),close 需联表 stock_daily 取。 """ import sys, sqlite3 from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") TOP_N = 5 def load_regime(): try: from regime_gate import get_current_regime rg = get_current_regime() if rg and rg.get("regime") != "unknown": return rg.get("regime") except Exception: pass try: conn = sqlite3.connect(str(DB_PATH), timeout=5) r = conn.execute("SELECT regime FROM market_regime WHERE market='a' ORDER BY date DESC LIMIT 1").fetchone() conn.close() return r[0] if r else "unknown" except Exception: return "unknown" def _sql_scan(): """纯 SQL:stock_indicators(加工层指标) JOIN stock_daily(close) 最新完整日,池条件过滤""" conn = sqlite3.connect(str(DB_PATH), timeout=10) day = conn.execute( "SELECT MAX(date) FROM stock_indicators WHERE date < date('now','localtime')").fetchone()[0] if not day: conn.close() return [] rows = conn.execute( "SELECT i.code, sd.close, i.bias60, i.rsi, i.dist_lo20, i.r5f, i.mcap_q, i.pe_q " "FROM stock_indicators i JOIN stock_daily sd ON sd.code=i.code AND sd.date=i.date " "WHERE i.date=? AND i.mcap_q<0.3 AND i.pe_q<0.3 " "AND i.bias60<-20 AND i.rsi<40 AND i.dist_lo20>5", (day,)).fetchall() conn.close() hits = [] for code, close, b60, rsi, dist, r5, mq, pq in rows: if not close or close <= 0: continue sc = 0 sc += 40 if b60 < -30 else 32 if b60 < -20 else 20 sc += 30 if rsi < 30 else 24 if rsi < 40 else 14 if r5 is not None: sc += 10 if r5 < -25 else 7 if r5 < -15 else 4 hits.append((code, code, { "price": close, "bias60": round(b60, 2), "rsi": round(rsi, 2), "dist_lo20": round(dist, 2), "ret5": round(r5 or 0, 2), "mcap_q": mq, "score": sc, "target": round(close * 1.15, 2), "stop_loss": round(close * 0.92, 2), "date": day, })) hits.sort(key=lambda x: -x[2]["score"]) return hits def main(): import argparse from datetime import datetime ap = argparse.ArgumentParser() ap.add_argument("--force", action="store_true") ap.add_argument("--top", type=int, default=TOP_N) args = ap.parse_args() regime = load_regime() print(f"[b_td1_v3] {datetime.now().strftime('%H:%M')} 扫描开始 温区={regime}", flush=True) if not args.force and regime not in ("trend_down", "choppy"): print(f" 温区 {regime} 非超跌池主战场,跳过", flush=True) return hits = _sql_scan() if not hits: print(" SQL扫描无命中(最新完整日无满足池条件的小市值超跌股)", flush=True) return hits = hits[: args.top] print(f" SQL扫描命中 {len(hits)} 只(score降序前{args.top})", flush=True) conn = sqlite3.connect(str(DB_PATH), timeout=10) inserted = 0 for code, name, sig in hits: reasons = (f"dist_lo20={sig['dist_lo20']}% bias60={sig['bias60']}% " f"rsi={sig['rsi']} ret5={sig['ret5']}% mcap_q={sig['mcap_q']} score={sig['score']}") _mid_v = (sig['price'] * 0.98 + sig['price']) / 2 _rr_v = round((sig['target'] - _mid_v) / (_mid_v - sig['stop_loss']), 2) if _mid_v > sig['stop_loss'] > 0 else 0 conn.execute( "INSERT INTO candidates (code, name, sector, reason, entry_range, stop_loss, target, rr, source_strategy, created_at) " "VALUES (?,?,?,?,?,?,?,?,?,datetime('now','localtime')) " "ON CONFLICT(code) DO UPDATE SET " "name=excluded.name, sector=excluded.sector, reason=excluded.reason, " "entry_range=excluded.entry_range, stop_loss=excluded.stop_loss, target=excluded.target, rr=excluded.rr, source_strategy=excluded.source_strategy", (code, code, "b_td1_v3", reasons, f"{sig['price']*0.98:.2f}~{sig['price']:.2f}", sig["stop_loss"], sig["target"], _rr_v, "b_td1_v3")) inserted += 1 print(f" 🟢 {code} {code} 价{sig['price']} score={sig['score']} {reasons}", flush=True) conn.commit() conn.close() print(f" ✅ 新增 {inserted} 只 b_td1_v3 候选", flush=True) if __name__ == "__main__": main()