Backtest tuning, data freshness guard, Telegram improvements

- Drop USD/JPY from scan pairs (zero signals across all timeframes)
- Disable divergence indicator (zero impact in 14-config lab test)
- Parallelize evaluate.py with ProcessPoolExecutor (16 cores)
- Add --pair CLI filter to evaluate.py for single-pair backtests
- Add data freshness guard: stale cache auto-invalidated, primary TF
  stale data suppresses signals to prevent trading on old data
- Add /cache command: shows fetch age and last bar per pair/TF
- Add /clearcache command: wipes in-memory cache for fresh API fetch
- Remove BB from WebSocket breach alerts (keep structural levels only)
- Show live prices only for WS-monitored pairs (EUR/USD, XAU/USD)
- Replace arrow emojis with text tags in indicator lines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-12 22:56:55 -04:00
parent 6fe1a3e0f0
commit 1e5047e20f
6 changed files with 1148 additions and 320 deletions
+156 -20
View File
@@ -110,9 +110,13 @@ class SignalAggregator:
timeframes.append(bt.regime_filter_timeframe)
dfs: Dict[Timeframe, pd.DataFrame] = {}
stale_tfs = []
for tf in timeframes:
try:
dfs[tf] = self.data_provider.fetch_ohlcv(symbol, tf)
df = self.data_provider.fetch_ohlcv(symbol, tf)
dfs[tf] = df
if not self.data_provider._is_data_fresh(df, symbol, tf):
stale_tfs.append(tf.value)
except Exception as e:
logger.warning(f"Fetch failed for {symbol} {tf.value}: {e}")
@@ -123,6 +127,14 @@ class SignalAggregator:
use_news=True,
)
if stale_tfs:
report.warnings.append(f"STALE DATA on {', '.join(stale_tfs)} — signal may be unreliable")
if self.config.primary_timeframe.value in stale_tfs:
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.trade_setup = None
report.warnings.append("Primary timeframe stale — signal suppressed")
if bt.regime_filter:
self._apply_regime_filter_live(report, dfs)
@@ -206,12 +218,12 @@ class SignalAggregator:
minus_dm = -low.diff()
plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0.0)
minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0.0)
atr_s = tr.ewm(alpha=1/period, min_periods=period).mean()
plus_di = 100 * (plus_dm.ewm(alpha=1/period, min_periods=period).mean() / atr_s)
minus_di = 100 * (minus_dm.ewm(alpha=1/period, min_periods=period).mean() / atr_s)
atr_s = tr.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
plus_di = 100 * (plus_dm.ewm(alpha=1/period, min_periods=period, adjust=False).mean() / atr_s)
minus_di = 100 * (minus_dm.ewm(alpha=1/period, min_periods=period, adjust=False).mean() / atr_s)
denom = (plus_di + minus_di).replace(0, float("nan"))
dx = 100 * (plus_di - minus_di).abs() / denom
adx = dx.ewm(alpha=1/period, min_periods=period).mean().dropna()
adx = dx.ewm(alpha=1/period, min_periods=period, adjust=False).mean().dropna()
if adx.empty:
return None
return float(adx.iloc[-1])
@@ -308,9 +320,32 @@ class SignalAggregator:
# ── 6. Final signal ────────────────────────────────────────
self._compute_final_signal(report)
# ── 6b. Session filter (works for both live and backtest) ─
if self.config.backtest.session_filter:
try:
bar_hour = pd.Timestamp(report.timestamp).hour
if bar_hour in self.config.backtest.blocked_hours_utc:
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.warnings.append(f"SESSION FILTER: {bar_hour:02}:00 UTC blocked")
except Exception:
pass
# ── 7. Trade setup for actionable signals ──────────────────
if report.signal_quality in ("A+", "A", "B", "C"):
report.trade_setup = self._generate_trade_setup(report, pip_value)
is_sell_direction = (
report.final_signal in (SignalStrength.SELL, SignalStrength.STRONG_SELL)
or (report.final_signal == SignalStrength.NEUTRAL and report.final_score < 0)
)
if is_sell_direction and self.config.backtest.sell_filter:
if report.display_name in self.config.backtest.sell_allowed_pairs:
report.trade_setup = self._generate_trade_setup(report, pip_value)
elif self._forex_sell_qualifies(report):
report.trade_setup = self._generate_trade_setup(report, pip_value)
else:
report.trade_setup = None
else:
report.trade_setup = self._generate_trade_setup(report, pip_value)
# ── 8. Reasons ─────────────────────────────────────────────
self._collect_reasons(report)
@@ -319,6 +354,11 @@ class SignalAggregator:
# ── Internals ──────────────────────────────────────────────────
def _forex_sell_qualifies(self, report: SignalReport) -> bool:
"""Always returns False — forex SELL is structurally unprofitable.
0/4 trades won even under best conditions (trending_down + MTF + ADX>=25 + RSI<=50)."""
return False
def _check_mtf_agreement(self, report: SignalReport) -> bool:
signals = list(report.mtf_signals.values())
if len(signals) < 2:
@@ -398,16 +438,90 @@ class SignalAggregator:
)
confidence -= 10
atr = primary.key_levels.get("atr", 0)
if atr > 0 and score != 0:
is_buy_signal = score > 0
if is_buy_signal:
swing_lows = sorted(
v for k, v in primary.key_levels.items()
if k.startswith("swing_low_")
)
if swing_lows and report.current_price - swing_lows[0] > 2.5 * atr:
confidence -= 15
report.warnings.append("TREND EXHAUSTION — price extended >2.5 ATR past swing low")
else:
swing_highs = sorted(
(v for k, v in primary.key_levels.items()
if k.startswith("swing_high_")),
reverse=True,
)
if swing_highs and swing_highs[0] - report.current_price > 2.5 * atr:
confidence -= 15
report.warnings.append("TREND EXHAUSTION — price extended >2.5 ATR past swing high")
ema_medium = primary.key_levels.get("ema_medium")
if atr > 0 and ema_medium and report.market_regime.startswith("trending"):
pullback_dist = abs(report.current_price - ema_medium) / atr
if pullback_dist > 2.0:
confidence -= 20
report.warnings.append(
f"NO PULLBACK — price {pullback_dist:.1f} ATR from EMA21, chasing risk"
)
elif pullback_dist > 1.5:
confidence -= 10
report.warnings.append(
f"Weak pullback — price {pullback_dist:.1f} ATR from EMA21"
)
if report.market_regime.startswith("trending"):
rsi_ind = primary.indicators.get("rsi")
bb_ind = primary.indicators.get("bollinger")
if rsi_ind:
rsi_val = rsi_ind.value
if report.market_regime in ("trending_up", "trending") and score > 0:
if rsi_val > 65:
confidence -= 15
report.warnings.append(
f"Extended entry — RSI {rsi_val:.0f} in uptrend (prefer pullback to 40-55)"
)
elif 38 <= rsi_val <= 55:
confidence += 5
elif report.market_regime == "trending_down" and score < 0:
if rsi_val < 35:
confidence -= 15
report.warnings.append(
f"Extended entry — RSI {rsi_val:.0f} in downtrend (prefer bounce to 45-62)"
)
elif 45 <= rsi_val <= 62:
confidence += 5
if bb_ind:
bb_pos = bb_ind.value
if report.market_regime in ("trending_up", "trending") and score > 0:
if bb_pos > 0.85:
confidence -= 10
elif bb_pos < 0.4:
confidence += 5
elif report.market_regime == "trending_down" and score < 0:
if bb_pos < 0.15:
confidence -= 10
elif bb_pos > 0.6:
confidence += 5
confidence = max(0.0, min(95.0, confidence))
# Final signal
if score >= 35 and confidence >= 60:
if score > 38:
confidence -= 20
report.warnings.append("EXHAUSTION: score > 38 — trend overextended")
if score >= 40 and confidence >= 60:
report.final_signal = SignalStrength.STRONG_BUY
elif score >= 12 and confidence >= 45:
elif score >= 30 and confidence >= 55:
report.final_signal = SignalStrength.BUY
elif score <= -35 and confidence >= 60:
elif score <= -40 and confidence >= 60:
report.final_signal = SignalStrength.STRONG_SELL
elif score <= -12 and confidence >= 45:
elif score <= -30 and confidence >= 55:
report.final_signal = SignalStrength.SELL
else:
report.final_signal = SignalStrength.NEUTRAL
@@ -415,17 +529,29 @@ class SignalAggregator:
report.final_score = score
report.final_confidence = confidence
# SELL suppression (pair-aware)
sell_suppressed = False
if self.config.backtest.sell_filter and report.final_signal in (
SignalStrength.SELL, SignalStrength.STRONG_SELL
):
if report.display_name not in self.config.backtest.sell_allowed_pairs:
report.final_signal = SignalStrength.NEUTRAL
report.warnings.append(f"SELL SUPPRESSED for {report.display_name}")
sell_suppressed = True
# Quality grade
abs_score = abs(score)
if abs_score >= 45 and confidence >= 70 and report.mtf_agreement:
if sell_suppressed:
report.signal_quality = "F"
elif abs_score >= 45 and confidence >= 70 and report.mtf_agreement:
report.signal_quality = "A+"
elif abs_score >= 30 and confidence >= 60 and report.mtf_agreement:
elif abs_score >= 38 and confidence >= 60 and report.mtf_agreement:
report.signal_quality = "A"
elif abs_score >= 20 and confidence >= 50:
elif abs_score >= 33 and confidence >= 55:
report.signal_quality = "B"
elif abs_score >= 12 and confidence >= 40:
elif abs_score >= 30 and confidence >= 50:
report.signal_quality = "C"
elif abs_score >= 8:
elif abs_score >= 15:
report.signal_quality = "D"
else:
report.signal_quality = "F"
@@ -450,7 +576,15 @@ class SignalAggregator:
if atr == 0:
return None
is_buy = report.final_signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY)
min_sl_pips = 8.0 if pip_value < 0.01 else 80.0
min_sl_price = min_sl_pips * pip_value
if report.final_signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY):
is_buy = True
elif report.final_signal in (SignalStrength.SELL, SignalStrength.STRONG_SELL):
is_buy = False
else:
is_buy = report.final_score >= 0
levels = report.primary_analysis.key_levels
min_rr = self.config.signals.min_rr_ratio
@@ -467,8 +601,9 @@ class SignalAggregator:
sl_price = swing_lows[0] - atr * 1.0
else:
sl_price = price - atr * 2.0
if price - sl_price < atr * 1.5:
sl_price = price - atr * 1.5
min_sl = max(atr * 1.5, min_sl_price)
if price - sl_price < min_sl:
sl_price = price - min_sl
sl_distance = price - sl_price
@@ -496,8 +631,9 @@ class SignalAggregator:
sl_price = swing_highs[0] + atr * 1.0
else:
sl_price = price + atr * 2.0
if sl_price - price < atr * 1.5:
sl_price = price + atr * 1.5
min_sl = max(atr * 1.5, min_sl_price)
if sl_price - price < min_sl:
sl_price = price + min_sl
sl_distance = sl_price - price