diff --git a/strategy_lab.py b/strategy_lab.py index f0e88f98..7a949113 100644 --- a/strategy_lab.py +++ b/strategy_lab.py @@ -218,6 +218,30 @@ STRATEGIES.update({ 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}), + # G组: 筹码/结构出场(趋势持有与波段) + "v8.0": _v40_branch("v8.0", "趋势持有", + "v7.1入场;出场改结构驱动:破MA10两日/破MA20/横盘出货识别,无固定目标,最长40天", + "用户经验:拉伸段不必早出场,固定15%目标截断利润。让利润奔跑至结构破位或高位放量滞涨(出货)信号出现", + 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": "structure", "sl_atr": 1.5, "max_hold_days": 40}), + "v8.1": _v40_branch("v8.1", "波段先出再进", + "v7.1入场;跌破MA10先出,10日内收回MA10且创新高再进,各段复合计算,最长60天", + "用户经验:调整时先出再进可避开回撤段——破MA10锁定利润,结构恢复再进场吃下一波", + 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", "sl_atr": 1.5, "max_hold_days": 60, "reentry_days": 10}), + "v8.2": _v40_branch("v8.2", "趋势持有(宽入场)", + "v6.1入场;出场同v8.0结构驱动——消融对比:结构出场本身贡献多少", + "对照实验:v6.1固定15%目标 vs v8.2结构持有,同入场下隔离出场模式的贡献", + entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0}, + exit_overrides={"tp_pct": None, "exit_mode": "structure", "sl_atr": 1.5, "max_hold_days": 40}), + "v8.3": _v40_branch("v8.3", "波段40天", + "v8.1持仓期60→40天,检验长尾巴交易的必要性", + "v8.1平均持仓58天接近上限,若40天版收益率不降说明长尾可砍、资金周转更优", + 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", "sl_atr": 1.5, "max_hold_days": 40, "reentry_days": 10}), }) @@ -589,6 +613,86 @@ def run_backtest(strategy_version, start_date, end_date, capital=1000000, save=T exit_reason = 'staged_end' if any(realized) else 'keep' pnl = realized_pnl * 100 exit_price = close * (1 + realized_pnl) + elif exit_cfg.get('exit_mode') == 'structure': + # ── 趋势持有(筹码视角):无固定目标,破位/出货才走 ── + below_ma10 = 0 + dist_lookback = exit_cfg.get('dist_gain', 12) # 涨幅超此值才识别出货 + for k, fb in enumerate(future): + fh, fl, fc = fb.get('high') or 0, fb.get('low') or 0, fb.get('close') or 0 + fv = fb.get('volume') or 0 + fma10, fma20 = fb.get('ma10') or 0, fb.get('ma20') or 0 + if fl <= stop: + exit_price, exit_reason, hold_days = fc, 'stop', k+1 + break + # 横盘出货识别:涨幅>12%后,5日振幅<4% 且 均量>前20日均量1.3倍 + if k >= 5 and (fc - close)/close*100 > dist_lookback: + recent = future[k-4:k+1] + lo = min(x.get('low') or 1e9 for x in recent) + hi = max(x.get('high') or 0 for x in recent) + amp = (hi - lo)/lo*100 if lo > 0 else 99 + avg_vol = sum(x.get('volume') or 0 for x in recent)/5 + base_win = bars[max(0, i-19):i+1] + base_vol = sum(x.get('volume') or 0 for x in base_win)/len(base_win) if base_win else 0 + if amp < 4 and base_vol > 0 and avg_vol > 1.3*base_vol: + exit_price, exit_reason, hold_days = fc, 'distribution', k+1 + break + # 结构破位:连续2日收破MA10,或单日收破MA20 + if fma10 > 0 and fc < fma10: + below_ma10 += 1 + if below_ma10 >= 2: + exit_price, exit_reason, hold_days = fc, 'ma10_break', k+1 + break + else: + below_ma10 = 0 + if fma20 > 0 and fc < fma20: + exit_price, exit_reason, hold_days = fc, 'ma20_break', 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 + elif exit_cfg.get('exit_mode') == 'swing': + # ── 波段操作(先出再进):破MA10出,10日内收回MA10且创新高再进 ── + 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 + fma10 = fb.get('ma10') or 0 + if in_pos: + if fl <= stop_cur: + legs.append(fc/entry_p - 1) + exit_reason = 'stop' + in_pos = False + break + if fma10 > 0 and fc < fma10: + 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 + # 重新站上MA10且当天创新高 → 结构恢复,再进场 + if fma10 > 0 and fc > fma10 and fh > prev_high: + in_pos = True + entry_p = fc + stop_cur = 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