feat: mr_scanner切换v_weak参数(v_mr精选版已证伪)

v_weak六条件(与回测严格对齐, docs/v_mr_strategy.md §19/§21):
- bias60∈[-35,-20) + RSI≤25 + 5日急跌≤-3% + 距20日低点<5% + 收阳 + 缩量vol5/vol20<1.0
- 出场 tp30%/sl12%/40日(RR2.5)
- 排序: bias60升序(最深超跌优先, §15自然序)
- 门控保留: trend_down/choppy + 大盘ADX甜区[25,30)
- 候选仍写sector=v_mr(管道槽位不变, 下游兼容)
- 回测: 234笔/wr62.4%/avg+7.17%/8槽ret178.3%/cagr12.8%/dd16.4%
This commit is contained in:
xxm
2026-08-05 08:35:31 +08:00
parent 86bd3690c1
commit b82bd9ac1b
+60 -124
View File
@@ -1,30 +1,22 @@
#!/usr/bin/env python3
"""mr_scanner.py — v_mr 均值回复策略实盘扫描器
"""mr_scanner.py — v_weak 弱市策略实盘扫描器(2026-08-05 从 v_mr 精选版切换)
回测侧 v_mrstrategy_lab.run_mr_backtest / mr_engine_v2.py)已 10y 全市场验证
21690笔 / WR 53.6% / avg +3.34%),但实盘此前没有该策略的扫描机制 ——
本脚本把 v_mr 的入场筛选原样搬到实盘,按日扫描全市场深超跌小盘票
v_weak 是六步法+12维框架定稿的弱市策略(docs/v_mr_strategy.md §19/§21),
全市场 10y 验证:234笔 / wr 62.4% / avg +7.17% / 8槽组合 ret 178.3% / cagr 12.8% / dd 16.4%
v_mr 已被证伪(2018 依赖 + 去2018 掷硬币,§12),v_weak 是其替代
入场条件(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%
入场条件(v_weak 六条件,与回测严格对齐):
市场门控(main):大盘 MA20 下方(trend_down/choppy+ 大盘 ADX ∈ [25,30) 甜区
个股:
1. bias60 ∈ [-35, -20) : 深超跌但不过深(过深易续跌)
2. RSI14 ≤ 25 : 极度超卖
3. r5f ≤ -3% : 5日急跌(下跌动能确认
4. dist_lo20 < 5% : 距20日低点 < 5%(下方有支撑承接,§17
5. 信号日收阳 : 入场重评确认=资金进场(§19
6. 缩量 vol5/vol20<1.0 : 入场重评确认=抛压衰竭(§19)
出场建议:tp=+30% / sl=-12% / max_hold=40 交易日(RR 2.5:1
排序:bias60 升序(最深超跌优先,§15 自然序=隐式质量排序)
候选写入 sector='v_mr'(管道槽位名不变,下游兼容)
市场门控(2026-08-02 新增):
- 只读 market_regime 表,regime 为 trend_down 或 choppy 时启用扫描
@@ -47,31 +39,18 @@ from datetime import datetime
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
UA = "Mozilla/5.0"
# ── 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 日低动量(基线门槛)
"amount_max": 15, # 百万元 = 1500 万日成交额
"rsi_delta_min": 2, # RSI 5 日回升 ≥2(基线门槛
"mkt_mode": "any", # 实盘由 regime 门控替代(trend_down/choppy 才扫
# ── v_weak 参数(2026-08-03 六步法+12维框架定稿,见 docs/v_mr_strategy.md §19/§21)──
# 弱市(大盘MA20下) + 大盘ADX∈[25,30]甜区(门控在main) + 个股六条件
WEAK_CFG = {
"bias60_min": -35, # bias60 ∈ [-35, -20):深超跌但不过深(过深易续跌)
"bias60_max": -20,
"rsi_max": 25, # RSI ≤ 25:极度超卖
"r5f_max": -3, # 5日急跌 ≤ -3%(下跌动能确认)
"dist_lo20_max": 5, # 距20日低点 < 5%(下方有支撑承接
"vol_shrink_max": 1.0, # 缩量 vol5/vol20 < 1.0(抛压衰竭,入场重评确认
"require_yang": True, # 信号日收阳(资金进场,入场重评确认)
}
# 精选正面因子(六步方法论第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}
EXIT_CFG = {"tp_pct": 0.30, "sl_pct": 0.12, "max_hold_days": 40}
TOP_N = 10
@@ -231,13 +210,15 @@ def load_regime():
# ── v_mr 筛选(精选版:基线6条件 + 正面4因子 + 剔除4负面)──
def check_vmr(klines, mkt_adx=None):
"""对单只股票做 v_mr 精选入场筛选。命中返回信号 dict,否则 None
klines 为升序日K。mkt_adx 为大盘 ADX(来自 market_regime,用于负面因子)。"""
"""对单只股票做 v_weak 入场筛选(2026-08-05 从 v_mr 精选版切换)
六条件:bias60∈[-35,-20) + RSI≤25 + 5日急跌≤-3% + 距20日低点<5% + 收阳 + 缩量。
klines 为升序日K。mkt_adx 保留参数兼容(甜区门控在 main)。"""
if not klines or len(klines) < 70:
return None
closes = [k["close"] for k in klines]
highs = [k["high"] for k in klines]
lows = [k["low"] for k in klines]
vols = [k["volume"] for k in klines]
i = len(klines) - 1 # 最新一日
close = closes[i]
if close <= 0:
@@ -251,76 +232,36 @@ def check_vmr(klines, mkt_adx=None):
if not m60 or m60 <= 0 or rsi is None:
return None
# 1. MA60 下方超跌(单边)
# 1. bias60 ∈ [-35, -20):深超跌但不过深
bias60 = (close - m60) / m60 * 100
if bias60 > MR_CFG["bias_max"]: # 只拦上沿(-10%),无下限
if not (WEAK_CFG["bias60_min"] <= bias60 < WEAK_CFG["bias60_max"]):
return None
# 2. RSI 超卖
if rsi > MR_CFG["rsi_max"]:
# 2. RSI ≤ 25:极度超卖
if rsi > WEAK_CFG["rsi_max"]:
return None
# 3. 60 日跌幅(单边
prev60 = closes[i - 60] if i >= 60 else 0
prev_ret60 = (close - prev60) / prev60 * 100 if prev60 > 0 else 0
if prev_ret60 > MR_CFG["ret_max"]: # 只拦上沿(-15%),无下限
# 3. 5日急跌 ≤ -3%(下跌动能确认
prev5 = closes[i - 5] if i >= 5 else 0
r5f = (close - prev5) / prev5 * 100 if prev5 > 0 else 0
if r5f > WEAK_CFG["r5f_max"]:
return None
# 4. 20 日低动量
prev20 = closes[i - 20] if i >= 20 else 0
mom20 = (close - prev20) / prev20 * 100 if prev20 > 0 else 0
if mom20 > MR_CFG["mom20_max"]:
# 4. 20日低点 < 5%(下方有支撑承接,§17 老莫洞察)
lo20 = min(lows[max(0, i - 19):i + 1])
dist_lo20 = (close - lo20) / lo20 * 100 if lo20 > 0 else 999
if dist_lo20 >= WEAK_CFG["dist_lo20_max"]:
return None
# 5. 小盘(20 日均成交额,百万元
# 腾讯日K volume 单位=手(×100股),成交额=手×100×均价
amt20 = []
for k in klines[max(0, i - 19):i + 1]:
avg_px = (k["high"] + k["low"] + k["close"]) / 3
amt20.append(k["volume"] * 100 * avg_px) # 元
amt_valid = [a for a in amt20 if a and a > 0]
amount_ma20 = sum(amt_valid) / len(amt_valid) if amt_valid else 0
amount_ma20_m = amount_ma20 / 1e6 # 元 → 百万元
if MR_CFG["amount_max"] is not None and amount_ma20_m > MR_CFG["amount_max"]:
# 5. 信号日收阳(入场重评确认:资金进场,§19
if WEAK_CFG["require_yang"] and close <= klines[i]["open"]:
return None
# 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
# ── 精选正面因子(六步方法论第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"]:
# 6. 缩量 vol5/vol20 < 1.0(入场重评确认:抛压衰竭,§19
vol5 = sum(vols[max(0, i - 4):i + 1]) / 5 if i >= 4 else 0
vol20 = sum(vols[max(0, i - 19):i + 1]) / 20 if i >= 19 else 0
vol_shrink = vol5 / vol20 if vol20 > 0 else 99
if vol_shrink >= WEAK_CFG["vol_shrink_max"]:
return None
# 命中 → 出场建议(精选版 exit_cfg)
@@ -333,13 +274,9 @@ def check_vmr(klines, mkt_adx=None):
"price": close,
"bias60": round(bias60, 2),
"rsi": round(rsi, 2),
"prev_ret60": round(prev_ret60, 2),
"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),
"r5f": round(r5f, 2),
"dist_lo20": round(dist_lo20, 2),
"vol_shrink": round(vol_shrink, 3),
"mkt_adx": mkt_adx,
"target": target,
"stop_loss": stop,
@@ -421,10 +358,10 @@ def main():
if done % 400 == 0:
print(f" 已扫描 {done}/{len(pool)}", flush=True)
print(f" 命中 v_mr 精选条件: {len(found)}", flush=True)
print(f" 命中 v_weak 条件: {len(found)}", flush=True)
# 排序:同分按偏度,精选因子已强过滤,按 rsi_delta 降序(止跌确认最强优先)
found.sort(key=lambda x: -x[1]["rsi_delta"])
found.sort(key=lambda x: x[1]["bias60"]) # 最深超跌优先(§15 自然序)
# ── 写 candidates 表(UPSERT,保留计算列)──
conn = sqlite3.connect(str(DB_PATH), timeout=5)
@@ -442,10 +379,9 @@ 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']} "
f"ret60={sig['prev_ret60']}% mom20={sig['mom20']}% "
f"dist20={sig['dist_ma20']}% atr={sig['atr_pct']}% "
f"rsi_delta={sig['rsi_delta']} obv={sig['obv_delta']:.0f})")
reasons = (f"v_weak(bias60={sig['bias60']}% rsi={sig['rsi']} "
f"r5f={sig['r5f']}% dist_lo20={sig['dist_lo20']}% "
f"vol={sig['vol_shrink']} 收阳+缩量确认)")
# 检查是否已在 candidates 且未 promoted
exists = conn.execute(
"SELECT code FROM candidates WHERE code=? AND (promoted IS NULL OR promoted=0)",
@@ -463,10 +399,10 @@ def main():
f"{entry_low}~{entry_high}", sl, tp))
inserted += 1
print(f" 🟢 {code} {name}{price} bias60={sig['bias60']}% "
f"rsi={sig['rsi']} ret60={sig['prev_ret60']}% {reasons}", flush=True)
f"rsi={sig['rsi']} r5f={sig['r5f']}% {reasons}", flush=True)
conn.commit()
conn.close()
print(f" ✅ 新增 {inserted} 只 v_mr 精选候选(前 {top_n}", flush=True)
print(f" ✅ 新增 {inserted} 只 v_weak 候选(前 {top_n}", flush=True)
if __name__ == "__main__":