Compare commits

..

2 Commits

Author SHA1 Message Date
harwinder de57c016d1 Tune EUR/USD defaults: tighter SL, higher TP, A-grade filter, unblock ranging
Config changes from EUR/USD optimization rounds:
- sl_atr_multiplier 1.5→0.5, tp_atr_multiplier 1.0→1.5, min_rr 0.5→1.0
- Unblock ranging regime, restrict to A+/A quality on M15
- Lower entry thresholds: score 38→30, confidence 60→50
- Add M5-specific regime weights and config overrides in eurusd.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 17:26:05 -04:00
harwinder e6e09c587e Fix 3 bugs (swing NaN, ROC div-by-zero, currency matching) and add 77 new tests
Bug fixes:
- Swing point detection: false positives when all neighbors are NaN (all([]) == True)
- ROC indicator: division by zero when historical close price is zero
- News currency extraction: substring matching ("usd" in "usda") replaced with word-boundary regex
- ADX DX calculation: use epsilon instead of NaN to avoid cascade
- Signal aggregator: refactor ternary append, add pip_value<=0 guard

New tests (77) cover: swing point edge cases, ROC/ADX/RSI/MACD/BB boundaries,
regime classification, composite scoring, quality grade thresholds, EUR filter
pipeline, trade setup edge cases, MTF agreement, weekend filter, and integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 17:25:52 -04:00
6 changed files with 1040 additions and 32 deletions
+7 -7
View File
@@ -169,22 +169,22 @@ class EurUsdStrategyConfig:
weekend_filter: bool = True
allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4])
min_atr_threshold: float = 0.0003
sl_atr_multiplier: float = 1.5
tp_atr_multiplier: float = 1.0
sl_atr_multiplier: float = 0.5
tp_atr_multiplier: float = 1.5
sl_atr_floor: float = 1.2
min_rr_ratio: float = 0.5
min_rr_ratio: float = 1.0
mature_move_threshold: float = 35.0
tp_quality_scale: Dict[str, float] = field(default_factory=lambda: {
"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"])
blocked_regimes: List[str] = field(default_factory=list)
min_quality_grades: List[str] = field(default_factory=lambda: ["A+", "A"])
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
min_entry_score: float = 30.0
min_entry_confidence: float = 50.0
block_rsi_extended: bool = True
be_trigger_r: float = 0.3
be_buffer_r: float = 0.05
+13 -7
View File
@@ -488,7 +488,7 @@ class TechnicalEngine:
plus_di = 100 * (plus_dm.ewm(alpha=1/c.adx_period, min_periods=c.adx_period, adjust=False).mean() / atr)
minus_di = 100 * (minus_dm.ewm(alpha=1/c.adx_period, min_periods=c.adx_period, adjust=False).mean() / atr)
dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)
dx = 100 * (plus_di - minus_di).abs() / np.maximum(plus_di + minus_di, 1e-10)
adx = dx.ewm(alpha=1/c.adx_period, min_periods=c.adx_period, adjust=False).mean()
adx_val = adx.iloc[-1] if not adx.empty and not pd.isna(adx.iloc[-1]) else 0.0
@@ -598,8 +598,12 @@ class TechnicalEngine:
if len(close) < n + 5:
return
roc_val = ((close.iloc[-1] - close.iloc[-n - 1]) / close.iloc[-n - 1]) * 100
roc_prev = ((close.iloc[-2] - close.iloc[-n - 2]) / close.iloc[-n - 2]) * 100
denom1 = close.iloc[-n - 1]
denom2 = close.iloc[-n - 2]
if denom1 == 0 or denom2 == 0:
return
roc_val = ((close.iloc[-1] - denom1) / denom1) * 100
roc_prev = ((close.iloc[-2] - denom2) / denom2) * 100
if pd.isna(roc_val) or pd.isna(roc_prev):
return
@@ -1112,12 +1116,14 @@ class TechnicalEngine:
for i in range(window, len(vals) - window):
if np.isnan(vals[i]):
continue
left_vals = [vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])]
right_vals = [vals[i+j] for j in range(1, window+1) if i+j < len(vals) and not np.isnan(vals[i+j])]
if not left_vals or not right_vals:
continue
if mode == "low":
if all(vals[i] <= vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])) and \
all(vals[i] <= vals[i+j] for j in range(1, window+1) if i+j < len(vals) and not np.isnan(vals[i+j])):
if all(vals[i] <= v for v in left_vals) and all(vals[i] <= v for v in right_vals):
points.append((i, vals[i]))
else:
if all(vals[i] >= vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])) and \
all(vals[i] >= vals[i+j] for j in range(1, window+1) if i+j < len(vals) and not np.isnan(vals[i+j])):
if all(vals[i] >= v for v in left_vals) and all(vals[i] >= v for v in right_vals):
points.append((i, vals[i]))
return points
+3 -2
View File
@@ -497,11 +497,12 @@ class NewsEngine:
found = []
for currency, keywords in self.CURRENCY_KEYWORDS.items():
if currency.lower() in headline_lower:
if re.search(r'\b' + re.escape(currency.lower()) + r'\b', headline_lower):
found.append(currency)
continue
for kw in keywords:
if kw in headline_lower:
kw_stripped = kw.strip()
if kw_stripped and re.search(r'\b' + re.escape(kw_stripped) + r'\b', headline_lower):
found.append(currency)
break
+31 -1
View File
@@ -32,6 +32,28 @@ EUR_REGIME_WEIGHTS = {
},
}
_M5_TRENDING = {
"ema_trend": 1.5, "rsi": 1.0, "macd": 2.5,
"bollinger": 0.5, "keltner": 2.0, "roc": 2.0,
"candlestick": 0.3, "divergence": 1.0, "adx": 1.0,
}
M5_REGIME_WEIGHTS = {
"trending_up": _M5_TRENDING,
"trending_down": _M5_TRENDING,
"trending": _M5_TRENDING,
"ranging": {
"ema_trend": 0.3, "rsi": 1.5, "macd": 1.5,
"bollinger": 1.5, "keltner": 2.0, "roc": 1.0,
"candlestick": 0.3, "divergence": 1.5, "adx": 0.5,
},
"transitioning": {
"ema_trend": 1.0, "rsi": 1.0, "macd": 2.5,
"bollinger": 0.5, "keltner": 2.5, "roc": 2.0,
"candlestick": 0.3, "divergence": 1.0, "adx": 1.0,
},
}
def make_config(
primary_tf: Timeframe,
@@ -53,6 +75,14 @@ def make_config(
config.backtest.blocked_hours_utc = [22, 23, 0, 1, 2, 3]
config.backtest.allowed_weekdays = [0, 1, 2, 3, 4]
config.technical.regime_weights_override = EUR_REGIME_WEIGHTS
if primary_tf == Timeframe.M5:
config.technical.regime_weights_override = M5_REGIME_WEIGHTS
config.eur_strategy.sl_atr_floor = 0.8
config.eur_strategy.sl_atr_multiplier = 0.3
config.eur_strategy.min_quality_grades = ["A+", "A", "B"]
config.eur_strategy.min_entry_score = 28.0
config.eur_strategy.min_entry_confidence = 50.0
else:
config.technical.regime_weights_override = EUR_REGIME_WEIGHTS
return config
+16 -11
View File
@@ -252,11 +252,11 @@ class SignalAggregator:
)
report.trade_setup = None
else:
report.bullish_reasons.append(
f"Regime: {regime_tf.value} ADX={adx_value:.1f} confirms trending market"
) if report.final_score > 0 else report.bearish_reasons.append(
f"Regime: {regime_tf.value} ADX={adx_value:.1f} confirms trending market"
)
reason = f"Regime: {regime_tf.value} ADX={adx_value:.1f} confirms trending market"
if report.final_score > 0:
report.bullish_reasons.append(reason)
else:
report.bearish_reasons.append(reason)
@staticmethod
def _latest_adx(df: pd.DataFrame, period: int = 14) -> Optional[float]:
@@ -915,6 +915,8 @@ class SignalAggregator:
)
return None
if pip_value <= 0:
return None
min_sl_pips = 8.0 if pip_value < 0.01 else 80.0
min_sl_price = min_sl_pips * pip_value
@@ -932,9 +934,11 @@ class SignalAggregator:
eur = self.config.eur_strategy
min_rr = eur.min_rr_ratio
mature_move = abs(report.final_score) > eur.mature_move_threshold
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
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_rr = self.config.signals.min_rr_ratio
mature_move = abs(report.final_score) > 30
@@ -942,6 +946,7 @@ class SignalAggregator:
tp_swing_cap = 2.0 if mature_move else 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
if is_buy:
swing_lows = sorted(
@@ -949,9 +954,9 @@ class SignalAggregator:
reverse=True,
)
if swing_lows:
sl_price = swing_lows[0] - atr * 1.0
sl_price = swing_lows[0] - atr * sl_swing_buffer
else:
sl_price = price - atr * 2.0
sl_price = price - atr * (1.0 + sl_swing_buffer)
min_sl = max(atr * sl_atr_floor, min_sl_price)
if price - sl_price < min_sl:
sl_price = price - min_sl
@@ -979,9 +984,9 @@ class SignalAggregator:
v for k, v in levels.items() if k.startswith("swing_high_") and v > price
)
if swing_highs:
sl_price = swing_highs[0] + atr * 1.0
sl_price = swing_highs[0] + atr * sl_swing_buffer
else:
sl_price = price + atr * 2.0
sl_price = price + atr * (1.0 + sl_swing_buffer)
min_sl = max(atr * sl_atr_floor, min_sl_price)
if sl_price - price < min_sl:
sl_price = price + min_sl
+970 -4
View File
@@ -1677,14 +1677,15 @@ 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 == 0.5
assert cfg.min_rr_ratio == 1.0
assert cfg.mature_move_threshold == 35.0
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 cfg.blocked_regimes == []
assert "A" in cfg.min_quality_grades
assert "C" not in cfg.min_quality_grades
assert "A" in cfg.min_quality_grades
assert "B" not in cfg.min_quality_grades
@test("PlatformConfig has eur_strategy field")
@@ -1776,7 +1777,7 @@ def _():
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 == 0.5
assert config.eur_strategy.min_rr_ratio == 1.0
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}
@@ -1785,6 +1786,971 @@ def _():
assert report.trade_setup.risk_reward >= 0.5 - 0.01
# ══════════════════════════════════════════════════════════════════════
# MODULE 10: Bug Fix Regressions & Comprehensive Edge Cases
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 10: Bug Fix Regressions & Edge Cases ═══")
# ── 10a. Swing Point Bug Fix (engine.py) ─────────────────────────────
@test("Swing points: all-NaN neighbors produces no false positives")
def _():
series = pd.Series([np.nan, np.nan, np.nan, 5.0, np.nan, np.nan, np.nan])
lows = TechnicalEngine._find_swing_points(series, mode="low", window=3)
highs = TechnicalEngine._find_swing_points(series, mode="high", window=3)
assert lows == [], f"Expected no lows with all-NaN neighbors, got {lows}"
assert highs == [], f"Expected no highs with all-NaN neighbors, got {highs}"
@test("Swing points: partial NaN neighbors still detect valid swings")
def _():
series = pd.Series([10.0, 8.0, np.nan, 4.0, np.nan, 8.0, 10.0])
lows = TechnicalEngine._find_swing_points(series, mode="low", window=2)
low_vals = [v for _, v in lows]
assert 4.0 in low_vals, f"Expected 4.0 as swing low, got {low_vals}"
@test("Swing points: empty series returns empty list")
def _():
series = pd.Series([], dtype=float)
assert TechnicalEngine._find_swing_points(series, mode="low") == []
assert TechnicalEngine._find_swing_points(series, mode="high") == []
@test("Swing points: series shorter than 2*window returns empty")
def _():
series = pd.Series([1.0, 2.0, 3.0])
assert TechnicalEngine._find_swing_points(series, mode="low", window=3) == []
@test("Swing points: monotonic increasing has no interior lows")
def _():
series = pd.Series([float(i) for i in range(20)])
lows = TechnicalEngine._find_swing_points(series, mode="low", window=2)
assert len(lows) == 0, f"Monotonic increase should have no interior lows, got {lows}"
@test("Swing points: monotonic decreasing has no interior highs")
def _():
series = pd.Series([float(20 - i) for i in range(20)])
highs = TechnicalEngine._find_swing_points(series, mode="high", window=2)
assert len(highs) == 0, f"Monotonic decrease should have no interior highs, got {highs}"
@test("Swing points: V-shape detects low at bottom")
def _():
series = pd.Series([10.0, 8.0, 6.0, 4.0, 6.0, 8.0, 10.0])
lows = TechnicalEngine._find_swing_points(series, mode="low", window=2)
low_vals = [v for _, v in lows]
assert 4.0 in low_vals, f"V-shape should detect 4.0 as low, got {low_vals}"
@test("Swing points: inverted V-shape detects high at top")
def _():
series = pd.Series([4.0, 6.0, 8.0, 10.0, 8.0, 6.0, 4.0])
highs = TechnicalEngine._find_swing_points(series, mode="high", window=2)
high_vals = [v for _, v in highs]
assert 10.0 in high_vals, f"Inverted V should detect 10.0 as high, got {high_vals}"
# ── 10b. ROC Bug Fix (engine.py) ─────────────────────────────────────
@test("ROC: zero close at lookback position does not produce inf")
def _():
df = make_ohlcv(n=100, seed=42)
df.iloc[-11, df.columns.get_loc("Close")] = 0.0
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
if "roc" in result.indicators:
assert np.isfinite(result.indicators["roc"].value), "ROC should not be inf"
@test("ROC: normal computation produces finite values")
def _():
df = make_ohlcv(n=100, seed=55)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
if "roc" in result.indicators:
assert np.isfinite(result.indicators["roc"].value)
assert 0 <= result.indicators["roc"].confidence <= 100
# ── 10c. ADX Epsilon Fix (engine.py) ────────────────────────────────
@test("ADX: flat data produces finite ADX value")
def _():
n = 100
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
df = pd.DataFrame({
"Open": [100.0]*n, "High": [100.01]*n, "Low": [99.99]*n,
"Close": [100.0]*n, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
if "adx" in result.indicators:
assert np.isfinite(result.indicators["adx"].value), "ADX should be finite on flat data"
@test("ADX: strong uptrend shows high ADX")
def _():
df = make_ohlcv(n=300, trend=0.001, seed=10)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert "adx" in result.indicators
assert result.indicators["adx"].value > 0
# ── 10d. RSI Boundaries ─────────────────────────────────────────────
@test("RSI: monotonic up approaches 100")
def _():
n = 100
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
close = np.linspace(100, 150, n)
df = pd.DataFrame({
"Open": close - 0.1, "High": close + 0.5, "Low": close - 0.5,
"Close": close, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert "rsi" in result.indicators
assert result.indicators["rsi"].value > 80, f"RSI should be high on uptrend, got {result.indicators['rsi'].value}"
@test("RSI: monotonic down approaches 0")
def _():
n = 100
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
close = np.linspace(150, 100, n)
df = pd.DataFrame({
"Open": close + 0.1, "High": close + 0.5, "Low": close - 0.5,
"Close": close, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert "rsi" in result.indicators
assert result.indicators["rsi"].value < 20, f"RSI should be low on downtrend, got {result.indicators['rsi'].value}"
@test("RSI: flat price produces RSI near 50")
def _():
n = 100
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
np.random.seed(99)
close = 100.0 + np.random.randn(n) * 0.001
df = pd.DataFrame({
"Open": close, "High": close + 0.01, "Low": close - 0.01,
"Close": close, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
if "rsi" in result.indicators:
rsi = result.indicators["rsi"].value
assert 30 <= rsi <= 70, f"RSI on flat should be near 50, got {rsi}"
# ── 10e. MACD Edge Cases ────────────────────────────────────────────
@test("MACD: very short data (< 35 bars) skips cleanly")
def _():
df = make_ohlcv(n=30, seed=42)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
# Should not crash; macd may or may not be present
@test("MACD: strong uptrend produces non-SELL signal")
def _():
df = make_ohlcv(n=300, trend=0.001, seed=20)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
if "macd" in result.indicators:
assert result.indicators["macd"].signal not in (SignalStrength.SELL, SignalStrength.STRONG_SELL), \
f"MACD on uptrend should not be bearish, got {result.indicators['macd'].signal}"
# ── 10f. Bollinger Bands Edge Cases ─────────────────────────────────
@test("Bollinger: flat price collapses bands, position near 0.5")
def _():
n = 100
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
df = pd.DataFrame({
"Open": [100.0]*n, "High": [100.01]*n, "Low": [99.99]*n,
"Close": [100.0]*n, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
if "bollinger" in result.indicators:
pos = result.indicators["bollinger"].value
assert 0.0 <= pos <= 1.0, f"BB position should be in [0,1], got {pos}"
# ── 10g. Keltner Channel Edge Cases ────────────────────────────────
@test("Keltner: BB disabled does not crash squeeze detection")
def _():
df = make_ohlcv(n=200, seed=42)
config = PlatformConfig()
config.technical.disabled_indicators = ["bollinger"]
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert "keltner" in result.indicators, "Keltner should still work with BB disabled"
# ── 10h. Candlestick Pattern Edge Cases ─────────────────────────────
@test("Candlestick: zero-range bar (H==L) does not crash")
def _():
df = make_ohlcv(n=100, seed=42)
df.iloc[-1, df.columns.get_loc("High")] = df.iloc[-1]["Close"]
df.iloc[-1, df.columns.get_loc("Low")] = df.iloc[-1]["Close"]
df.iloc[-1, df.columns.get_loc("Open")] = df.iloc[-1]["Close"]
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert result is not None
@test("Candlestick: very short data (< 5 bars) does not crash")
def _():
df = make_ohlcv(n=4, seed=42)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert result is not None
# ── 10i. Regime Classification ──────────────────────────────────────
@test("Regime: strong uptrend produces trending_up or trending")
def _():
df = make_ohlcv(n=300, trend=0.002, seed=15)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert result.market_regime in ("trending_up", "trending", "transitioning"), \
f"Strong uptrend should be trending, got {result.market_regime}"
@test("Regime: strong downtrend produces trending_down or trending")
def _():
df = make_ohlcv(n=300, trend=-0.002, seed=16)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert result.market_regime in ("trending_down", "trending", "transitioning"), \
f"Strong downtrend should be trending, got {result.market_regime}"
@test("Regime: flat/choppy data produces ranging or transitioning")
def _():
n = 300
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
np.random.seed(17)
close = 100.0 + np.random.randn(n) * 0.001
df = pd.DataFrame({
"Open": close, "High": close + 0.002, "Low": close - 0.002,
"Close": close, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert result.market_regime in ("ranging", "transitioning"), \
f"Flat data should be ranging, got {result.market_regime}"
# ── 10j. Composite Score ────────────────────────────────────────────
@test("Composite score always clamped to [-100, 100]")
def _():
for seed in [1, 2, 3, 10, 20, 30]:
df = make_ohlcv(n=300, trend=0.005, seed=seed)
config = PlatformConfig()
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert -100 <= result.composite_score <= 100, \
f"Score {result.composite_score} out of bounds (seed={seed})"
@test("Composite: regime weights affect score magnitude")
def _():
df = make_ohlcv(n=300, trend=0.001, seed=42)
config1 = PlatformConfig()
config2 = PlatformConfig()
config2.technical.regime_weights_override = {
"trending_up": {"ema_trend": 0.1, "rsi": 0.1, "macd": 0.1,
"bollinger": 0.1, "keltner": 0.1, "roc": 0.1,
"candlestick": 0.1, "divergence": 0.1, "adx": 0.1},
"trending_down": {"ema_trend": 0.1, "rsi": 0.1, "macd": 0.1,
"bollinger": 0.1, "keltner": 0.1, "roc": 0.1,
"candlestick": 0.1, "divergence": 0.1, "adx": 0.1},
"ranging": {"ema_trend": 0.1, "rsi": 0.1, "macd": 0.1,
"bollinger": 0.1, "keltner": 0.1, "roc": 0.1,
"candlestick": 0.1, "divergence": 0.1, "adx": 0.1},
"transitioning": {"ema_trend": 0.1, "rsi": 0.1, "macd": 0.1,
"bollinger": 0.1, "keltner": 0.1, "roc": 0.1,
"candlestick": 0.1, "divergence": 0.1, "adx": 0.1},
}
engine1 = TechnicalEngine(config1.technical)
engine2 = TechnicalEngine(config2.technical)
r1 = engine1.analyze(df, "EUR/USD", Timeframe.H1)
r2 = engine2.analyze(df, "EUR/USD", Timeframe.H1)
assert r1.composite_score != r2.composite_score or r1.market_regime != r2.market_regime, \
"Different regime weights should produce different results"
# ── 10k. Divergence Edge Cases ──────────────────────────────────────
@test("Divergence: short lookback finds no divergence")
def _():
df = make_ohlcv(n=100, seed=42)
config = PlatformConfig()
config.technical.divergence_lookback = 5
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
# Should not crash, divergence may or may not appear
@test("Divergence: disabled divergence not in indicators")
def _():
df = make_ohlcv(n=300, seed=42)
config = PlatformConfig()
config.technical.disabled_indicators = ["divergence"]
engine = TechnicalEngine(config.technical)
result = engine.analyze(df, "EUR/USD", Timeframe.H1)
assert "divergence" not in result.indicators
# ══════════════════════════════════════════════════════════════════════
# MODULE 11: News Engine Edge Cases
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 11: News Engine Edge Cases ═══")
from news_engine import NewsEngine
@test("Currency extraction: USDA does not false-match USD")
def _():
ne = NewsEngine(PlatformConfig().news)
result = ne._extract_currencies("USDA releases crop report")
assert "USD" not in result, f"USDA should not match USD, got {result}"
@test("Currency extraction: custodian does not false-match USD")
def _():
ne = NewsEngine(PlatformConfig().news)
result = ne._extract_currencies("custodian reports quarterly earnings")
assert "USD" not in result, f"custodian should not match USD, got {result}"
@test("Currency extraction: euro matches EUR correctly")
def _():
ne = NewsEngine(PlatformConfig().news)
result = ne._extract_currencies("euro strengthens against yen")
assert "EUR" in result, f"euro should match EUR, got {result}"
@test("Currency extraction: Eurozone matches EUR")
def _():
ne = NewsEngine(PlatformConfig().news)
result = ne._extract_currencies("Eurozone GDP beats forecast")
assert "EUR" in result, f"Eurozone should match EUR, got {result}"
@test("Currency extraction: empty headline returns empty list")
def _():
ne = NewsEngine(PlatformConfig().news)
result = ne._extract_currencies("")
assert result == [], f"Empty headline should return empty list, got {result}"
@test("Currency extraction: headline with multiple currencies")
def _():
ne = NewsEngine(PlatformConfig().news)
result = ne._extract_currencies("EUR/USD pair rises as dollar weakens")
assert "EUR" in result, f"Should match EUR, got {result}"
assert "USD" in result, f"Should match USD, got {result}"
@test("Sentiment: both bullish and bearish keywords gives bounded score")
def _():
ne = NewsEngine(PlatformConfig().news)
score = ne._analyze_headline_sentiment("rate hike fears amid bullish recovery expectations")
assert -1.0 <= score <= 1.0, f"Score should be bounded, got {score}"
@test("Sentiment: empty headline returns 0.0")
def _():
ne = NewsEngine(PlatformConfig().news)
assert ne._analyze_headline_sentiment("") == 0.0
@test("Sentiment: very long headline does not crash")
def _():
ne = NewsEngine(PlatformConfig().news)
long_headline = "market " * 500 + "bullish recovery"
score = ne._analyze_headline_sentiment(long_headline)
assert isinstance(score, float)
# ══════════════════════════════════════════════════════════════════════
# MODULE 12: Signal Aggregator Edge Cases
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 12: Signal Aggregator Edge Cases ═══")
# ── 12a. Quality Grade Boundaries ────────────────────────────────────
@test("Quality grade: A+ requires score>=40, confidence>=65, MTF agreement")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
df = make_ohlcv(n=300, trend=0.003, seed=100)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.signal_quality == "A+":
assert abs(report.final_score) >= 40
assert report.final_confidence >= 65
assert report.mtf_agreement is True
@test("Quality grade: A requires score>=33, confidence>=55, MTF")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
df = make_ohlcv(n=300, trend=0.002, seed=200)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.signal_quality == "A":
assert abs(report.final_score) >= 33
assert report.final_confidence >= 55
assert report.mtf_agreement is True
@test("Quality grade: B requires score>=28, confidence>=50")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=300)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.signal_quality == "B":
assert abs(report.final_score) >= 28
assert report.final_confidence >= 50
@test("Quality grade: C requires score>=25, confidence>=45")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=400)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.signal_quality == "C":
assert abs(report.final_score) >= 25
assert report.final_confidence >= 45
@test("Quality grade: D requires score>=15")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=500)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.signal_quality == "D":
assert abs(report.final_score) >= 15
@test("Quality grade: F has score<15 or zero confidence")
def _():
n = 300
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
np.random.seed(600)
close = 100.0 + np.random.randn(n) * 0.0001
df = pd.DataFrame({
"Open": close, "High": close + 0.001, "Low": close - 0.001,
"Close": close, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.signal_quality == "F":
assert abs(report.final_score) < 15 or report.final_confidence < 45
@test("Quality grade: all grades belong to valid set")
def _():
valid_grades = {"A+", "A", "B", "C", "D", "F"}
for seed in range(42, 62):
df = make_ohlcv(n=300, seed=seed, trend=0.001 * (seed % 5 - 2))
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert report.signal_quality in valid_grades, \
f"Unexpected grade '{report.signal_quality}' (seed={seed})"
# ── 12b. EUR Filter Pipeline ────────────────────────────────────────
@test("EUR filter: blocked regime sets quality F")
def _():
from pair_signals.eurusd import make_config as eur_make_config
config = eur_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 as eur_make_config
config = eur_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 as eur_make_config
config = eur_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 as eur_make_config
config = eur_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 as eur_make_config
config = eur_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"
# ── 12c. Trade Setup Edge Cases ─────────────────────────────────────
@test("Trade setup: ATR 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:
report.primary_analysis.key_levels["atr"] = 0.0
setup = agg._generate_trade_setup(report, pip_value=config.pip_value)
assert setup is None, "ATR=0 should produce no trade setup"
@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 and report.final_signal != SignalStrength.NEUTRAL:
setup = agg._generate_trade_setup(report, pip_value=0.0)
assert setup is None, "pip_value=0 should produce no trade setup"
@test("Trade setup: BUY SL always below entry")
def _():
for seed in [10, 20, 30, 40, 50]:
df = make_ohlcv(n=300, trend=0.002, seed=seed)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.trade_setup and report.trade_setup.direction == "BUY":
assert report.trade_setup.stop_loss < report.trade_setup.entry_price, \
f"BUY SL should be below entry (seed={seed})"
@test("Trade setup: SELL SL always above entry")
def _():
for seed in [10, 20, 30, 40, 50]:
df = make_ohlcv(n=300, trend=-0.002, seed=seed)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.trade_setup and report.trade_setup.direction == "SELL":
assert report.trade_setup.stop_loss > report.trade_setup.entry_price, \
f"SELL SL should be above entry (seed={seed})"
@test("Trade setup: risk_reward is always positive when present")
def _():
for seed in range(10, 30):
df = make_ohlcv(n=300, trend=0.001 * (seed % 5 - 2), seed=seed)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.trade_setup:
assert report.trade_setup.risk_reward > 0, \
f"RR should be positive, got {report.trade_setup.risk_reward} (seed={seed})"
@test("Trade setup: EUR uses EUR-specific parameters")
def _():
from pair_signals.eurusd import make_config as eur_make_config
config = eur_make_config(Timeframe.H1)
agg = SignalAggregator(config)
df = make_ohlcv(n=300, trend=0.002, start_price=1.08, 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 >= config.eur_strategy.min_rr_ratio - 0.01
# ── 12d. MTF Agreement ──────────────────────────────────────────────
@test("MTF: all NEUTRAL returns False")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
report.mtf_signals = {
"15min": SignalStrength.NEUTRAL,
"1h": SignalStrength.NEUTRAL,
"4h": SignalStrength.NEUTRAL,
}
assert agg._check_mtf_agreement(report) is False
@test("MTF: 2 bearish + 1 bullish returns True (bearish agreement)")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
report.mtf_signals = {
"15min": SignalStrength.SELL,
"1h": SignalStrength.STRONG_SELL,
"4h": SignalStrength.BUY,
}
assert agg._check_mtf_agreement(report) is True
@test("MTF: all same direction returns True")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
report.mtf_signals = {
"15min": SignalStrength.BUY,
"1h": SignalStrength.BUY,
"4h": SignalStrength.STRONG_BUY,
}
assert agg._check_mtf_agreement(report) is True
@test("MTF: empty signals returns False")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
report.mtf_signals = {}
assert agg._check_mtf_agreement(report) is False
# ── 12e. Final Signal Computation ───────────────────────────────────
@test("Final signal: STRONG_BUY requires score>=35, confidence>=55")
def _():
for seed in range(42, 62):
df = make_ohlcv(n=300, trend=0.001 * (seed % 4), seed=seed)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.final_signal == SignalStrength.STRONG_BUY:
assert report.final_score >= 35, f"STRONG_BUY needs score>=35, got {report.final_score}"
assert report.final_confidence >= 55, f"STRONG_BUY needs conf>=55, got {report.final_confidence}"
@test("Final signal: STRONG_SELL requires score<=-35, confidence>=55")
def _():
for seed in range(42, 62):
df = make_ohlcv(n=300, trend=-0.001 * (seed % 4), seed=seed)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if report.final_signal == SignalStrength.STRONG_SELL:
assert report.final_score <= -35, f"STRONG_SELL needs score<=-35, got {report.final_score}"
assert report.final_confidence >= 55, f"STRONG_SELL needs conf>=55, got {report.final_confidence}"
@test("Final signal: NEUTRAL when score is low regardless of confidence")
def _():
n = 300
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
np.random.seed(42)
close = 100.0 + np.random.randn(n) * 0.0001
df = pd.DataFrame({
"Open": close, "High": close + 0.001, "Low": close - 0.001,
"Close": close, "Volume": [1000.0]*n,
}, index=dates)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
if abs(report.final_score) < 25:
assert report.final_signal == SignalStrength.NEUTRAL
# ── 12f. Penalty System ─────────────────────────────────────────────
@test("Penalty: confidence never exceeds 95 or goes below 0")
def _():
for seed in range(42, 72):
df = make_ohlcv(n=300, trend=0.001 * (seed % 6 - 3), seed=seed)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert 0.0 <= report.final_confidence <= 95.0, \
f"Confidence out of [0,95] range: {report.final_confidence} (seed={seed})"
# ── 12g. Weekend Filter ─────────────────────────────────────────────
@test("Weekend filter: Friday 22:01 UTC is weekend")
def _():
from signal_aggregator import _is_forex_weekend
dt = datetime(2026, 5, 15, 22, 1, tzinfo=timezone.utc) # Friday
assert dt.weekday() == 4
assert _is_forex_weekend(dt) is True
@test("Weekend filter: Friday 21:59 UTC is NOT weekend")
def _():
from signal_aggregator import _is_forex_weekend
dt = datetime(2026, 5, 15, 21, 59, tzinfo=timezone.utc) # Friday
assert dt.weekday() == 4
assert _is_forex_weekend(dt) is False
@test("Weekend filter: Saturday noon is weekend")
def _():
from signal_aggregator import _is_forex_weekend
dt = datetime(2026, 5, 16, 12, 0, tzinfo=timezone.utc) # Saturday
assert dt.weekday() == 5
assert _is_forex_weekend(dt) is True
@test("Weekend filter: Sunday 21:59 UTC is weekend")
def _():
from signal_aggregator import _is_forex_weekend
dt = datetime(2026, 5, 17, 21, 59, tzinfo=timezone.utc) # Sunday
assert dt.weekday() == 6
assert _is_forex_weekend(dt) is True
@test("Weekend filter: Sunday 22:00 UTC is NOT weekend")
def _():
from signal_aggregator import _is_forex_weekend
dt = datetime(2026, 5, 17, 22, 0, tzinfo=timezone.utc) # Sunday
assert dt.weekday() == 6
assert _is_forex_weekend(dt) is False
@test("Weekend filter: Monday is not weekend")
def _():
from signal_aggregator import _is_forex_weekend
dt = datetime(2026, 5, 18, 10, 0, tzinfo=timezone.utc) # Monday
assert dt.weekday() == 0
assert _is_forex_weekend(dt) is False
# ══════════════════════════════════════════════════════════════════════
# MODULE 13: Integration Edge Cases
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 13: Integration Edge Cases ═══")
@test("Integration: all indicators disabled produces valid report")
def _():
config = PlatformConfig()
config.technical.disabled_indicators = [
"ema_trend", "rsi", "bollinger", "macd", "adx",
"keltner", "roc", "candlestick", "divergence",
]
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=XAUUSD, dfs=dfs, use_news=False)
assert report is not None
assert report.signal_quality in ("A+", "A", "B", "C", "D", "F", "")
@test("Integration: data with 50% NaN still produces valid report")
def _():
df = make_ohlcv(n=300, seed=42)
mask = np.random.RandomState(99).choice([True, False], size=len(df), p=[0.5, 0.5])
df.loc[mask, "Close"] = np.nan
df.loc[mask, "High"] = np.nan
df.loc[mask, "Low"] = np.nan
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert report is not None
assert report.signal_quality in ("A+", "A", "B", "C", "D", "F", "")
@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)
xau_dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
xau_report = xau_agg.analyze_from_dataframes(symbol=XAUUSD, dfs=xau_dfs, use_news=False)
eur_config = eur_make_config(Timeframe.H1)
eur_agg = SignalAggregator(eur_config)
eur_dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
eur_report = eur_agg.analyze_from_dataframes(symbol=EURUSD, dfs=eur_dfs, use_news=False)
assert xau_report.display_name == "XAU/USD"
assert eur_report.display_name == "EUR/USD"
@test("Integration: missing confirmation TFs still produces valid report")
def _():
config = PlatformConfig()
agg = SignalAggregator(config)
df = make_ohlcv(n=300, seed=42)
dfs = {Timeframe.H1: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert report is not None
assert report.mtf_agreement is False
@test("Integration: extreme volatility data does not crash")
def _():
n = 300
dates = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=n, freq="h")
np.random.seed(42)
close = 100.0 + np.cumsum(np.random.randn(n) * 10.0)
close = np.maximum(close, 1.0)
df = pd.DataFrame({
"Open": close * 0.99, "High": close * 1.05,
"Low": close * 0.95, "Close": close,
"Volume": [5000.0]*n,
}, index=dates)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert report is not None
assert -100 <= report.final_score <= 100
@test("Integration: very short data (50 bars) produces valid report")
def _():
df = make_ohlcv(n=50, seed=42)
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {Timeframe.H1: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert report is not None
@test("Integration: multiple seeds produce deterministic results")
def _():
df = make_ohlcv(n=300, seed=42)
config = PlatformConfig()
agg1 = SignalAggregator(config)
agg2 = SignalAggregator(config)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
r1 = agg1.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
r2 = agg2.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert r1.final_score == r2.final_score
assert r1.final_signal == r2.final_signal
assert r1.signal_quality == r2.signal_quality
@test("Integration: session filter blocks outside hours correctly")
def _():
from pair_signals.eurusd import make_config as eur_make_config
config = eur_make_config(Timeframe.H1)
config.eur_strategy.session_hours = [8, 9, 10, 11, 12, 13, 14, 15, 16]
agg = SignalAggregator(config)
n = 300
end_time = pd.Timestamp("2026-05-18 03:00:00", tz="UTC")
dates = pd.date_range(end=end_time, periods=n, freq="h")
np.random.seed(42)
close = 1.08 + np.cumsum(np.random.randn(n) * 0.001 + 0.0002)
df = pd.DataFrame({
"Open": close - 0.0001, "High": close + 0.0005,
"Low": close - 0.0005, "Close": close, "Volume": [1000.0]*n,
}, index=dates)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
bar_hour = dates[-1].hour
if bar_hour not in config.eur_strategy.session_hours and report.signal_quality != "":
assert report.signal_quality == "F", \
f"Hour {bar_hour} should be blocked, got {report.signal_quality}"
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════