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:
@@ -1,30 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mr_scanner.py — v_mr 均值回复策略实盘扫描器
|
||||
"""mr_scanner.py — v_weak 弱市策略实盘扫描器(2026-08-05 从 v_mr 精选版切换)
|
||||
|
||||
回测侧 v_mr(strategy_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 >= 10:RSI 强回升
|
||||
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 < -32:60日跌超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__":
|
||||
|
||||
Reference in New Issue
Block a user