From b82bd9ac1b37405af1a966212ecddfa05b8599ac Mon Sep 17 00:00:00 2001 From: xxm Date: Wed, 5 Aug 2026 08:35:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20mr=5Fscanner=E5=88=87=E6=8D=A2v=5Fweak?= =?UTF-8?q?=E5=8F=82=E6=95=B0(v=5Fmr=E7=B2=BE=E9=80=89=E7=89=88=E5=B7=B2?= =?UTF-8?q?=E8=AF=81=E4=BC=AA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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% --- deploy/profile-scripts/mr_scanner.py | 184 +++++++++------------------ 1 file changed, 60 insertions(+), 124 deletions(-) diff --git a/deploy/profile-scripts/mr_scanner.py b/deploy/profile-scripts/mr_scanner.py index e9851cbd..75c18e31 100644 --- a/deploy/profile-scripts/mr_scanner.py +++ b/deploy/profile-scripts/mr_scanner.py @@ -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__":