From d768cb7f0f1546a48b3f06005e884c139a3d5176 Mon Sep 17 00:00:00 2001 From: xxm Date: Mon, 3 Aug 2026 00:16:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v=5Fmr=E6=89=AB=E6=8F=8F=E5=99=A8?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=E7=B2=BE=E9=80=89=E8=A7=84=E5=88=99(?= =?UTF-8?q?=E5=85=AD=E6=AD=A5=E6=96=B9=E6=B3=95=E8=AE=BA=E8=B0=83=E4=BC=98?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 替换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% --- deploy/profile-scripts/mr_scanner.py | 156 ++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 28 deletions(-) diff --git a/deploy/profile-scripts/mr_scanner.py b/deploy/profile-scripts/mr_scanner.py index f10a5447..5d5c89c1 100644 --- a/deploy/profile-scripts/mr_scanner.py +++ b/deploy/profile-scripts/mr_scanner.py @@ -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 收敛) +数据源:腾讯前复权日K(qfq),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): + """ATR(Average 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 为升序日K(Sina 返回顺序)。""" +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 >= 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"]: + 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 - # 并发拉日K(ThreadPool 8 并发,Sina 单只逐个拉) + # 并发拉日K(ThreadPool 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__":