From ff54d37915b2e4c4b1ecaa8f099eb98b1d81a387 Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 29 Jul 2026 13:21:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9E=A2=E8=BD=B4S/R=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E5=9B=9E=E6=B5=8B=E5=BC=95=E6=93=8E+v11=E7=B3=BB=E5=88=97?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=9E=A2=E8=BD=B4=E6=B3=A2=E6=AE=B5(v11.0)4?= =?UTF-8?q?=E4=BB=93+86.3%/=E5=9B=9E=E6=92=A41.3%=E7=A2=BE=E5=8E=8BMA10?= =?UTF-8?q?=E6=B3=A2=E6=AE=B5,=E8=AF=81=E6=98=8E=E5=A4=8D=E5=90=88?= =?UTF-8?q?=E5=BC=BA=E5=BC=B1=E6=94=AF=E6=92=91=E4=BC=98=E4=BA=8E=E7=AE=80?= =?UTF-8?q?=E5=8D=95=E5=9D=87=E7=BA=BF;=E5=9B=BA=E5=AE=9A15%/ATR=E4=BB=8D?= =?UTF-8?q?=E4=BC=98=E4=BA=8E=E8=A3=B8=E6=9E=A2=E8=BD=B4=E5=BC=BA=E5=8E=8B?= =?UTF-8?q?=E6=AD=A2=E7=9B=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backtest_framework.py | 26 ++++++++++++++- strategy_lab.py | 75 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/backtest_framework.py b/backtest_framework.py index 841ad7d9..0c859c97 100644 --- a/backtest_framework.py +++ b/backtest_framework.py @@ -170,9 +170,32 @@ def prepare_bars(code, start_date, end_date): trend_strength = calc_trend_strength(highs, lows, closes) obv = calc_obv(closes, volumes) roc = calc_roc(closes) - + bars = [] for i in range(len(dates)): + # ── 枢轴S/R(与实盘 technical_analysis.calc_support_resistance 同算法)── + piv = {} + if i >= 1: + h, l, c = highs[i], lows[i], closes[i] + yc = closes[i-1] + # 多日波幅(近20日) + win = bars[max(0, i-19):i] if i > 0 else [] + multi_high = max([h] + [x.get('high', 0) or 0 for x in win]) + multi_low = min([l] + [x.get('low', 1e9) or 1e9 for x in win]) + eff_range = max(h - l, multi_high - multi_low, c * 0.05) + tp = (c - multi_low) / (multi_high - multi_low) if multi_high > multi_low else 0.5 + if tp > 0.8 or tp < 0.2: + eff_range = max(eff_range, c * 0.08) + pp = (h + l + c) / 3 + s1 = 2 * pp - h + s2 = pp - eff_range + r1 = 2 * pp - l + r2 = pp + eff_range + if yc < s1: s1 = yc + if yc > r1: r1 = yc + piv = {'pivot': pp, 'weak_support': s1, 'strong_support': s2, + 'weak_resist': r1, 'strong_resist': r2} + bars.append({ 'date': dates[i], 'open': opens[i], @@ -192,6 +215,7 @@ def prepare_bars(code, start_date, end_date): 'adx': trend_strength[i] if i < len(trend_strength) else None, 'obv': obv[i] if i < len(obv) else None, 'roc': roc[i] if i < len(roc) else None, + **piv, }) return bars diff --git a/strategy_lab.py b/strategy_lab.py index dcf22a89..6ccb0029 100644 --- a/strategy_lab.py +++ b/strategy_lab.py @@ -258,6 +258,19 @@ STRATEGIES.update({ "v9归因反用:v7.1交易中weekly_up=False胜率78.4% vs True 60.9%——周线级回调中的日线动量回归正是本策略的核心边缘", entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0, "hl_only": True, "rsi_delta_min": 6, "weekly_down_only": True}), + # I组: 枢轴S/R出场(与实盘technical_analysis同算法) + "v11.0": _v40_branch("v11.0", "波段·枢轴版", + "v8.1入场;出场改枢轴体系:破弱支撑先出,收复弱压且创新高再进", + "v8.1的MA10只是弱支撑的粗糙代理——用实盘同款枢轴S/R验证复合技术位是否优于简单均线", + entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0, + "hl_only": True, "rsi_delta_min": 6}, + exit_overrides={"tp_pct": None, "exit_mode": "swing_pivot", "sl_atr": 1.5, "max_hold_days": 60, "reentry_days": 10}), + "v11.1": _v40_branch("v11.1", "强压止盈弱撑止损", + "v7.1入场;出场改实盘口径:强压止盈+弱支撑止损(枢轴点体系,替代固定15%/1.5ATR)", + "实盘策略的真实出场方式就是枢轴S/R——回测必须验证这个口径而非固定百分比", + entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0, + "hl_only": True, "rsi_delta_min": 6}, + exit_overrides={"tp_pct": None, "use_pivot_sr": True, "max_hold_days": 20}), # ── 港股专用版本(港股通宇宙归因推导,2026-07-29)── "h1.0": { "version": "h1.0", @@ -694,7 +707,13 @@ def run_backtest(strategy_version, start_date, end_date, capital=1000000, save=T if pass_filters(factors, filters): atr_val = last.get('atr') or 0 - if exit_cfg.get('tp_pct'): + if exit_cfg.get('use_pivot_sr'): + # 实盘口径:强压止盈 + 弱支撑止损(枢轴点体系) + target = last.get('strong_resist') if (last.get('strong_resist') or 0) > close else None + stop = last.get('weak_support') if (last.get('weak_support') or 0) < close else (close - atr_val * exit_cfg.get('sl_atr', 1.5) if atr_val > 0 else close * 0.93) + if target is None: + target = close * (1 + exit_cfg.get('tp_pct', 0.15)) + elif exit_cfg.get('tp_pct'): target = close * (1 + exit_cfg['tp_pct']) elif exit_cfg.get('tp_atr') and atr_val > 0: target = close + atr_val * exit_cfg['tp_atr'] @@ -702,12 +721,13 @@ def run_backtest(strategy_version, start_date, end_date, capital=1000000, save=T target = None # 移动止盈模式无固定目标 else: target = close * 1.10 - if exit_cfg.get('sl_atr') and atr_val > 0: - stop = close - atr_val * exit_cfg['sl_atr'] - elif exit_cfg.get('sl_pct'): - stop = close * (1 - exit_cfg['sl_pct']) - else: - stop = close * 0.93 + if not exit_cfg.get('use_pivot_sr'): + if exit_cfg.get('sl_atr') and atr_val > 0: + stop = close - atr_val * exit_cfg['sl_atr'] + elif exit_cfg.get('sl_pct'): + stop = close * (1 - exit_cfg['sl_pct']) + else: + stop = close * 0.93 kelly = 0 if cfg['sizing'].get('kelly'): @@ -832,6 +852,47 @@ def run_backtest(strategy_version, start_date, end_date, capital=1000000, save=T pnl = (total_ret - 1) * 100 exit_price = close * total_ret hold_days = len(future) if future else 0 + elif exit_cfg.get('exit_mode') == 'swing_pivot': + # ── 波段·枢轴版:破弱支撑先出,收复弱压且创新高再进 ── + reentry_window = exit_cfg.get('reentry_days', 10) + legs = [] + in_pos = True + entry_p = close + stop_cur = stop + wait = 0 + exit_reason = 'keep' + for k, fb in enumerate(future): + fh, fl, fc = fb.get('high') or 0, fb.get('low') or 0, fb.get('close') or 0 + fws, fwr = fb.get('weak_support') or 0, fb.get('weak_resist') or 0 + if in_pos: + if fl <= stop_cur: + legs.append(fc/entry_p - 1) + exit_reason = 'stop' + in_pos = False + break + if fws > 0 and fc < fws: + legs.append(fc/entry_p - 1) + in_pos = False + wait = reentry_window + exit_reason = 'swing_out' + else: + wait -= 1 + if wait < 0: + break + prev_high = future[k-1].get('high') or 0 if k > 0 else 0 + if fwr > 0 and fc > fwr and fh > prev_high: + in_pos = True + entry_p = fc + stop_cur = fb.get('weak_support') or (fc - atr_val * exit_cfg.get('sl_atr', 1.5) if atr_val > 0 else fc * 0.93) + exit_reason = 'swing_re' + if in_pos: + legs.append((future[-1].get('close') if future else entry_p)/entry_p - 1) + total_ret = 1.0 + for l in legs: + total_ret *= (1 + l) + pnl = (total_ret - 1) * 100 + exit_price = close * total_ret + hold_days = len(future) if future else 0 else: exit_price = exit_reason = None hold_days = 0