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:
+17
-177
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user