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:
+16
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user