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:
@@ -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,10 +1236,79 @@ def simulate_trades(
|
||||
t.exit_reason = "NO_DATA"
|
||||
continue
|
||||
|
||||
risk = abs(t.entry_price - t.sl)
|
||||
is_buy_flag = t.direction == "BUY"
|
||||
|
||||
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:
|
||||
sl_hit_idx = int(sl_first_arr[i])
|
||||
tp_hit_idx = int(tp_first_arr[i])
|
||||
|
||||
# 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
|
||||
@@ -1251,24 +1322,21 @@ def simulate_trades(
|
||||
t.r_multiple = t.rr_planned
|
||||
t.exit_time = exit_times[i][tp_hit_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)
|
||||
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'})"
|
||||
|
||||
@@ -173,11 +173,21 @@ class EurUsdStrategyConfig:
|
||||
tp_atr_multiplier: float = 1.0
|
||||
|
||||
sl_atr_floor: float = 1.2
|
||||
min_rr_ratio: float = 1.0
|
||||
min_rr_ratio: float = 0.5
|
||||
mature_move_threshold: float = 35.0
|
||||
tp_quality_scale: Dict[str, float] = field(default_factory=lambda: {
|
||||
"A+": 1.8, "A": 1.2, "B": 1.1, "C": 1.0,
|
||||
"A+": 1.0, "A": 1.0, "B": 1.0, "C": 1.0,
|
||||
})
|
||||
blocked_regimes: List[str] = field(default_factory=lambda: ["ranging"])
|
||||
min_quality_grades: List[str] = field(default_factory=lambda: ["A+", "A", "B"])
|
||||
max_exhaustion_tags: int = 99
|
||||
block_near_ema200: bool = False
|
||||
session_hours: List[int] = field(default_factory=list)
|
||||
min_entry_score: float = 38.0
|
||||
min_entry_confidence: float = 60.0
|
||||
block_rsi_extended: bool = True
|
||||
be_trigger_r: float = 0.3
|
||||
be_buffer_r: float = 0.05
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+69
-5
@@ -426,6 +426,72 @@ class SignalAggregator:
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 6e. EUR/USD regime + quality + risk gate ─────────────
|
||||
if report.display_name == "EUR/USD" and self.config.eur_strategy.enabled:
|
||||
eur = self.config.eur_strategy
|
||||
if eur.blocked_regimes and report.market_regime in eur.blocked_regimes:
|
||||
report.warnings.append(
|
||||
f"EUR REGIME FILTER: {report.market_regime} blocked"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
if (eur.min_quality_grades
|
||||
and report.signal_quality not in eur.min_quality_grades
|
||||
and report.signal_quality != "F"):
|
||||
report.warnings.append(
|
||||
f"EUR QUALITY FILTER: {report.signal_quality} below minimum"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
if (report.signal_quality != "F"
|
||||
and getattr(report, 'exhaustion_count', 0) > eur.max_exhaustion_tags):
|
||||
report.warnings.append(
|
||||
f"EUR EXHAUSTION FILTER: {report.exhaustion_count} tags > max {eur.max_exhaustion_tags}"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
if (report.signal_quality != "F"
|
||||
and eur.block_near_ema200
|
||||
and any(w.startswith("NEAR EMA200") for w in report.warnings)):
|
||||
report.warnings.append("EUR EMA200 FILTER: too close to EMA200")
|
||||
report.signal_quality = "F"
|
||||
if eur.session_hours and report.signal_quality != "F":
|
||||
try:
|
||||
bar_hour = pd.Timestamp(report.timestamp).hour
|
||||
if bar_hour not in eur.session_hours:
|
||||
report.warnings.append(
|
||||
f"EUR SESSION FILTER: {bar_hour:02}:00 UTC outside allowed hours"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
except Exception:
|
||||
pass
|
||||
if report.signal_quality != "F":
|
||||
abs_s = abs(report.final_score)
|
||||
if abs_s < eur.min_entry_score:
|
||||
report.warnings.append(
|
||||
f"EUR SCORE FILTER: |{abs_s:.1f}| < {eur.min_entry_score}"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
elif report.final_confidence < eur.min_entry_confidence:
|
||||
report.warnings.append(
|
||||
f"EUR CONF FILTER: {report.final_confidence} < {eur.min_entry_confidence}"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
if (report.signal_quality != "F"
|
||||
and eur.block_rsi_extended
|
||||
and report.primary_analysis):
|
||||
rsi_ind = report.primary_analysis.indicators.get("rsi")
|
||||
if rsi_ind:
|
||||
rsi_v = rsi_ind.value
|
||||
is_buy = report.final_score > 0
|
||||
if is_buy and rsi_v > 70:
|
||||
report.warnings.append(
|
||||
f"EUR RSI FILTER: RSI {rsi_v:.0f} overbought for BUY"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
elif not is_buy and rsi_v < 30:
|
||||
report.warnings.append(
|
||||
f"EUR RSI FILTER: RSI {rsi_v:.0f} oversold for SELL"
|
||||
)
|
||||
report.signal_quality = "F"
|
||||
|
||||
# ── 7. Trade setup for actionable signals ──────────────────
|
||||
if report.trade_setup is None and report.signal_quality in ("A+", "A") and report.display_name == "XAU/USD":
|
||||
atr = (report.primary_analysis.key_levels.get("atr", 0)
|
||||
@@ -864,12 +930,10 @@ class SignalAggregator:
|
||||
|
||||
if _is_eur:
|
||||
eur = self.config.eur_strategy
|
||||
q_scale = eur.tp_quality_scale.get(report.signal_quality, 1.0)
|
||||
min_rr = eur.min_rr_ratio * q_scale
|
||||
min_rr = eur.min_rr_ratio
|
||||
mature_move = abs(report.final_score) > eur.mature_move_threshold
|
||||
base_tp = eur.tp_atr_multiplier if mature_move else eur.tp_atr_multiplier * 1.5
|
||||
tp_mult = base_tp * q_scale
|
||||
tp_swing_cap = (2.0 if mature_move else 2.5) * q_scale
|
||||
tp_mult = eur.tp_atr_multiplier if mature_move else eur.tp_atr_multiplier * 1.5
|
||||
tp_swing_cap = 2.0 if mature_move else 2.5
|
||||
sl_atr_floor = eur.sl_atr_floor
|
||||
else:
|
||||
min_rr = self.config.signals.min_rr_ratio
|
||||
|
||||
+11
-7
@@ -1677,11 +1677,14 @@ def _():
|
||||
assert 6 not in cfg.allowed_weekdays # No Sunday
|
||||
assert cfg.min_atr_threshold == 0.0003
|
||||
assert cfg.sl_atr_floor == 1.2
|
||||
assert cfg.min_rr_ratio == 1.0
|
||||
assert cfg.min_rr_ratio == 0.5
|
||||
assert cfg.mature_move_threshold == 35.0
|
||||
assert cfg.tp_quality_scale["A+"] == 1.8
|
||||
assert cfg.tp_quality_scale["A"] == 1.2
|
||||
assert cfg.tp_quality_scale["A+"] == 1.0
|
||||
assert cfg.tp_quality_scale["A"] == 1.0
|
||||
assert cfg.tp_quality_scale["C"] == 1.0
|
||||
assert "ranging" in cfg.blocked_regimes
|
||||
assert "A" in cfg.min_quality_grades
|
||||
assert "C" not in cfg.min_quality_grades
|
||||
|
||||
|
||||
@test("PlatformConfig has eur_strategy field")
|
||||
@@ -1764,21 +1767,22 @@ def _():
|
||||
assert "EUR/USD" not in NewsEngine._get_affected_pairs("GBP")
|
||||
|
||||
|
||||
@test("EUR/USD trade setup uses quality-scaled TP and tighter SL")
|
||||
@test("EUR/USD trade setup uses tighter RR and SL")
|
||||
def _():
|
||||
from pair_signals.eurusd import make_config
|
||||
from signal_aggregator import SignalAggregator
|
||||
config = make_config(Timeframe.H1)
|
||||
config.backtest.session_filter = False
|
||||
config.eur_strategy.blocked_regimes = []
|
||||
config.eur_strategy.min_quality_grades = []
|
||||
assert config.eur_strategy.sl_atr_floor == 1.2
|
||||
assert config.eur_strategy.min_rr_ratio == 1.0
|
||||
assert config.eur_strategy.tp_quality_scale["A"] == 1.2
|
||||
assert config.eur_strategy.min_rr_ratio == 0.5
|
||||
agg = SignalAggregator(config)
|
||||
df = make_ohlcv(n=300, start_price=1.0800, trend=0.0003, seed=77)
|
||||
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
|
||||
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
|
||||
if report.trade_setup:
|
||||
assert report.trade_setup.risk_reward >= 1.0 - 0.01
|
||||
assert report.trade_setup.risk_reward >= 0.5 - 0.01
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user