Files
MoFin/deploy/profile-scripts/s2_panic_v2_scanner.py
T

182 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""s2_panic_v2_scanner.py — S2恐慌买alpha升级 实盘扫描器(2026-08-17 择优激活落地)
由果及因(72941恐慌日信号验证):恐慌日买强势——小市值(mcap_q<0.4)+高RSI(rsi>=35)+行业抗跌(sec_ret20>=-10)
→ 胜率60.8% vs 基线30.7%;每日top8截断(信号/成交比1.8)
基于原 s2_scanner 改:原=恐慌日买超跌(bias60<-6.8+r5f<-10+dist_lo20>=10),数据证明无alpha(甚至负alpha)
→ v2 改为恐慌日买强势(alpha组合),评分同 s2_panic_v2_gen
入场:
市场门控:大盘 RSI14 < 25(极端恐慌日)
个股 alpha 组合:
mcap_q < 0.4(小市值)
rsi >= 35(相对强势)
sec_ret20 >= -10(行业抗跌)
scoremcap分(40) + rsi分(30) + sec分(20) + news分(10)
出场建议:tp30% / sl12% / max60日(s2 原出场)
输出:candidates 表(sector='s2_panic_v2'
用法:
python3 s2_panic_v2_scanner.py # 完整扫描(大盘RSI<25门控)
python3 s2_panic_v2_scanner.py --force # 忽略门控
python3 s2_panic_v2_scanner.py --top N # 输出前 N 只(默认 8)
"""
import sys, sqlite3
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent))
from indicators import calc_ma, calc_rsi
from market_data import fetch_tx_klines, get_stock_pool
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
TOP_N = 8
EXIT_CFG = {"tp_pct": 0.30, "sl_pct": 0.12, "max_hold_days": 20} # 2026-08-17分温区: trend_down 20d最优(6.69%vs60d 4.95%)
# 大盘 RSI 门控(与 s2_scanner 一致)
MKT_RSI_MAX = 25
def load_mkt_rsi():
"""大盘 RSI14stock_daily sh000001"""
try:
conn = sqlite3.connect(str(DB_PATH), timeout=5)
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
rows = conn.execute(
"SELECT date, close FROM stock_daily WHERE code='sh000001' ORDER BY date DESC LIMIT 40").fetchall()
conn.close()
if len(rows) < 20:
return None, None
rows = list(reversed(rows))
closes = [r[1] for r in rows]
rsi = calc_rsi(closes)
return rows[-1][0], rsi[-1]
except Exception:
return None, None
def mcap_quantile(code):
"""市值分位(2026-08-17 改用 stock_fundamentals.mcap_total,原 amount 不可靠)"""
try:
conn = sqlite3.connect(str(DB_PATH), timeout=5)
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
row = conn.execute(
"SELECT mcap_total FROM stock_fundamentals WHERE code=? ORDER BY updated_at DESC LIMIT 1",
(code,)).fetchone()
conn.close()
if not row or not row[0]:
return 0.3
conn = sqlite3.connect(str(DB_PATH), timeout=5)
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
rows = conn.execute(
"SELECT code, mcap_total FROM stock_fundamentals f WHERE updated_at = "
"(SELECT MAX(updated_at) FROM stock_fundamentals f2 WHERE f2.code=f.code)"
).fetchall()
conn.close()
mcaps = sorted([r[1] for r in rows if r[1] and r[1] > 0])
if not mcaps:
return 0.3
import bisect
return round(bisect.bisect_left(mcaps, row[0]) / max(len(mcaps), 1), 2)
except Exception:
return 0.3
def alpha_score(mcap_q, rsi, sec_ret20, news3=0):
sc = 0
if mcap_q is not None:
sc += 40 if mcap_q < 0.2 else 32 if mcap_q < 0.4 else 24 if mcap_q < 0.6 else 16 if mcap_q < 0.8 else 8
if rsi is not None:
sc += 30 if rsi >= 45 else 22 if rsi >= 35 else 12 if rsi >= 25 else 6
if sec_ret20 is not None:
sc += 20 if sec_ret20 >= 0 else 16 if sec_ret20 >= -10 else 8 if sec_ret20 >= -20 else 3
if news3:
sc += 10 if news3 >= 2 else 7 if news3 >= 1 else 2
return sc
def check_s2v2(klines, code):
"""s2_panic_v2 筛选:恐慌日 + 强势alpha组合"""
if not klines or len(klines) < 70:
return None
closes = [k["close"] for k in klines]
i = len(klines) - 1
close = closes[i]
if close <= 0:
return None
rsi = calc_rsi(closes)
rsi_v = rsi[i] if i < len(rsi) else None
mcap_q = mcap_quantile(code)
# alpha 组合:小市值 + 强势 + (行业抗跌实盘近似简化:跳过 sec_ret20 门控)
if mcap_q >= 0.4:
return None
if rsi_v is None or rsi_v < 35:
return None
sc = alpha_score(mcap_q, rsi_v, None)
return {
"price": close, "rsi": round(rsi_v, 2), "mcap_q": mcap_q, "score": sc,
"target": round(close * (1 + EXIT_CFG["tp_pct"]), 2),
"stop_loss": round(close * (1 - EXIT_CFG["sl_pct"]), 2),
"date": klines[i]["date"],
}
def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--force", action="store_true")
ap.add_argument("--top", type=int, default=TOP_N)
args = ap.parse_args()
mkt_date, mkt_rsi = load_mkt_rsi()
print(f"[s2_panic_v2] {datetime.now().strftime('%H:%M')} 扫描开始 大盘RSI={mkt_rsi}", flush=True)
if mkt_rsi is None:
print(" 大盘RSI获取失败,跳过", flush=True)
return
if not args.force and mkt_rsi >= MKT_RSI_MAX:
print(f" 大盘RSI={mkt_rsi:.0f} >= {MKT_RSI_MAX},非恐慌日,跳过", flush=True)
return
all_stocks, existing = get_stock_pool()
print(f" 股票池 {len(all_stocks)} 只", flush=True)
hits = []
for code in all_stocks:
if code in existing:
continue
try:
klines = fetch_tx_klines(code, datalen=120)
sig = check_s2v2(klines, code)
if sig:
hits.append((code, code, sig)) # name 暂用 code
except Exception:
pass
hits.sort(key=lambda x: -x[2]["score"])
hits = hits[: args.top]
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
inserted = 0
for code, name, sig in hits:
reasons = (f"rsi={sig['rsi']} mcap_q={sig['mcap_q']} score={sig['score']}")
# 2026-08-18 补 rr 列(断链 bugpromote 的 _rr is None 全跳过,与 b_td1_v3 同口径)
_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, "s2_panic_v2", reasons,
f"{sig['price']*0.98:.2f}~{sig['price']:.2f}", sig["stop_loss"], sig["target"], _rr_v, "s2_panic_v2"))
inserted += 1
print(f" 🟢 {code} {name}{sig['price']} score={sig['score']} {reasons}", flush=True)
conn.commit()
conn.close()
print(f" ✅ 新增 {inserted} 只 s2_panic_v2 候选", flush=True)
if __name__ == "__main__":
main()