Add EUR/USD test suite + map EUR news events to EUR/USD

Module 10 (21 tests): EUR config/registry/factory, EUR filter pipeline
(regime/quality/score/confidence/RSI-extended/weekend), EUR trade
geometry (swing SL buffers, pip-denominated spread gate, per-pair gate
contrast), and cross-pair isolation regressions (XAU report bit-equal
with eur_strategy flipped; XAU gates never fire on EUR reports).

news_engine: _get_affected_pairs now maps USD events to EUR/USD too and
adds an EUR entry (calendar warnings reach EUR scans).

175/175 pass.
This commit is contained in:
Harwinder Sekhon
2026-07-10 21:06:55 -04:00
parent 9ed07d4b65
commit 6546b89d6b
2 changed files with 344 additions and 1 deletions
+2 -1
View File
@@ -673,7 +673,8 @@ class NewsEngine:
@staticmethod @staticmethod
def _get_affected_pairs(currency: str) -> List[str]: def _get_affected_pairs(currency: str) -> List[str]:
_MAP = { _MAP = {
"USD": ["XAU/USD"], "USD": ["XAU/USD", "EUR/USD"],
"XAU": ["XAU/USD"], "XAU": ["XAU/USD"],
"EUR": ["EUR/USD"],
} }
return _MAP.get(currency, []) return _MAP.get(currency, [])
+342
View File
@@ -1893,6 +1893,348 @@ def _():
assert np.isfinite(result.indicators["adx"].value), "ADX should be finite on flat data" assert np.isfinite(result.indicators["adx"].value), "ADX should be finite on flat data"
print("\n═══ Module 10: EUR/USD Support ═══")
from config import EURUSD, EurUsdStrategyConfig
# ── 10a. Config & registry ──────────────────────────────────────────
@test("PAIR_INFO includes EUR/USD with correct pip_value")
def _():
assert EURUSD in PAIR_INFO
assert PAIR_INFO[EURUSD]["pip_value"] == 0.0001
assert PAIR_INFO[EURUSD]["is_jpy_pair"] is False
assert PAIR_INFO[EURUSD]["display"] == "EUR/USD"
assert PAIR_INFO[EURUSD]["price_precision"] == 5
assert PAIR_INFO[EURUSD]["default_spread"] == 0.0001
@test("EurUsdStrategyConfig defaults are sensible")
def _():
cfg = EurUsdStrategyConfig()
assert cfg.enabled is True
assert 0 in cfg.blocked_hours_utc
assert 5 not in cfg.allowed_weekdays # No Saturday
assert 6 not in cfg.allowed_weekdays # No Sunday
assert cfg.min_atr_threshold == 0.0003
assert cfg.min_rr_ratio == 1.0
assert cfg.mature_move_threshold == 35.0
assert cfg.blocked_regimes == ["ranging", "transitioning"]
assert "A" in cfg.min_quality_grades
# EUR-local cost knobs must be pip-scale, not gold-dollar-scale
assert cfg.round_trip_cost == 0.0001
assert cfg.round_trip_cost * cfg.min_tp_cost_multiple < 0.01
assert cfg.min_sl_pips == 8.0
@test("PlatformConfig has eur_strategy field")
def _():
config = PlatformConfig()
assert hasattr(config, "eur_strategy")
assert isinstance(config.eur_strategy, EurUsdStrategyConfig)
@test("EUR/USD config factory produces correct config")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
assert config.symbol == EURUSD
assert config.display_name == "EUR/USD"
assert config.pip_value == 0.0001
assert config.xau_strategy.enabled is False
assert config.technical.regime_weights_override
assert "trending" in config.technical.regime_weights_override
assert "ranging" in config.technical.regime_weights_override
@test("XAU/USD config factory keeps XAU strategy enabled")
def _():
from pair_signals.xauusd import make_config
config = make_config(Timeframe.H1)
assert config.symbol == XAUUSD
assert config.display_name == "XAU/USD"
assert config.pip_value == 0.10
assert config.xau_strategy.enabled is True
@test("NewsEngine._get_affected_pairs includes EUR/USD for EUR and USD events")
def _():
from news_engine import NewsEngine
assert "EUR/USD" in NewsEngine._get_affected_pairs("EUR")
assert "EUR/USD" in NewsEngine._get_affected_pairs("USD")
assert "XAU/USD" in NewsEngine._get_affected_pairs("USD")
assert "EUR/USD" not in NewsEngine._get_affected_pairs("GBP")
# ── 10b. EUR filter pipeline ────────────────────────────────────────
@test("EUR/USD signal analysis produces valid report (mocked data)")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
config.backtest.session_filter = False
agg = SignalAggregator(config)
df = make_ohlcv(n=300, start_price=1.0800, trend=0.00005)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
assert report.display_name == "EUR/USD"
assert report.signal_quality in ("A+", "A", "B", "C", "D", "F")
assert not any("SELL FILTER" in w for w in report.warnings)
@test("EUR filter: blocked regime sets quality F")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
config.eur_strategy.blocked_regimes = ["ranging"]
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=42)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
if report.market_regime == "ranging":
assert report.signal_quality == "F", \
f"Ranging should be blocked, got {report.signal_quality}"
@test("EUR filter: quality not in min_quality_grades sets F")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
config.eur_strategy.min_quality_grades = ["A+"]
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=42, trend=0.001)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
if report.signal_quality not in ("A+", "F"):
assert False, f"Non-A+ grade should be blocked, got {report.signal_quality}"
@test("EUR filter: score below min sets F")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
config.eur_strategy.min_entry_score = 99.0
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=42, trend=0.001)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
assert report.signal_quality == "F", \
f"Score 99 threshold should block all, got {report.signal_quality}"
@test("EUR filter: confidence below min sets F")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
config.eur_strategy.min_entry_confidence = 99.0
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=42, trend=0.001)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
assert report.signal_quality == "F", \
f"Confidence 99 threshold should block all, got {report.signal_quality}"
@test("EUR filter: RSI extended buy (RSI>70) blocked")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
config.eur_strategy.block_rsi_extended = True
config.eur_strategy.min_entry_score = 0.0
config.eur_strategy.min_entry_confidence = 0.0
config.eur_strategy.min_quality_grades = ["A+", "A", "B", "C", "D"]
agg = SignalAggregator(config)
df = make_ohlcv(n=300, trend=0.005, seed=42)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
if report.primary_analysis and "rsi" in report.primary_analysis.indicators:
rsi_v = report.primary_analysis.indicators["rsi"].value
if report.final_score > 0 and rsi_v > 70:
assert report.signal_quality == "F", \
f"Buy with RSI {rsi_v:.0f} should be blocked"
@test("EUR weekend filter returns F report on Saturday")
def _():
import signal_aggregator as sa_mod
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
agg = SignalAggregator(config)
saturday = datetime(2026, 7, 11, 12, 0, tzinfo=timezone.utc)
assert saturday.weekday() == 5
with patch.object(sa_mod, "datetime", wraps=datetime) as mock_dt:
mock_dt.now.return_value = saturday
report = agg.analyze_pair(symbol=EURUSD)
assert report.signal_quality == "F"
assert any("WEEKEND" in w for w in report.warnings)
assert report.trade_setup is None
# ── 10c. EUR trade geometry ─────────────────────────────────────────
def _mk_eur_report(score=45.0, quality="A", atr=0.0006, price=1.0800):
from signal_aggregator import SignalReport
r = SignalReport(symbol=EURUSD, display_name="EUR/USD", timestamp="2026-07-08 14:00")
r.final_signal = SignalStrength.BUY
r.final_score = score
r.final_confidence = 70.0
r.signal_quality = quality
class _Analysis:
key_levels = {
"atr": atr,
"swing_low_1": price - 0.0015,
"swing_low_2": price - 0.0030,
"swing_high_1": price + 0.0030,
"swing_high_2": price + 0.0050,
}
r.primary_analysis = _Analysis()
r.current_price = price
return r
@test("EUR trade setup: swing SL with EUR buffers, RR >= min_rr")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.M5)
agg = SignalAggregator(config)
r = _mk_eur_report()
ts = agg._generate_trade_setup(r, 0.0001)
assert ts is not None, f"no setup, warnings: {r.warnings}"
assert ts.pip_value == 0.0001
assert ts.stop_loss < ts.entry_price < ts.take_profit_1
assert ts.risk_pips < 60, f"EUR risk pips insane: {ts.risk_pips}"
assert ts.risk_reward >= config.eur_strategy.min_rr_ratio - 0.01
# levels must NOT be snapped to gold's 2-decimal grid
assert abs(ts.stop_loss * 100 - round(ts.stop_loss * 100)) > 1e-6 or \
abs(ts.take_profit_1 * 100 - round(ts.take_profit_1 * 100)) > 1e-6
@test("EUR spread gate rejects sub-minimum pip targets")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.M5)
config.eur_strategy.min_rr_ratio = 0.1
agg = SignalAggregator(config)
r = _mk_eur_report(atr=0.0002)
r.primary_analysis.key_levels = {"atr": 0.0002}
ts = agg._generate_trade_setup(r, 0.0001)
assert ts is None
assert any("SPREAD GATE" in w and "pip" in w for w in r.warnings), r.warnings
@test("Per-pair spread gates: 40-pip EUR target passes, $4 XAU target rejected")
def _():
from pair_signals.eurusd import make_config
# EUR: 40-pip TP distance clears the 10-pip minimum
config = make_config(Timeframe.M5)
agg = SignalAggregator(config)
r = _mk_eur_report(atr=0.0020)
ts = agg._generate_trade_setup(r, 0.0001)
assert ts is not None and abs(ts.take_profit_1 - ts.entry_price) / 0.0001 >= 10
# XAU: a $4 target must fail gold's $9 minimum (round_trip 0.30 × 30)
xau_cfg = PlatformConfig()
xau_agg = SignalAggregator(xau_cfg)
from signal_aggregator import SignalReport
xr = SignalReport(symbol=XAUUSD, display_name="XAU/USD", timestamp="2026-07-08 14:00")
xr.final_signal = SignalStrength.BUY
xr.final_score = 45.0
xr.final_confidence = 70.0
xr.signal_quality = "B"
class _XA:
key_levels = {"atr": 2.0}
xr.primary_analysis = _XA()
xr.current_price = 4000.0
xau_cfg.signals.min_rr_ratio = 0.1
xau_cfg.signals.tp_floor_rr = 0.1
ts2 = xau_agg._generate_trade_setup(xr, 0.10)
# ATR 2.0 → TP distance ~3, below the $9 gate
assert ts2 is None
assert any("SPREAD GATE" in w and "$" in w for w in xr.warnings), xr.warnings
@test("Trade setup: pip_value zero returns None")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
df = make_ohlcv(n=300, trend=0.002, seed=42)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.primary_analysis:
setup = agg._generate_trade_setup(report, pip_value=0.0)
assert setup is None, "pip_value=0 should produce no trade setup"
# ── 10d. Cross-pair isolation ───────────────────────────────────────
@test("XAU sell gate does not apply to EUR/USD")
def _():
from pair_signals.eurusd import make_config
config = make_config(Timeframe.H1)
config.backtest.session_filter = False
agg = SignalAggregator(config)
df = make_ohlcv(n=300, start_price=1.0800, trend=-0.0005, seed=99)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
assert not any("SELL FILTER" in w for w in report.warnings)
@test("EUR report never carries XAU gate warnings across seeds")
def _():
from pair_signals.eurusd import make_config
for seed in (7, 21, 42, 77, 99):
config = make_config(Timeframe.M5)
agg = SignalAggregator(config)
df = make_ohlcv(n=400, start_price=1.0800, trend=0.0004, seed=seed)
dfs = {Timeframe.M5: df, Timeframe.M15: df, Timeframe.H1: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
leaked = [w for w in report.warnings
if "SELL FILTER" in w or "M5 FILTER" in w or "XAU PULLBACK" in w
or "LOW ATR FILTER" in w]
assert not leaked, f"XAU gates leaked into EUR (seed={seed}): {leaked}"
if report.trade_setup:
assert report.trade_setup.pip_value == 0.0001
@test("XAU report identical whether eur_strategy is enabled or disabled")
def _():
df = make_ohlcv(n=300, seed=42, trend=0.001)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
results = []
for eur_enabled in (True, False):
config = PlatformConfig()
config.eur_strategy.enabled = eur_enabled
agg = SignalAggregator(config)
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
results.append((report.final_score, report.final_confidence,
report.signal_quality, str(report.trade_setup),
tuple(report.warnings)))
assert results[0] == results[1], \
f"XAU output changed with eur_strategy.enabled flip: {results[0]} vs {results[1]}"
# and no EUR warnings on an XAU report
assert not any("EUR" in w for w in results[0][4])
@test("Integration: XAU and EUR produce independent results")
def _():
from pair_signals.eurusd import make_config as eur_make_config
df = make_ohlcv(n=300, seed=42, trend=0.001)
xau_config = PlatformConfig()
xau_agg = SignalAggregator(xau_config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
xau_report = xau_agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
eur_config = eur_make_config(Timeframe.H1)
eur_agg = SignalAggregator(eur_config)
eur_report = eur_agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
assert xau_report.display_name == "XAU/USD"
assert eur_report.display_name == "EUR/USD"
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════
# SUMMARY # SUMMARY
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════