129 lines
5.3 KiB
Python
129 lines
5.3 KiB
Python
import time
|
||
#!/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
|
||
# 2026-08-18 database is locked 重试(整点调度撞锁致崩溃,工单P1)
|
||
import random as _rd
|
||
_w_ok = False
|
||
for _try in range(4):
|
||
try:
|
||
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"))
|
||
_w_ok = True
|
||
break
|
||
except Exception as _e:
|
||
if "locked" in str(_e).lower():
|
||
time.sleep(0.4 * (2 ** _try) + _rd.uniform(0, 0.2))
|
||
else:
|
||
raise
|
||
if _w_ok:
|
||
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()
|