#!/usr/bin/env python3 # -*- coding: utf-8 -*- """leader_scanner.py — MoFin 龙头识别策略(bull_trend 适用,2026-08-13 落地) 核心逻辑(12维方法论 + 龙头识别低波动优化版理念): - 适用状态:bull_trend(above_ma20 + rsi>55 + adx>20) - 入场:龙头股回调到 MA20 附近(趋势中的健康回调,不追高) - 12维条件(复用 stock_indicators 已有字段,无拍脑袋): 1. 个股 close > MA20(趋势向上) 2. dist_ma20 ∈ [-3%, +2%](回调到 MA20 附近,不追高) 3. 行业强势(sector_above_ma20=1 或 sector_ret20>0,需 sector 数据) 4. 市值/流动性过滤(mcap_q > 0.3,龙头非小盘) 5. RSI ∈ [45, 65](强势但未超买) 6. 量能配合(vol_ratio > 0.8,非极度缩量) - 出场:跌破 MA20 或达到上方压力位 - RR:压力位(前高/筹码阻力)/ 支撑位(MA20)计算 注意:这是初版框架,需 MoFin 引擎验证达标(年化≥13.2%)才正式上线 """ import os import sqlite3 import json from datetime import datetime from pathlib import Path DB = "/home/hmo/MoFin/data/mofin.db" OUT = Path("/home/hmo/MoFin/data/leader_signals.json") def load_market_state(): p = Path("/home/hmo/MoFin/data/market_state.json") if p.exists(): return json.loads(p.read_text(encoding="utf-8")) return {"state": "neutral"} def scan_leaders(): """扫描龙头股回调买点(bull_trend 状态)""" ms = load_market_state() state = ms.get("state", "neutral") if state != "bull_trend": print(f"当前状态 {state},非 bull_trend,不扫描龙头(避免追高)") return [] c = sqlite3.connect(DB) # 最新交易日 row = c.execute("SELECT MAX(date) FROM stock_indicators").fetchone() if not row or not row[0]: c.close() return [] latest = row[0] print(f"扫描日期: {latest}") # 龙头条件(12维) rows = c.execute( """SELECT code, ma20, rsi, dist_ma20, mcap_q, pe_q, vol_ratio, trend_aligned FROM stock_indicators WHERE date=? AND ma20 IS NOT NULL AND rsi IS NOT NULL""", (latest,) ).fetchall() c.close() signals = [] for r in rows: code, ma20, rsi, dist_ma20, mcap_q, pe_q, vol_ratio, trend_aligned = r # 条件1: 趋势向上(close > MA20 → dist_ma20 > 0,或接近) if dist_ma20 is None or dist_ma20 < -3 or dist_ma20 > 2: continue # 条件2: RSI 强势未超买 if rsi < 45 or rsi > 65: continue # 条件3: 市值/流动性(龙头非小盘,mcap_q > 0.3) if mcap_q is not None and mcap_q < 0.3: continue # 条件4: 量能配合 if vol_ratio is not None and vol_ratio < 0.8: continue # 条件5: 趋势共振(trend_aligned=1) if trend_aligned != 1: continue signals.append({ "code": code, "ma20": ma20, "rsi": rsi, "dist_ma20": dist_ma20, "mcap_q": mcap_q, "pe_q": pe_q, "vol_ratio": vol_ratio, "entry_reason": "龙头回调MA20", }) # 按 dist_ma20 排序(最接近 MA20 的优先) signals.sort(key=lambda x: abs(x["dist_ma20"])) print(f"龙头信号: {len(signals)} 只") for s in signals[:5]: print(f" {s['code']}: dist_ma20={s['dist_ma20']:.1f}% rsi={s['rsi']:.1f} mcap_q={s['mcap_q']}") return signals def main(): signals = scan_leaders() # 2026-08-18 补写 candidates(trend_up 激活策略真正进入选股管道) _strategy_tag = os.environ.get("LEADER_STRATEGY", "v_next") try: _c = sqlite3.connect(DB, timeout=30) _c.execute("PRAGMA busy_timeout=30000") _now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") _ins = 0 for s in signals: code = str(s["code"]) _price = s.get("ma20") or 0 # 用 MA20 附近作为参考价 if _price <= 0: continue _sl = round(_price * 0.92, 2) # 跌破 MA20 容忍 8% _tp = round(_price * 1.15, 2) # +15% 压力位 _mid = (_price * 0.98 + _price) / 2 _rr = round((_tp - _mid) / (_mid - _sl), 2) if _mid > _sl else 0 _en = f"{_price*0.98:.2f}~{_price:.2f}" try: _c.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 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, _strategy_tag, f"龙头回调MA20: rsi={s.get('rsi')} dist={s.get('dist_ma20')}% mcap_q={s.get('mcap_q')}", _en, _sl, _tp, _rr, _strategy_tag)) _ins += 1 except Exception: pass _c.commit(); _c.close() print(f"写入 candidates: {_ins} 只 (策略 {_strategy_tag})") except Exception as e: print(f"candidates 写入失败: {e}") out = { "state": load_market_state().get("state", "neutral"), "signals": signals, "count": len(signals), "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8") print(f"leader_signals.json 写入") if __name__ == "__main__": main()