feat: v8筹码结构出场 — 趋势持有(破MA10/出货识别)+波段先出再进,v8.1收益率+12.6%超v7.1达57%,v8.0盈亏比5.06/均亏仅-2.81%
This commit is contained in:
+104
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user