Optimize EUR/USD to 78.7% WR on 2-year M15 backtest

Three-layer optimization:

1. Entry filters (signal_aggregator.py section 6e):
   - Block ranging regime
   - Only A+/A/B quality grades
   - min_entry_score=38, min_entry_confidence=60
   - Block RSI > 70 for BUY, RSI < 30 for SELL

2. Trade setup (config.py EurUsdStrategyConfig):
   - RR=0.50 with uniform quality scale
   - SL floor 1.2 ATR, TP 1.0 ATR

3. Breakeven trail (backtest/gpu_backtest.py):
   - When MFE reaches 0.3R, move SL to entry + 0.05R buffer
   - Converts 4 of 14 losses to +0.05R small wins
   - Bar-by-bar simulation integrated into simulate_trades()

Results (EUR/USD M15, 2024-05-18 to 2026-05-18):
  47 trades | 78.7% WR | +1.81R | PF 1.18 | MaxDD 3.35R
XAU/USD unchanged: 77.8% WR on 30-day, 68.7% WR on 2-year.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-18 14:00:59 -04:00
parent 74b723ba06
commit 08d7c9ade1
4 changed files with 191 additions and 42 deletions
+99 -28
View File
@@ -1145,6 +1145,8 @@ def simulate_trades(
trades: List[Trade],
sim_df: pd.DataFrame,
max_bars: int = 200,
be_trigger_r: float = 0.0,
be_buffer_r: float = 0.0,
) -> None:
"""Walk forward bar-by-bar after each entry and resolve SL / TP1.
@@ -1234,41 +1236,107 @@ def simulate_trades(
t.exit_reason = "NO_DATA"
continue
sl_hit_idx = int(sl_first_arr[i])
tp_hit_idx = int(tp_first_arr[i])
risk = abs(t.entry_price - t.sl)
is_buy_flag = t.direction == "BUY"
# SL wins ties (conservative same-bar handling).
if sl_hit_idx < n_valid and sl_hit_idx <= tp_hit_idx:
t.exit_reason = "SL"
t.bars_held = sl_hit_idx + 1
t.exit_price = t.sl
t.r_multiple = -1.0
t.exit_time = exit_times[i][sl_hit_idx]
elif tp_hit_idx < n_valid:
t.exit_reason = "TP1"
t.bars_held = tp_hit_idx + 1
t.exit_price = t.tp1
t.r_multiple = t.rr_planned
t.exit_time = exit_times[i][tp_hit_idx]
if be_trigger_r > 0 and risk > 0:
triggered = False
eff_sl = t.sl
exit_found = False
for j in range(n_valid):
h = float(highs_arr[i, j])
l = float(lows_arr[i, j])
if is_buy_flag:
sl_hit_bar = l <= eff_sl
tp_hit_bar = h >= t.tp1
else:
sl_hit_bar = h >= eff_sl
tp_hit_bar = l <= t.tp1
if sl_hit_bar and tp_hit_bar:
if triggered:
t.exit_reason = "BE"
t.r_multiple = be_buffer_r
t.exit_price = eff_sl
else:
t.exit_reason = "SL"
t.r_multiple = -1.0
t.exit_price = t.sl
t.bars_held = j + 1
t.exit_time = exit_times[i][j]
exit_found = True
break
elif sl_hit_bar:
if triggered:
t.exit_reason = "BE"
t.r_multiple = be_buffer_r
t.exit_price = eff_sl
else:
t.exit_reason = "SL"
t.r_multiple = -1.0
t.exit_price = t.sl
t.bars_held = j + 1
t.exit_time = exit_times[i][j]
exit_found = True
break
elif tp_hit_bar:
t.exit_reason = "TP1"
t.bars_held = j + 1
t.exit_price = t.tp1
t.r_multiple = t.rr_planned
t.exit_time = exit_times[i][j]
exit_found = True
break
if not triggered:
if is_buy_flag:
bar_mfe = (h - t.entry_price) / risk
else:
bar_mfe = (t.entry_price - l) / risk
if bar_mfe >= be_trigger_r:
triggered = True
if is_buy_flag:
eff_sl = t.entry_price + be_buffer_r * risk
else:
eff_sl = t.entry_price - be_buffer_r * risk
if not exit_found:
t.exit_reason = "TIMEOUT"
t.bars_held = int(n_valid)
last_idx = int(n_valid) - 1
t.exit_price = float(closes_arr[i, last_idx])
if risk > 0:
pnl = (t.exit_price - t.entry_price) if is_buy_flag else (t.entry_price - t.exit_price)
t.r_multiple = pnl / risk
t.exit_time = exit_times[i][last_idx]
else:
# Time out at last simulated bar.
t.exit_reason = "TIMEOUT"
t.bars_held = int(n_valid)
last_idx = int(n_valid) - 1
t.exit_price = float(closes_arr[i, last_idx])
risk = abs(t.entry_price - t.sl)
if risk > 0:
pnl = (t.exit_price - t.entry_price) if t.direction == "BUY" else (t.entry_price - t.exit_price)
t.r_multiple = pnl / risk
t.exit_time = exit_times[i][last_idx]
sl_hit_idx = int(sl_first_arr[i])
tp_hit_idx = int(tp_first_arr[i])
if sl_hit_idx < n_valid and sl_hit_idx <= tp_hit_idx:
t.exit_reason = "SL"
t.bars_held = sl_hit_idx + 1
t.exit_price = t.sl
t.r_multiple = -1.0
t.exit_time = exit_times[i][sl_hit_idx]
elif tp_hit_idx < n_valid:
t.exit_reason = "TP1"
t.bars_held = tp_hit_idx + 1
t.exit_price = t.tp1
t.r_multiple = t.rr_planned
t.exit_time = exit_times[i][tp_hit_idx]
else:
t.exit_reason = "TIMEOUT"
t.bars_held = int(n_valid)
last_idx = int(n_valid) - 1
t.exit_price = float(closes_arr[i, last_idx])
if risk > 0:
pnl = (t.exit_price - t.entry_price) if is_buy_flag else (t.entry_price - t.exit_price)
t.r_multiple = pnl / risk
t.exit_time = exit_times[i][last_idx]
# MFE / MAE measured only over bars [0 .. bars_held).
bars = t.bars_held
risk = abs(t.entry_price - t.sl)
if bars > 0 and risk > 0:
h_win = highs_arr[i, :bars]
l_win = lows_arr[i, :bars]
if t.direction == "BUY":
if is_buy_flag:
mfe = (np.nanmax(h_win) - t.entry_price) / risk
mae = (t.entry_price - np.nanmin(l_win)) / risk
else:
@@ -1512,7 +1580,10 @@ def run_backtest(
# ── Resolve trade outcomes ──
t_sim = time.perf_counter()
simulate_trades(trades, primary_df, max_bars=bt_cfg.max_hold_bars)
be_trigger = getattr(platform_cfg.eur_strategy, 'be_trigger_r', 0.0) if platform_cfg.eur_strategy.enabled and bt_cfg.pair != XAUUSD else 0.0
be_buffer = getattr(platform_cfg.eur_strategy, 'be_buffer_r', 0.0) if be_trigger > 0 else 0.0
simulate_trades(trades, primary_df, max_bars=bt_cfg.max_hold_bars,
be_trigger_r=be_trigger, be_buffer_r=be_buffer)
logger.info(
f"Trade simulation took {time.perf_counter() - t_sim:.2f}s "
f"(GPU={'yes' if _GPU_AVAILABLE else 'no'})"