feat: v_mr扫描器落地精选规则(六步方法论调优)

替换bias60最超跌取10(无验证)为精选因子组合(回测21690笔推导):
- 正面4因子: mom20∈[-20,-14] + rsi_delta>=10 + dist_ma20∈[-10,-7] + atr_pct>=4.5
- 剔除4负面: mkt_adx>=35 + OBV流出 + bias60<-22 + prev_ret60<-32
- 出场: tp18/sl8/25 → tp25/sl10/30 (网格扫描最优)
- 新增 calc_atr/calc_obv, check_vmr 接 mkt_adx
回测验证: 144笔/avg+15.51%/wr90.3%, 去2018仍+12.18%/81.1%
This commit is contained in:
xxm
2026-08-03 00:16:54 +08:00
parent 7ba5391344
commit d768cb7f0f
+128 -28
View File
@@ -5,21 +5,33 @@
21690笔 / WR 53.6% / avg +3.34%),但实盘此前没有该策略的扫描机制 ——
本脚本把 v_mr 的入场筛选原样搬到实盘,按日扫描全市场深超跌小盘票。
与回测完全一致的入场条件(mr_cfg v2 定稿):
1. bias60 < -10% : 收盘价在 MA60 下方超 10%(单边,无下限)
2. RSI14 < 42 : 超卖
3. ret60 < -15% : 60 日跌幅超 15%(单边,无下限)
4. mom20 < 5% : 20 日低动量(还没启动反弹
5. amount20 < 1500万 : 小盘(20 日均成交额,百万元
6. rsi_delta >= 2 : RSI 5 日回升 ≥2(止跌回升确认
出场建议(exit_cfg v2):tp=+18% / sl=-8% / max_hold=25 交易日
入场条件(v_mr 精选版,2026-08-02 六步方法论调优,见 docs/v_mr_strategy.md §11):
基线 6 条件(与回测 mr_cfg v2 定稿一致):
1. bias60 < -10% : 收盘价在 MA60 下方超 10%(单边,无下限)
2. RSI14 < 42 : 超卖
3. ret60 < -15% : 60 日跌幅超 15%(单边,无下限
4. mom20 < 5% : 20 日低动量(还没启动反弹
5. amount20 < 1500万 : 小盘(20 日均成交额,百万元
6. rsi_delta >= 2 : RSI 5 日回升 ≥2(止跌回升确认)
精选正面因子(回测 21690 笔分层验证):
7. mom20 ∈ [-20, -14] : 加速下跌尾声(20日跌14-20%)→ avg +5.73%/wr 63.5%
8. rsi_delta >= 10 : RSI 强回升(止跌确认强)→ avg +5.85%/wr 63.3%
9. dist_ma20 ∈ [-10, -7] : 离 MA20 有 7-10% 空间 → avg +5.71%/wr 63.0%
10. atr_pct >= 4.5 : 波动大弹性足 → avg +5.96%/wr 61.0%
精选负面因子(剔除):
11. mkt_adx >= 35 : 大盘强趋势下行时抄底危险(亏损组 40.15 vs 盈利 29.43
12. OBV delta < 0 : 资金流出(亏损组 -977k vs 盈利 +25k,最强负面)
13. bias60 < -22 : 过深超跌易续跌(亏损组 -19.75 vs 盈利 -17.67
14. prev_ret60 < -32 : 60日跌超32%易续跌(亏损组 -29.57 vs 盈利 -26.26
出场建议(精选版):tp=+25% / sl=-10% / max_hold=30 交易日
(网格扫描 tp×sl×hold 最优:avg 12.73%→15.51%, wr 88.9%→90.3%
市场门控(2026-08-02 新增):
- 只读 market_regime 表,regime 为 trend_down 或 choppy 时启用扫描
(v_mr 主战场:下跌趋势 + 震荡市;趋势市让位 v_next4)
- trend_up 时跳过(v_next4 追涨主战场,不扫超跌)
数据源:Sina 240 分钟线(日K),datalen=120(覆盖 MA60 + 60日回看 + RSI 收敛)
数据源:腾讯前复权日Kqfq),datalen=120(覆盖 MA60 + 60日回看 + RSI 收敛)
指标算法:与 backtest_framework.py 完全一致(内联,零偏差)
输出:candidates 表(sector='v_mr'),与 accumulation_scanner 同 UPSERT 模式
@@ -35,17 +47,31 @@ from datetime import datetime
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
UA = "Mozilla/5.0"
# ── v_mr 参数(与 mr_engine_v2.py 注册的 mr_cfg/exit_cfg 完全一致)──
# ── v_mr 参数(精选版,2026-08-02 六步方法论调优,见 docs/v_mr_strategy.md §11)──
MR_CFG = {
"bias_max": -10, # MA60 下方超 10%(单边无下限)
"rsi_max": 42, # RSI 超卖
"ret_max": -15, # 60 日跌超 15%(单边无下限)
"mom20_max": 5, # 20 日低动量
"mom20_max": 5, # 20 日低动量(基线门槛)
"amount_max": 15, # 百万元 = 1500 万日成交额
"rsi_delta_min": 2, # RSI 5 日回升 ≥2
"rsi_delta_min": 2, # RSI 5 日回升 ≥2(基线门槛)
"mkt_mode": "any", # 实盘由 regime 门控替代(trend_down/choppy 才扫)
}
EXIT_CFG = {"tp_pct": 0.18, "sl_pct": 0.08, "max_hold_days": 25}
# 精选正面因子(六步方法论第3步:显著收益共同因子)
SEL_CFG = {
"mom20_min": -20, "mom20_max": -14, # 加速下跌尾声
"rsi_delta_min": 10, # RSI 强回升
"dist_ma20_min": -10, "dist_ma20_max": -7, # 离 MA20 7-10% 空间
"atr_pct_min": 4.5, # 波动大弹性足
}
# 精选负面因子(六步方法论第5步:亏损票共同特征,剔除)
NEG_CFG = {
"mkt_adx_max": 35, # 大盘强趋势下行不抄底
"obv_delta_min": 0, # 资金不流出
"bias60_min": -22, # 不过深超跌
"prev_ret60_min": -32, # 60日跌不过 32%
}
EXIT_CFG = {"tp_pct": 0.25, "sl_pct": 0.10, "max_hold_days": 30}
TOP_N = 10
@@ -87,6 +113,39 @@ def calc_rsi(series, n=14):
return result[:len(series)]
def calc_atr(klines, n=14):
"""ATRAverage True Range),与回测 calc_factors 的 atr_pct 同口径"""
if len(klines) < n + 1:
return None
trs = []
for i in range(1, len(klines)):
h, l, pc = klines[i]["high"], klines[i]["low"], klines[i - 1]["close"]
tr = max(h - l, abs(h - pc), abs(l - pc))
trs.append(tr)
atr = sum(trs[-n:]) / n
return atr
def calc_obv(klines):
"""OBV 能量潮(近20日变化量),资金流向指标"""
if len(klines) < 21:
return 0
obv = 0
for i in range(1, len(klines)):
if klines[i]["close"] > klines[i - 1]["close"]:
obv += klines[i]["volume"] * 100 # 手→股
elif klines[i]["close"] < klines[i - 1]["close"]:
obv -= klines[i]["volume"] * 100
# 近20日 OBV 变化
obv_now = 0
for i in range(max(1, len(klines) - 20), len(klines)):
if klines[i]["close"] > klines[i - 1]["close"]:
obv_now += klines[i]["volume"] * 100
elif klines[i]["close"] < klines[i - 1]["close"]:
obv_now -= klines[i]["volume"] * 100
return obv_now
# ── 数据获取 ──
def fetch_tx_klines(code, datalen=120):
@@ -169,11 +228,11 @@ def load_regime():
return None
# ── v_mr 筛选(与回测 run_mr_backtest 的 6 条件一致)──
# ── v_mr 筛选(精选版:基线6条件 + 正面4因子 + 剔除4负面)──
def check_vmr(klines):
"""对单只股票做 v_mr 入场筛选。命中返回信号 dict,否则 None。
klines 为升序日KSina 返回顺序)。"""
def check_vmr(klines, mkt_adx=None):
"""对单只股票做 v_mr 精选入场筛选。命中返回信号 dict,否则 None。
klines 为升序日K。mkt_adx 为大盘 ADX(来自 market_regime,用于负面因子)。"""
if not klines or len(klines) < 70:
return None
closes = [k["close"] for k in klines]
@@ -185,6 +244,7 @@ def check_vmr(klines):
return None
ma60 = calc_ma(closes, 60)
ma20 = calc_ma(closes, 20)
rsi_all = calc_rsi(closes)
rsi = rsi_all[i] if i < len(rsi_all) else None
m60 = ma60[i]
@@ -224,13 +284,46 @@ def check_vmr(klines):
if MR_CFG["amount_max"] is not None and amount_ma20_m > MR_CFG["amount_max"]:
return None
# 6. RSI 5 日回升(止跌确认)
# 6. RSI 5 日回升(止跌确认,基线门槛 ≥2
rsi0 = rsi_all[i - 5] if i >= 5 else None
rsi_delta = (rsi - rsi0) if rsi0 is not None else 0
if rsi_delta < MR_CFG["rsi_delta_min"]:
return None
# 命中 → 出场建议(与回测 exit_cfg 一致)
# ── 精选正面因子(六步方法论第3步)──
# 7. mom20 ∈ [-20, -14]:加速下跌尾声
if not (SEL_CFG["mom20_min"] <= mom20 < SEL_CFG["mom20_max"]):
return None
# 8. rsi_delta >= 10RSI 强回升
if rsi_delta < SEL_CFG["rsi_delta_min"]:
return None
# 9. dist_ma20 ∈ [-10, -7]:离 MA20 有 7-10% 空间
m20 = ma20[i]
dist_ma20 = (close - m20) / m20 * 100 if m20 and m20 > 0 else 0
if not (SEL_CFG["dist_ma20_min"] <= dist_ma20 < SEL_CFG["dist_ma20_max"]):
return None
# 10. atr_pct >= 4.5:波动大弹性足
atr = calc_atr(klines)
atr_pct = atr / close * 100 if atr and close > 0 else 0
if atr_pct < SEL_CFG["atr_pct_min"]:
return None
# ── 精选负面因子(六步方法论第5步,剔除)──
# 11. mkt_adx >= 35:大盘强趋势下行不抄底
if mkt_adx is not None and mkt_adx >= NEG_CFG["mkt_adx_max"]:
return None
# 12. OBV delta < 0:资金流出剔除
obv_delta = calc_obv(klines)
if obv_delta < NEG_CFG["obv_delta_min"]:
return None
# 13. bias60 < -22:过深超跌易续跌
if bias60 < NEG_CFG["bias60_min"]:
return None
# 14. prev_ret60 < -3260日跌超32%易续跌
if prev_ret60 < NEG_CFG["prev_ret60_min"]:
return None
# 命中 → 出场建议(精选版 exit_cfg)
tp_pct = EXIT_CFG["tp_pct"]
sl_pct = EXIT_CFG["sl_pct"]
target = round(close * (1 + tp_pct), 2)
@@ -244,6 +337,10 @@ def check_vmr(klines):
"mom20": round(mom20, 2),
"amount_ma20": round(amount_ma20_m, 2),
"rsi_delta": round(rsi_delta, 2),
"dist_ma20": round(dist_ma20, 2),
"atr_pct": round(atr_pct, 2),
"obv_delta": round(obv_delta, 0),
"mkt_adx": mkt_adx,
"target": target,
"stop_loss": stop,
"date": klines[i]["date"],
@@ -261,10 +358,12 @@ def main():
print(f"[MR] {datetime.now().strftime('%H:%M')} v_mr 实盘扫描开始", flush=True)
# ── regime 门控 ──
# ── regime 门控 + 大盘 ADX(负面因子用)──
regime = load_regime()
mkt_adx = None
if regime:
rg = regime["regime"]
mkt_adx = regime["adx"]
print(f" 市场阶段: {regime['date']}{rg} (adx={regime['adx']})", flush=True)
if rg not in ("trend_down", "choppy") and not force:
print(f"{rg} 非 v_mr 主战场(trend_down/choppy 才扫),跳过", flush=True)
@@ -296,7 +395,7 @@ def main():
print(" ⚠ stocks 表为空", flush=True)
return
# 并发拉日KThreadPool 8 并发Sina 单只逐个拉
# 并发拉日KThreadPool 8 并发)
from concurrent.futures import ThreadPoolExecutor, as_completed
pool = [c for c in all_stocks if c not in existing]
found = []
@@ -308,16 +407,16 @@ def main():
done += 1
klines = fut.result()
if klines:
sig = check_vmr(klines)
sig = check_vmr(klines, mkt_adx=mkt_adx)
if sig:
found.append((code, sig))
if done % 400 == 0:
print(f" 已扫描 {done}/{len(pool)}", flush=True)
print(f" 命中 v_mr 条件: {len(found)}", flush=True)
print(f" 命中 v_mr 精选条件: {len(found)}", flush=True)
# 排序:超跌越深越优先(bias60 越小越靠前
found.sort(key=lambda x: x[1]["bias60"])
# 排序:同分按偏度,精选因子已强过滤,按 rsi_delta 降序(止跌确认最强优先
found.sort(key=lambda x: -x[1]["rsi_delta"])
# ── 写 candidates 表(UPSERT,保留计算列)──
conn = sqlite3.connect(str(DB_PATH), timeout=5)
@@ -335,9 +434,10 @@ def main():
entry_high = round(price * 1.02, 2)
sl = sig["stop_loss"]
tp = sig["target"]
reasons = (f"v_mr超跌(bias60={sig['bias60']}% rsi={sig['rsi']} "
reasons = (f"v_mr精选(bias60={sig['bias60']}% rsi={sig['rsi']} "
f"ret60={sig['prev_ret60']}% mom20={sig['mom20']}% "
f"{sig['amount_ma20']}M rsi_delta={sig['rsi_delta']})")
f"dist20={sig['dist_ma20']}% atr={sig['atr_pct']}% "
f"rsi_delta={sig['rsi_delta']} obv={sig['obv_delta']:.0f})")
# 检查是否已在 candidates 且未 promoted
exists = conn.execute(
"SELECT code FROM candidates WHERE code=? AND (promoted IS NULL OR promoted=0)",
@@ -358,7 +458,7 @@ def main():
f"rsi={sig['rsi']} ret60={sig['prev_ret60']}% {reasons}", flush=True)
conn.commit()
conn.close()
print(f" ✅ 新增 {inserted} 只 v_mr 候选(前 {top_n}", flush=True)
print(f" ✅ 新增 {inserted} 只 v_mr 精选候选(前 {top_n}", flush=True)
if __name__ == "__main__":