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
This commit is contained in:
+156
-20
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user