From c7aa01a5daf35a6674855093b7b7cbf4014a2fe3 Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 29 Jul 2026 01:45:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v7=E5=AE=B6=E6=97=8F=20=E2=80=94=20?= =?UTF-8?q?=E5=88=86=E6=89=B9=E6=AD=A2=E7=9B=88=E5=BC=95=E6=93=8E+?= =?UTF-8?q?=E4=BA=8F=E6=8D=9F=E5=B0=B8=E6=A3=80=E5=9B=A0=E5=AD=90(hl?= =?UTF-8?q?=E7=BB=93=E6=9E=84/RSI=E5=A2=9E=E9=87=8F)=EF=BC=8Cv7.1=E5=8F=8C?= =?UTF-8?q?=E8=B5=A273%/+8.01%=EF=BC=8Cv7.2=E8=BE=BE79.7%=E8=83=9C?= =?UTF-8?q?=E7=8E=87=EF=BC=8Cv7.3=E8=BE=BE85.7%/=E5=9B=9E=E6=92=A49.68%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/index.html | 2 +- strategy_lab.py | 100 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/static/index.html b/static/index.html index 0e6467e7..d9acf2ac 100644 --- a/static/index.html +++ b/static/index.html @@ -2059,7 +2059,7 @@ function showStrategyResult(data) { '代码名称入场' + '买入卖出收益' + '结果持仓评分'; - const reasonMap = { target: '🎯止盈', stop: '🛑止损', keep: '⏳到期' }; + const reasonMap = { target: '🎯止盈', stop: '🛑止损', keep: '⏳到期', staged_end: '🪜分批', trail: '📈跟踪' }; for (const t of trades) { const pc = (t.profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400'; html += '' + diff --git a/strategy_lab.py b/strategy_lab.py index 22d904c4..f0e88f98 100644 --- a/strategy_lab.py +++ b/strategy_lab.py @@ -195,6 +195,29 @@ STRATEGIES.update({ "资金流(37.5%→55-78%)与板块(27.3%→80%)两个独立维度的负向排除叠加,期望在v4.0f基础上再提升胜率且不显著减样本", entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "flow_5d_min": -2.5, "sector_slope_max": 1.0}), + # F组: 出场优化 + 尸检因子(基于 v6.1) + "v7.0": _v40_branch("v7.0", "分批止盈", + "v6.1入场不变;出场改50%@+8%落袋+50%@+15%,止损不变", + "亏损尸检:12笔亏损9笔为止损出局——先到+8%落袋一半可将部分止损单转为盈利单;牺牲部分大赢换取胜率", + entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0}, + exit_overrides={"tp_pct": None, "staged_tp": [[0.5, 0.08], [0.5, 0.15]], "sl_atr": 1.5, "max_hold_days": 20}), + "v7.1": _v40_branch("v7.1", "尸检因子过滤", + "v6.1 + 必须hl结构(更高低点) + RSI增量≥6(动量加速)", + "亏损尸检:盈利组100%具备hl结构而亏损组仅75%;盈利组RSI增量11.3 vs 亏损组6.1——动量加速度区分输赢", + entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0, + "hl_only": True, "rsi_delta_min": 6}), + "v7.2": _v40_branch("v7.2", "尸检+分批", + "v7.1入场 + 分批止盈出场(双管齐下)", + "入场端尸检因子过滤+出场端分批止盈,两个独立改进点叠加", + 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, "staged_tp": [[0.5, 0.08], [0.5, 0.15]], "sl_atr": 1.5, "max_hold_days": 20}), + "v7.3": _v40_branch("v7.3", "资金+板块+分批", + "v6.2入场(资金+板块双滤) + 分批止盈出场", + "v6.2的82.1%胜率入场叠加分批止盈,目标在不损胜率前提下改善盈亏结构", + entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, + "flow_5d_min": -2.5, "sector_slope_max": 1.0}, + exit_overrides={"tp_pct": None, "staged_tp": [[0.5, 0.08], [0.5, 0.15]], "sl_atr": 1.5, "max_hold_days": 20}), }) @@ -372,9 +395,11 @@ def pass_filters(factors, filters): # 趋势变化 if not chk('ma20_slope', filters.get('ma20_slope_min'), filters.get('ma20_slope_max')): return False if not chk('macd_hist_delta', filters.get('macd_hist_delta_min'), filters.get('macd_hist_delta_max')): return False + if not chk('rsi_delta', filters.get('rsi_delta_min'), filters.get('rsi_delta_max')): return False if filters.get('adx_rising') and not factors.get('adx_rising'): return False if filters.get('trend_only') and not factors.get('trend_aligned'): return False if filters.get('hh_only') and not factors.get('hh_structure'): return False + if filters.get('hl_only') and not factors.get('hl_structure'): return False if filters.get('no_new_high') and factors.get('near_high_20d'): return False # 大盘 if filters.get('mkt_above_ma20') and factors.get('mkt_above_ma20') is not True: return False @@ -529,32 +554,65 @@ def run_backtest(strategy_version, start_date, end_date, capital=1000000, save=T max_hold = exit_cfg.get('max_hold_days', 20) trail_atr = exit_cfg.get('trail_atr') + staged_tp = exit_cfg.get('staged_tp') # [[frac, pct], ...] 分批止盈 future = bars[i+1:i+1+max_hold] exit_price = exit_reason = None hold_days = 0 highest_close = close - for k, fb in enumerate(future): - fh, fl, fc = fb.get('high') or 0, fb.get('low') or 0, fb.get('close') or 0 - if target and fh >= target: - exit_price, exit_reason, hold_days = target, 'target', k+1 - break - # 移动止损线:随最高收盘价上移,从不下移 - eff_stop = stop - if trail_atr and atr_val > 0: - highest_close = max(highest_close, fc) - eff_stop = max(stop, highest_close - atr_val * trail_atr) - if fl <= eff_stop: - if trail_atr and eff_stop > stop: - exit_price, exit_reason = eff_stop, 'trail' - else: - exit_price, exit_reason = (eff_stop if trail_atr else fc), 'stop' - hold_days = k + 1 - break - if exit_price is None: - exit_price = future[-1].get('close') if future else close - exit_reason, hold_days = 'keep', len(future) - pnl = (exit_price - close) / close * 100 if close > 0 else 0 + if staged_tp: + # ── 分批止盈模拟:按目标分批落袋,止损约束剩余仓位 ── + remaining = 1.0 + realized_pnl = 0.0 + realized = [False] * len(staged_tp) + for k, fb in enumerate(future): + fh, fl, fc = fb.get('high') or 0, fb.get('low') or 0, fb.get('close') or 0 + # 保守假设:同日先触止损 + if fl <= stop: + realized_pnl += remaining * ((fc - close) / close) + remaining = 0 + exit_reason, hold_days = 'stop', k + 1 + break + for si, (frac, tp) in enumerate(staged_tp): + if not realized[si] and fh >= close * (1 + tp): + realized_pnl += frac * tp + remaining -= frac + realized[si] = True + if remaining <= 1e-9: + exit_reason, hold_days = 'target', k + 1 + break + if remaining > 1e-9: + last_c = future[-1].get('close') if future else close + realized_pnl += remaining * ((last_c - close) / close) + hold_days = len(future) + if exit_reason is None: + exit_reason = 'staged_end' if any(realized) else 'keep' + pnl = realized_pnl * 100 + exit_price = close * (1 + realized_pnl) + else: + exit_price = exit_reason = None + hold_days = 0 + for k, fb in enumerate(future): + fh, fl, fc = fb.get('high') or 0, fb.get('low') or 0, fb.get('close') or 0 + if target and fh >= target: + exit_price, exit_reason, hold_days = target, 'target', k+1 + break + # 移动止损线:随最高收盘价上移,从不下移 + eff_stop = stop + if trail_atr and atr_val > 0: + highest_close = max(highest_close, fc) + eff_stop = max(stop, highest_close - atr_val * trail_atr) + if fl <= eff_stop: + if trail_atr and eff_stop > stop: + exit_price, exit_reason = eff_stop, 'trail' + else: + exit_price, exit_reason = (eff_stop if trail_atr else fc), 'stop' + hold_days = k + 1 + break + if exit_price is None: + exit_price = future[-1].get('close') if future else close + exit_reason, hold_days = 'keep', len(future) + pnl = (exit_price - close) / close * 100 if close > 0 else 0 trades.append({ 'code': code, 'name': name, 'entry_date': date,