Compare commits
2 Commits
08d7c9ade1
...
de57c016d1
| Author | SHA1 | Date | |
|---|---|---|---|
| de57c016d1 | |||
| e6e09c587e |
@@ -169,22 +169,22 @@ class EurUsdStrategyConfig:
|
|||||||
weekend_filter: bool = True
|
weekend_filter: bool = True
|
||||||
allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4])
|
allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4])
|
||||||
min_atr_threshold: float = 0.0003
|
min_atr_threshold: float = 0.0003
|
||||||
sl_atr_multiplier: float = 1.5
|
sl_atr_multiplier: float = 0.5
|
||||||
tp_atr_multiplier: float = 1.0
|
tp_atr_multiplier: float = 1.5
|
||||||
|
|
||||||
sl_atr_floor: float = 1.2
|
sl_atr_floor: float = 1.2
|
||||||
min_rr_ratio: float = 0.5
|
min_rr_ratio: float = 1.0
|
||||||
mature_move_threshold: float = 35.0
|
mature_move_threshold: float = 35.0
|
||||||
tp_quality_scale: Dict[str, float] = field(default_factory=lambda: {
|
tp_quality_scale: Dict[str, float] = field(default_factory=lambda: {
|
||||||
"A+": 1.0, "A": 1.0, "B": 1.0, "C": 1.0,
|
"A+": 1.0, "A": 1.0, "B": 1.0, "C": 1.0,
|
||||||
})
|
})
|
||||||
blocked_regimes: List[str] = field(default_factory=lambda: ["ranging"])
|
blocked_regimes: List[str] = field(default_factory=list)
|
||||||
min_quality_grades: List[str] = field(default_factory=lambda: ["A+", "A", "B"])
|
min_quality_grades: List[str] = field(default_factory=lambda: ["A+", "A"])
|
||||||
max_exhaustion_tags: int = 99
|
max_exhaustion_tags: int = 99
|
||||||
block_near_ema200: bool = False
|
block_near_ema200: bool = False
|
||||||
session_hours: List[int] = field(default_factory=list)
|
session_hours: List[int] = field(default_factory=list)
|
||||||
min_entry_score: float = 38.0
|
min_entry_score: float = 30.0
|
||||||
min_entry_confidence: float = 60.0
|
min_entry_confidence: float = 50.0
|
||||||
block_rsi_extended: bool = True
|
block_rsi_extended: bool = True
|
||||||
be_trigger_r: float = 0.3
|
be_trigger_r: float = 0.3
|
||||||
be_buffer_r: float = 0.05
|
be_buffer_r: float = 0.05
|
||||||
|
|||||||
@@ -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)
|
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)
|
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 = 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
|
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:
|
if len(close) < n + 5:
|
||||||
return
|
return
|
||||||
|
|
||||||
roc_val = ((close.iloc[-1] - close.iloc[-n - 1]) / close.iloc[-n - 1]) * 100
|
denom1 = close.iloc[-n - 1]
|
||||||
roc_prev = ((close.iloc[-2] - close.iloc[-n - 2]) / close.iloc[-n - 2]) * 100
|
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):
|
if pd.isna(roc_val) or pd.isna(roc_prev):
|
||||||
return
|
return
|
||||||
@@ -1112,12 +1116,14 @@ class TechnicalEngine:
|
|||||||
for i in range(window, len(vals) - window):
|
for i in range(window, len(vals) - window):
|
||||||
if np.isnan(vals[i]):
|
if np.isnan(vals[i]):
|
||||||
continue
|
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 mode == "low":
|
||||||
if all(vals[i] <= vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])) and \
|
if all(vals[i] <= v for v in left_vals) and all(vals[i] <= v for v in right_vals):
|
||||||
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])):
|
|
||||||
points.append((i, vals[i]))
|
points.append((i, vals[i]))
|
||||||
else:
|
else:
|
||||||
if all(vals[i] >= vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])) and \
|
if all(vals[i] >= v for v in left_vals) and all(vals[i] >= v for v in right_vals):
|
||||||
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])):
|
|
||||||
points.append((i, vals[i]))
|
points.append((i, vals[i]))
|
||||||
return points
|
return points
|
||||||
|
|||||||
+3
-2
@@ -497,11 +497,12 @@ class NewsEngine:
|
|||||||
found = []
|
found = []
|
||||||
|
|
||||||
for currency, keywords in self.CURRENCY_KEYWORDS.items():
|
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)
|
found.append(currency)
|
||||||
continue
|
continue
|
||||||
for kw in keywords:
|
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)
|
found.append(currency)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
+31
-1
@@ -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(
|
def make_config(
|
||||||
primary_tf: Timeframe,
|
primary_tf: Timeframe,
|
||||||
@@ -53,6 +75,14 @@ def make_config(
|
|||||||
config.backtest.blocked_hours_utc = [22, 23, 0, 1, 2, 3]
|
config.backtest.blocked_hours_utc = [22, 23, 0, 1, 2, 3]
|
||||||
config.backtest.allowed_weekdays = [0, 1, 2, 3, 4]
|
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
|
return config
|
||||||
|
|||||||
+16
-11
@@ -252,11 +252,11 @@ class SignalAggregator:
|
|||||||
)
|
)
|
||||||
report.trade_setup = None
|
report.trade_setup = None
|
||||||
else:
|
else:
|
||||||
report.bullish_reasons.append(
|
reason = f"Regime: {regime_tf.value} ADX={adx_value:.1f} confirms trending market"
|
||||||
f"Regime: {regime_tf.value} ADX={adx_value:.1f} confirms trending market"
|
if report.final_score > 0:
|
||||||
) if report.final_score > 0 else report.bearish_reasons.append(
|
report.bullish_reasons.append(reason)
|
||||||
f"Regime: {regime_tf.value} ADX={adx_value:.1f} confirms trending market"
|
else:
|
||||||
)
|
report.bearish_reasons.append(reason)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _latest_adx(df: pd.DataFrame, period: int = 14) -> Optional[float]:
|
def _latest_adx(df: pd.DataFrame, period: int = 14) -> Optional[float]:
|
||||||
@@ -915,6 +915,8 @@ class SignalAggregator:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if pip_value <= 0:
|
||||||
|
return None
|
||||||
min_sl_pips = 8.0 if pip_value < 0.01 else 80.0
|
min_sl_pips = 8.0 if pip_value < 0.01 else 80.0
|
||||||
min_sl_price = min_sl_pips * pip_value
|
min_sl_price = min_sl_pips * pip_value
|
||||||
|
|
||||||
@@ -932,9 +934,11 @@ class SignalAggregator:
|
|||||||
eur = self.config.eur_strategy
|
eur = self.config.eur_strategy
|
||||||
min_rr = eur.min_rr_ratio
|
min_rr = eur.min_rr_ratio
|
||||||
mature_move = abs(report.final_score) > eur.mature_move_threshold
|
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
|
quality_scale = eur.tp_quality_scale.get(report.signal_quality, 1.0)
|
||||||
tp_swing_cap = 2.0 if mature_move else 2.5
|
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_atr_floor = eur.sl_atr_floor
|
||||||
|
sl_swing_buffer = eur.sl_atr_multiplier
|
||||||
else:
|
else:
|
||||||
min_rr = self.config.signals.min_rr_ratio
|
min_rr = self.config.signals.min_rr_ratio
|
||||||
mature_move = abs(report.final_score) > 30
|
mature_move = abs(report.final_score) > 30
|
||||||
@@ -942,6 +946,7 @@ class SignalAggregator:
|
|||||||
tp_swing_cap = 2.0 if mature_move else 2.5
|
tp_swing_cap = 2.0 if mature_move else 2.5
|
||||||
sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier
|
sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier
|
||||||
if report.display_name == "XAU/USD" else 1.5)
|
if report.display_name == "XAU/USD" else 1.5)
|
||||||
|
sl_swing_buffer = 1.0
|
||||||
|
|
||||||
if is_buy:
|
if is_buy:
|
||||||
swing_lows = sorted(
|
swing_lows = sorted(
|
||||||
@@ -949,9 +954,9 @@ class SignalAggregator:
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
if swing_lows:
|
if swing_lows:
|
||||||
sl_price = swing_lows[0] - atr * 1.0
|
sl_price = swing_lows[0] - atr * sl_swing_buffer
|
||||||
else:
|
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)
|
min_sl = max(atr * sl_atr_floor, min_sl_price)
|
||||||
if price - sl_price < min_sl:
|
if price - sl_price < min_sl:
|
||||||
sl_price = 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
|
v for k, v in levels.items() if k.startswith("swing_high_") and v > price
|
||||||
)
|
)
|
||||||
if swing_highs:
|
if swing_highs:
|
||||||
sl_price = swing_highs[0] + atr * 1.0
|
sl_price = swing_highs[0] + atr * sl_swing_buffer
|
||||||
else:
|
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)
|
min_sl = max(atr * sl_atr_floor, min_sl_price)
|
||||||
if sl_price - price < min_sl:
|
if sl_price - price < min_sl:
|
||||||
sl_price = price + min_sl
|
sl_price = price + min_sl
|
||||||
|
|||||||
+970
-4
@@ -1677,14 +1677,15 @@ def _():
|
|||||||
assert 6 not in cfg.allowed_weekdays # No Sunday
|
assert 6 not in cfg.allowed_weekdays # No Sunday
|
||||||
assert cfg.min_atr_threshold == 0.0003
|
assert cfg.min_atr_threshold == 0.0003
|
||||||
assert cfg.sl_atr_floor == 1.2
|
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.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["A"] == 1.0
|
assert cfg.tp_quality_scale["A"] == 1.0
|
||||||
assert cfg.tp_quality_scale["C"] == 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 "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")
|
@test("PlatformConfig has eur_strategy field")
|
||||||
@@ -1776,7 +1777,7 @@ def _():
|
|||||||
config.eur_strategy.blocked_regimes = []
|
config.eur_strategy.blocked_regimes = []
|
||||||
config.eur_strategy.min_quality_grades = []
|
config.eur_strategy.min_quality_grades = []
|
||||||
assert config.eur_strategy.sl_atr_floor == 1.2
|
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)
|
agg = SignalAggregator(config)
|
||||||
df = make_ohlcv(n=300, start_price=1.0800, trend=0.0003, seed=77)
|
df = make_ohlcv(n=300, start_price=1.0800, trend=0.0003, seed=77)
|
||||||
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
|
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
|
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
|
# SUMMARY
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user