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

196 lines
7.6 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 -*-
"""b_td1_v3_scanner.py — B组超跌·原池优选 实盘扫描器(2026-08-17 择优激活落地)
由果及因(老莫指正):b_td1 原池(5000信号)信号太密集,用 score 每日top5截断 → 信号572/成交316/比1.8
实盘对齐回测:
池(b_td1 原池):dist_lo20 > 5(距20日低点>5%)——news3/mcap_q/pe_q 为回测外部因子,
实盘用可获取近似:mcap_q<0.3(市值分位,从 stock_daily 市值算)pe_q<0.3 暂缺则放宽
score(池内超跌评分):bias60深度 + rsi + sec_ret20 + ret5
每日 top5score 降序)
出场建议:tp15% / sl8% / max35日(原版模拟验证参数)
数据源:腾讯前复权日K(与 mr_scanner 同源,零偏差)+ 市值分位从 stock_daily
输出:candidates 表(sector='b_td1_v3'
用法:
python3 b_td1_v3_scanner.py # 完整扫描
python3 b_td1_v3_scanner.py --force # 忽略门控
python3 b_td1_v3_scanner.py --top N # 输出前 N 只(默认 5)
"""
import sys, json, 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 = 5
EXIT_CFG = {"tp_pct": 0.15, "sl_pct": 0.08, "max_hold_days": 35}
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 mcap_quantile(code):
"""市值分位(mcap_q):用 stock_fundamentals.mcap_total(总市值,亿元)
2026-08-17 修复:原用 stock_daily.amount 成交额不可靠(amount=None 导致全返回0.3缺省,
而池条件 mcap_q<0.3 会挡掉所有——扫描0候选的根因)"""
try:
conn = sqlite3.connect(str(DB_PATH), timeout=5)
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 # 缺省中值
# 全市场市值分位(用基本面最新 mcap_total 全量)
conn = sqlite3.connect(str(DB_PATH), timeout=5)
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
pos = bisect.bisect_left(mcaps, row[0])
return round(pos / max(len(mcaps), 1), 2)
except Exception:
return 0.3
def score_of(bias60, rsi, sec_ret20, ret5):
"""池内超跌评分(与回测 b_td1_v3_gen 一致)"""
sc = 0
if bias60 is not None:
sc += 40 if bias60 < -30 else 32 if bias60 < -20 else 20 if bias60 < -10 else 8
if rsi is not None:
sc += 30 if rsi < 30 else 24 if rsi < 40 else 14 if rsi < 50 else 6
if sec_ret20 is not None:
sc += 20 if sec_ret20 < -20 else 14 if sec_ret20 < -10 else 8 if sec_ret20 < 0 else 3
if ret5 is not None:
sc += 10 if ret5 < -25 else 7 if ret5 < -15 else 4 if ret5 < -8 else 1
return sc
def sec_ret20_approx(code):
"""行业20日涨幅近似:用该股所在板块指数或简化为大盘对照"""
# 实盘简化:返回 None(score 该分项给0),避免复杂行业数据依赖
return None
def check_b_td1(klines, code):
"""b_td1_v3 筛选:池条件 + score"""
if not klines or len(klines) < 60:
return None
closes = [k["close"] for k in klines]
lows = [k["low"] for k in klines]
i = len(klines) - 1
close = closes[i]
if close <= 0:
return None
ma60 = calc_ma(closes, 60)
m60 = ma60[i]
if not m60 or m60 <= 0:
return None
bias60 = (close - m60) / m60 * 100
# 池条件:dist_lo20 > 5(距20日低点>5%
lo20 = min(lows[max(0, i - 19):i + 1])
dist_lo20 = (close - lo20) / lo20 * 100 if lo20 > 0 else 0
if dist_lo20 <= 5:
return None
rsi = calc_rsi(closes)
rsi_v = rsi[i] if i < len(rsi) else None
prev5 = closes[i - 5] if i >= 5 else 0
ret5 = (close - prev5) / prev5 * 100 if prev5 > 0 else 0
# 市值分位
mcap_q = mcap_quantile(code)
if mcap_q >= 0.3:
return None # 池条件:小市值
# score
sec20 = sec_ret20_approx(code)
sc = score_of(bias60, rsi_v, sec20, ret5)
return {
"price": close, "bias60": round(bias60, 2), "rsi": round(rsi_v, 2) if rsi_v else None,
"ret5": round(ret5, 2), "dist_lo20": round(dist_lo20, 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()
regime = load_regime()
print(f"[b_td1_v3] {datetime.now().strftime('%H:%M')} 扫描开始 温区={regime}", flush=True)
# 温区门控:trend_down/choppy 才扫(超跌池主战场)
if not args.force and regime not in ("trend_down", "choppy"):
print(f" 温区 {regime} 非超跌池主战场,跳过", 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_b_td1(klines, code)
if sig:
hits.append((code, code, sig)) # name 暂用 code(与 mr_scanner 同源)
except Exception:
pass
# score 降序 top-N
hits.sort(key=lambda x: -x[2]["score"])
hits = hits[: args.top]
print(f" 命中 {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']}")
conn.execute(
"INSERT INTO candidates (code, name, sector, reason, entry_range, stop_loss, target, 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",
(code, code, "b_td1_v3", reasons,
f"{sig['price']*0.98:.2f}~{sig['price']:.2f}", sig["stop_loss"], sig["target"]))
inserted += 1
print(f" 🟢 {code} {name}{sig['price']} score={sig['score']} {reasons}", flush=True)
conn.commit()
conn.close()
print(f" ✅ 新增 {inserted} 只 b_td1_v3 候选", flush=True)
if __name__ == "__main__":
main()