138 lines
5.6 KiB
Python
138 lines
5.6 KiB
Python
#!/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()
|