Harden Telegram bot, add EUR/GBP strategies, parallelize scans

Telegram reliability: rate-limited sends (1 msg/s), dashboard lock,
WebSocket JSON/price error handling, markdown escaping, level cache
TTL, cooldown cleanup, breach task tracking, graceful shutdown.

Telegram speed: parallelize 3-pair scans with asyncio.gather(),
increase thread pool 2→4, parallelize boot-up initial scan.

Strategy: add EUR/USD and GBP/USD pullback-buy strategies with
per-pair configs, optimize XAU TP to 0.9×ATR, block hours 8/9
for XAU and hour 17 for GBP.

Backtest: add --max-hours flag to evaluate.py for time-exit support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-13 21:07:49 -04:00
parent 1f3f69c21d
commit 235241f95a
4 changed files with 494 additions and 66 deletions
+278 -1
View File
@@ -144,7 +144,17 @@ class SignalAggregator:
and self.config.xau_strategy.enabled
and any("XAU STRATEGY" in r for r in report.bullish_reasons)
)
if not xau_active:
eur_active = (
report.display_name == "EUR/USD"
and self.config.eur_strategy.enabled
and any("EUR STRATEGY" in r for r in report.bullish_reasons)
)
gbp_active = (
report.display_name == "GBP/USD"
and self.config.gbp_strategy.enabled
and any("GBP STRATEGY" in r for r in report.bullish_reasons)
)
if not xau_active and not eur_active and not gbp_active:
self._apply_session_filter_live(report)
return report
@@ -354,6 +364,40 @@ class SignalAggregator:
)
report.trade_setup = self._generate_xau_trade_setup(report, dfs)
# ── 6d. EUR/USD pullback-buy strategy ─────────────────────
if report.display_name == "EUR/USD" and self.config.eur_strategy.enabled:
if self._check_eur_pullback_buy(report, dfs):
report.final_signal = SignalStrength.BUY
report.final_score = 40.0
report.final_confidence = 75.0
report.signal_quality = "A"
report.action_recommendation = (
"BUY — EUR pullback entry "
"(RSI pullback + not at resistance + M15/H1 above EMA200)"
)
report.bullish_reasons.insert(
0,
"EUR STRATEGY: M15 RSI pullback + BB mid-band + dual-TF EMA200 confirmation",
)
report.trade_setup = self._generate_eur_trade_setup(report, dfs)
# ── 6e. GBP/USD pullback-buy strategy ─────────────────────
if report.display_name == "GBP/USD" and self.config.gbp_strategy.enabled:
if self._check_gbp_pullback_buy(report, dfs):
report.final_signal = SignalStrength.BUY
report.final_score = 40.0
report.final_confidence = 75.0
report.signal_quality = "A"
report.action_recommendation = (
"BUY — GBP pullback entry "
"(RSI pullback + not at resistance + M15/H1 above EMA200)"
)
report.bullish_reasons.insert(
0,
"GBP STRATEGY: M15 RSI pullback + BB mid-band + dual-TF EMA200 confirmation",
)
report.trade_setup = self._generate_gbp_trade_setup(report, dfs)
# ── 7. Trade setup for actionable signals ──────────────────
if report.trade_setup is None and report.signal_quality in ("A+", "A", "B", "C"):
is_sell_direction = (
@@ -486,6 +530,239 @@ class SignalAggregator:
pip_value=pip_value,
)
def _check_eur_pullback_buy(
self, report: SignalReport, dfs: Dict[Timeframe, pd.DataFrame]
) -> bool:
"""Return True when EUR/USD pullback-buy conditions are met.
Conditions (all must be true):
1. Current bar hour not in blocked list
2. M15 RSI(14) in 30–55 (pullback, not overbought)
3. M15 BB position < 0.75 (not at resistance)
4. M15 price > EMA200 (uptrend)
5. H1 price > EMA200 (higher-TF confirmation)
6. Price within 1.5×ATR of EMA21 (not chasing)
"""
cfg = self.config.eur_strategy
try:
bar_hour = pd.Timestamp(report.timestamp).hour
if bar_hour in cfg.blocked_hours_utc:
return False
except Exception:
return False
m15_analysis = None
h1_analysis = None
if report.primary_analysis:
tf = report.primary_analysis.timeframe
if tf == Timeframe.M15.value:
m15_analysis = report.primary_analysis
elif tf == Timeframe.H1.value:
h1_analysis = report.primary_analysis
for ca in report.confirmation_analyses:
if ca.timeframe == Timeframe.M15.value:
m15_analysis = ca
elif ca.timeframe == Timeframe.H1.value:
h1_analysis = ca
if not m15_analysis or not h1_analysis:
return False
m15_rsi = m15_analysis.indicators.get("rsi")
if not m15_rsi or m15_rsi.value < cfg.rsi_lower or m15_rsi.value > cfg.rsi_upper:
return False
m15_bb = m15_analysis.indicators.get("bollinger")
if m15_bb and m15_bb.value > cfg.bb_max_position:
return False
m15_ema200 = m15_analysis.key_levels.get("ema_trend")
m15_df = dfs.get(Timeframe.M15)
if m15_df is None or m15_ema200 is None:
return False
m15_price = float(m15_df["Close"].iloc[-1])
if m15_price <= m15_ema200:
return False
h1_ema200 = h1_analysis.key_levels.get("ema_trend")
h1_df = dfs.get(Timeframe.H1)
if h1_ema200 is None:
return False
h1_price = float(h1_df["Close"].iloc[-1]) if h1_df is not None else report.current_price
if h1_price <= h1_ema200:
return False
m15_atr = m15_analysis.key_levels.get("atr", 0)
m15_ema21 = m15_analysis.key_levels.get("ema_medium")
if m15_atr > 0 and m15_ema21:
dist = abs(m15_price - m15_ema21) / m15_atr
if dist > cfg.chase_max_atr_from_ema21:
return False
return True
def _generate_eur_trade_setup(
self, report: SignalReport, dfs: Dict[Timeframe, pd.DataFrame]
) -> Optional[TradeSetup]:
"""Generate EUR/USD-specific trade setup with tight TP / wide SL."""
cfg = self.config.eur_strategy
m15_analysis = None
if report.primary_analysis and report.primary_analysis.timeframe == Timeframe.M15.value:
m15_analysis = report.primary_analysis
else:
for ca in report.confirmation_analyses:
if ca.timeframe == Timeframe.M15.value:
m15_analysis = ca
break
if not m15_analysis:
return None
atr = m15_analysis.key_levels.get("atr", 0)
if atr <= 0:
return None
price = report.current_price
pip_value = 0.0001
tp1 = price + cfg.tp_atr_multiplier * atr
sl = price - cfg.sl_atr_multiplier * atr
tp2 = price + cfg.tp_atr_multiplier * 2.0 * atr
sl_distance = price - sl
tp_distance = tp1 - price
rr = tp_distance / sl_distance if sl_distance > 0 else 0
return TradeSetup(
pair=report.symbol,
display_name=report.display_name,
direction="BUY",
entry_price=price,
stop_loss=round(sl, 5),
take_profit_1=round(tp1, 5),
take_profit_2=round(tp2 , 5),
risk_reward=round(rr, 2),
risk_pips=round(sl_distance / pip_value, 1),
reward_pips=round(tp_distance / pip_value, 1),
pip_value=pip_value,
)
def _check_gbp_pullback_buy(
self, report: SignalReport, dfs: Dict[Timeframe, pd.DataFrame]
) -> bool:
"""GBP/USD pullback-buy — same logic as EUR, different config."""
cfg = self.config.gbp_strategy
try:
bar_hour = pd.Timestamp(report.timestamp).hour
if bar_hour in cfg.blocked_hours_utc:
return False
except Exception:
return False
m15_analysis = None
h1_analysis = None
if report.primary_analysis:
tf = report.primary_analysis.timeframe
if tf == Timeframe.M15.value:
m15_analysis = report.primary_analysis
elif tf == Timeframe.H1.value:
h1_analysis = report.primary_analysis
for ca in report.confirmation_analyses:
if ca.timeframe == Timeframe.M15.value:
m15_analysis = ca
elif ca.timeframe == Timeframe.H1.value:
h1_analysis = ca
if not m15_analysis or not h1_analysis:
return False
m15_rsi = m15_analysis.indicators.get("rsi")
if not m15_rsi or m15_rsi.value < cfg.rsi_lower or m15_rsi.value > cfg.rsi_upper:
return False
m15_bb = m15_analysis.indicators.get("bollinger")
if m15_bb and (m15_bb.value < cfg.bb_min_position or m15_bb.value > cfg.bb_max_position):
return False
m15_ema200 = m15_analysis.key_levels.get("ema_trend")
m15_df = dfs.get(Timeframe.M15)
if m15_df is None or m15_ema200 is None:
return False
m15_price = float(m15_df["Close"].iloc[-1])
if m15_price <= m15_ema200:
return False
h1_ema200 = h1_analysis.key_levels.get("ema_trend")
h1_df = dfs.get(Timeframe.H1)
if h1_ema200 is None:
return False
h1_price = float(h1_df["Close"].iloc[-1]) if h1_df is not None else report.current_price
if h1_price <= h1_ema200:
return False
m15_atr = m15_analysis.key_levels.get("atr", 0)
m15_ema21 = m15_analysis.key_levels.get("ema_medium")
if m15_atr > 0 and m15_ema21:
dist = abs(m15_price - m15_ema21) / m15_atr
if dist > cfg.chase_max_atr_from_ema21:
return False
return True
def _generate_gbp_trade_setup(
self, report: SignalReport, dfs: Dict[Timeframe, pd.DataFrame]
) -> Optional[TradeSetup]:
"""Generate GBP/USD trade setup with tight TP / balanced SL."""
cfg = self.config.gbp_strategy
m15_analysis = None
if report.primary_analysis and report.primary_analysis.timeframe == Timeframe.M15.value:
m15_analysis = report.primary_analysis
else:
for ca in report.confirmation_analyses:
if ca.timeframe == Timeframe.M15.value:
m15_analysis = ca
break
if not m15_analysis:
return None
atr = m15_analysis.key_levels.get("atr", 0)
if atr <= 0:
return None
price = report.current_price
pip_value = 0.0001
tp1 = price + cfg.tp_atr_multiplier * atr
sl = price - cfg.sl_atr_multiplier * atr
tp2 = price + cfg.tp_atr_multiplier * 2.0 * atr
sl_distance = price - sl
tp_distance = tp1 - price
rr = tp_distance / sl_distance if sl_distance > 0 else 0
return TradeSetup(
pair=report.symbol,
display_name=report.display_name,
direction="BUY",
entry_price=price,
stop_loss=round(sl, 5),
take_profit_1=round(tp1, 5),
take_profit_2=round(tp2, 5),
risk_reward=round(rr, 2),
risk_pips=round(sl_distance / pip_value, 1),
reward_pips=round(tp_distance / pip_value, 1),
pip_value=pip_value,
)
def _check_mtf_agreement(self, report: SignalReport) -> bool:
signals = list(report.mtf_signals.values())
if len(signals) < 2: