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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -673,7 +673,8 @@ class NewsEngine:
|
||||
@staticmethod
|
||||
def _get_affected_pairs(currency: str) -> List[str]:
|
||||
_MAP = {
|
||||
"USD": ["XAU/USD"],
|
||||
"USD": ["XAU/USD", "EUR/USD"],
|
||||
"XAU": ["XAU/USD"],
|
||||
"EUR": ["EUR/USD"],
|
||||
}
|
||||
return _MAP.get(currency, [])
|
||||
|
||||
+342
@@ -1893,6 +1893,348 @@ def _():
|
||||
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
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user