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,