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>
This commit is contained in:
harwinder
2026-05-18 17:25:52 -04:00
parent 08d7c9ade1
commit e6e09c587e
4 changed files with 1002 additions and 24 deletions
+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