Remove EUR/USD support — XAU/USD-only codebase

EUR/USD never had a validated edge (2026-07-11/12 verdict: no config
passed validation on M5/M15/H1 at 1-pip cost; cost-to-volatility root
cause). Removed: EurUsdStrategyConfig, pair_signals/eurusd.py, EUR
gates 6h + weekend/day filters + strategy marker + trade geometry in
signal_aggregator, EUR pair registry/aliases in main.py and
gpu_backtest.py, EUR/USD news pair mappings, test_all.py Module 10.
Generic multi-currency news keyword infra kept (GBP/JPY/EUR keyword
table is not pair code). Full EUR history remains at 7d3fd95.

Suite: 162/162 green (was 187; 25 EUR-specific tests removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kby7M9JwpYi8NQT2yoxAi
This commit is contained in:
Harwinder Sekhon
2026-07-16 14:06:27 -04:00
parent 7d3fd9502e
commit 68a62b0a53
13 changed files with 75 additions and 919 deletions
+17 -177
View File
@@ -157,39 +157,6 @@ class SignalAggregator:
report.action_recommendation = f"NO TRADE — Only trading {allowed}"
return report
if pair_info["display"] == "EUR/USD" and self.config.eur_strategy.enabled:
eur_cfg = self.config.eur_strategy
if eur_cfg.weekend_filter and _is_forex_weekend(now_utc):
report = SignalReport(
symbol=symbol,
display_name=pair_info["display"],
timestamp=str(now_utc),
)
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.trade_setup = None
report.warnings.append("WEEKEND FILTER: EUR/USD market closed — no liquidity")
report.action_recommendation = "NO TRADE — Weekend (market closed)"
return report
if eur_cfg.allowed_weekdays and now_utc.weekday() not in eur_cfg.allowed_weekdays:
day_names = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun"}
allowed = "/".join(day_names.get(d, str(d)) for d in eur_cfg.allowed_weekdays)
report = SignalReport(
symbol=symbol,
display_name=pair_info["display"],
timestamp=str(now_utc),
)
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.trade_setup = None
report.warnings.append(
f"DAY FILTER: {day_names.get(now_utc.weekday(), '?')} not in allowed days ({allowed})"
)
report.action_recommendation = f"NO TRADE — Only trading {allowed}"
return report
timeframes = [self.config.primary_timeframe] + list(
self.config.confirmation_timeframes
)
@@ -827,93 +794,6 @@ class SignalAggregator:
report.signal_quality = "D"
report.trade_setup = None
# ── 6h. 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.blocked_vol_regimes
and report.volatility_regime):
if report.volatility_regime in eur.blocked_vol_regimes:
report.warnings.append(
f"EUR VOL FILTER: {report.volatility_regime} volatility blocked"
)
report.signal_quality = "F"
if (report.signal_quality != "F"
and eur.min_indicator_agreement > 0
and report.primary_analysis):
agree_count = sum(
1 for ind in report.primary_analysis.indicators.values()
if ind.signal.value != 0
and (ind.signal.value > 0) == (report.final_score > 0)
)
if agree_count < eur.min_indicator_agreement:
report.warnings.append(
f"EUR AGREE FILTER: {agree_count} < {eur.min_indicator_agreement}"
)
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"
# ── 6i. M5 noise gate (gold-tuned thresholds — XAU only) ──
if (self.config.primary_timeframe == Timeframe.M5
and report.display_name == "XAU/USD"):
@@ -934,17 +814,6 @@ class SignalAggregator:
# ── 8. Reasons ─────────────────────────────────────────────
self._collect_reasons(report)
# ── 8b. EUR strategy marker ────────────────────────────────
# The backtester tags trades from reasons[0]; mark EUR setups so
# they report under the EUR strategy instead of GENERIC.
if (report.display_name == "EUR/USD" and self.config.eur_strategy.enabled
and report.trade_setup is not None):
marker = (f"EUR STRATEGY: {report.market_regime} regime, "
f"score {report.final_score:+.1f}, quality {report.signal_quality}")
target = (report.bullish_reasons if report.trade_setup.direction == "BUY"
else report.bearish_reasons)
target.insert(0, marker)
# ── 9. M5 pullback entry (XAU_PB) ──────────────────────────
# Second archetype: when no breakout setup fired, look for a
# trend-continuation pullback (retrace to EMA21/KC-mid). Runs
@@ -1304,38 +1173,18 @@ class SignalAggregator:
is_buy = report.final_score >= 0
levels = report.primary_analysis.key_levels
_is_eur = report.display_name == "EUR/USD" and self.config.eur_strategy.enabled
min_sl_price = self.config.signals.min_sl_pips * pip_value
min_rr = self.config.signals.min_rr_ratio
tp_floor = max(min_rr, self.config.signals.tp_floor_rr)
tf = self.config.signals.tp_tightening_factor
pre_tight_min_rr = tp_floor / tf if tf > 0 else tp_floor
if _is_eur:
# EUR geometry reads only EurUsdStrategyConfig — nothing
# gold-calibrated from SignalConfig applies here.
eur = self.config.eur_strategy
min_sl_price = eur.min_sl_pips * pip_value
min_rr = eur.min_rr_ratio
# No TP-tightening interplay for EUR: the RR floor used for
# target selection IS min_rr (tp_tightening_factor is 1.0;
# revisit if EUR ever tunes it).
pre_tight_min_rr = min_rr
mature_move = abs(report.final_score) > eur.mature_move_threshold
quality_scale = eur.tp_quality_scale.get(report.signal_quality, 1.0)
tp_mult = (eur.tp_atr_multiplier if mature_move
else eur.tp_atr_multiplier * 1.5) * quality_scale
tp_swing_cap = (2.0 if mature_move else 2.5) * quality_scale
sl_atr_floor = eur.sl_atr_floor
sl_swing_buffer = eur.sl_atr_multiplier
else:
min_sl_price = self.config.signals.min_sl_pips * pip_value
min_rr = self.config.signals.min_rr_ratio
tp_floor = max(min_rr, self.config.signals.tp_floor_rr)
tf = self.config.signals.tp_tightening_factor
pre_tight_min_rr = tp_floor / tf if tf > 0 else tp_floor
tp_mult = 1.5
tp_swing_cap = 2.5
tp_mult = 1.5
tp_swing_cap = 2.5
sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier
if report.display_name == "XAU/USD" else 1.5)
sl_swing_buffer = 1.0
sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier
if report.display_name == "XAU/USD" else 1.5)
sl_swing_buffer = 1.0
if is_buy:
swing_lows = sorted(
@@ -1419,22 +1268,13 @@ class SignalAggregator:
# Spread gate: a target too close to entry hands most of the win
# to transaction costs (validated on M5: sub-$10 targets lose).
if _is_eur:
min_tp = eur.round_trip_cost * eur.min_tp_cost_multiple
if tp1_distance < min_tp:
report.warnings.append(
f"SPREAD GATE: target {tp1_distance / pip_value:.1f} pips "
f"< {min_tp / pip_value:.1f} pip minimum — costs would eat the edge"
)
return None
else:
min_tp = self.config.signals.round_trip_cost * self.config.signals.min_tp_cost_multiple
if tp1_distance < min_tp:
report.warnings.append(
f"SPREAD GATE: target ${tp1_distance:.2f} < ${min_tp:.2f} minimum — "
f"costs would eat the edge"
)
return None
min_tp = self.config.signals.round_trip_cost * self.config.signals.min_tp_cost_multiple
if tp1_distance < min_tp:
report.warnings.append(
f"SPREAD GATE: target ${tp1_distance:.2f} < ${min_tp:.2f} minimum — "
f"costs would eat the edge"
)
return None
risk_pips = sl_distance / pip_value
reward_pips = tp1_distance / pip_value