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:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user