diff --git a/config.py b/config.py index b0fd579..cc3cfaa 100755 --- a/config.py +++ b/config.py @@ -37,9 +37,11 @@ class MarketSession(Enum): # ── Supported pairs ────────────────────────────────────────────── XAUUSD = "XAU/USD" +EURUSD = "EUR/USD" PAIR_INFO = { XAUUSD: {"display": "XAU/USD", "pip_value": 0.10, "is_jpy_pair": False}, + EURUSD: {"display": "EUR/USD", "pip_value": 0.0001, "is_jpy_pair": False}, } @@ -123,7 +125,7 @@ class SignalConfig: @dataclass class NewsConfig: """Economic calendar & sentiment.""" - tracked_currencies: List[str] = field(default_factory=lambda: ["USD", "XAU"]) + tracked_currencies: List[str] = field(default_factory=lambda: ["USD", "XAU", "EUR"]) min_impact: str = "medium" pre_event_buffer_minutes: int = 30 post_event_buffer_minutes: int = 15 @@ -159,6 +161,18 @@ class XauStrategyConfig: sell_min_quality: List[str] = field(default_factory=lambda: ["A+", "A", "B"]) +@dataclass +class EurUsdStrategyConfig: + """EUR/USD strategy config — separate from XAU for independent tuning.""" + enabled: bool = True + blocked_hours_utc: List[int] = field(default_factory=lambda: [22, 23, 0, 1, 2, 3]) + weekend_filter: bool = True + allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4]) + min_atr_threshold: float = 0.0003 + sl_atr_multiplier: float = 1.5 + tp_atr_multiplier: float = 1.0 + + @dataclass class FilterConfig: """Regime and session filters for live signals.""" @@ -182,6 +196,7 @@ class PlatformConfig: news: NewsConfig = field(default_factory=NewsConfig) backtest: FilterConfig = field(default_factory=FilterConfig) xau_strategy: XauStrategyConfig = field(default_factory=XauStrategyConfig) + eur_strategy: EurUsdStrategyConfig = field(default_factory=EurUsdStrategyConfig) symbol: str = XAUUSD display_name: str = "XAU/USD" diff --git a/news_engine.py b/news_engine.py index 1d284f2..9460937 100755 --- a/news_engine.py +++ b/news_engine.py @@ -541,7 +541,8 @@ class NewsEngine: @staticmethod def _get_affected_pairs(currency: str) -> List[str]: _MAP = { - "USD": ["XAU/USD"], + "USD": ["XAU/USD", "EUR/USD"], "XAU": ["XAU/USD"], + "EUR": ["EUR/USD"], } return _MAP.get(currency, []) diff --git a/pair_signals/__init__.py b/pair_signals/__init__.py new file mode 100644 index 0000000..a60436c --- /dev/null +++ b/pair_signals/__init__.py @@ -0,0 +1,52 @@ +""" +Per-pair signal producers. + +Each pair module exposes: + make_config(primary_tf, confirmation_tfs) -> PlatformConfig + +To add a new pair: create pair_signals/.py with make_config(), +then register it in this file. +""" + +from typing import Optional, List, Dict, Callable + +from config import PlatformConfig, Timeframe +from signal_aggregator import SignalAggregator, SignalReport +from data_provider import ForexDataProvider + +PAIR_PRODUCERS: Dict[str, Callable] = {} + + +def register_pair(symbol: str, config_factory: Callable): + PAIR_PRODUCERS[symbol] = config_factory + + +def get_supported_pairs() -> List[str]: + return list(PAIR_PRODUCERS.keys()) + + +def produce_signal( + symbol: str, + primary_tf: Timeframe, + data_provider: ForexDataProvider, + confirmation_tfs: Optional[List[Timeframe]] = None, +) -> SignalReport: + config_factory = PAIR_PRODUCERS.get(symbol) + if config_factory is None: + raise ValueError( + f"No signal producer registered for {symbol}. " + f"Available: {list(PAIR_PRODUCERS.keys())}" + ) + + config = config_factory(primary_tf, confirmation_tfs) + aggregator = SignalAggregator(config) + aggregator.data_provider = data_provider + return aggregator.analyze_pair(symbol=symbol) + + +from pair_signals import xauusd as _xau_mod +from pair_signals import eurusd as _eur_mod +from config import XAUUSD, EURUSD + +register_pair(XAUUSD, _xau_mod.make_config) +register_pair(EURUSD, _eur_mod.make_config) diff --git a/pair_signals/eurusd.py b/pair_signals/eurusd.py new file mode 100644 index 0000000..cfb0827 --- /dev/null +++ b/pair_signals/eurusd.py @@ -0,0 +1,58 @@ +"""EUR/USD signal producer config.""" + +from typing import Optional, List +from config import PlatformConfig, Timeframe, EURUSD + +_CONFIRMATIONS = { + Timeframe.M5: [Timeframe.M15, Timeframe.H1], + Timeframe.M15: [Timeframe.M5, Timeframe.H1], + Timeframe.H1: [Timeframe.M15, Timeframe.H4], + Timeframe.H4: [Timeframe.H1, Timeframe.D1], +} + +_TRENDING = { + "ema_trend": 2.0, "rsi": 1.0, "macd": 2.0, + "bollinger": 0.5, "keltner": 1.0, "roc": 1.5, + "candlestick": 0.5, "divergence": 2.0, "adx": 1.0, +} + +EUR_REGIME_WEIGHTS = { + "trending_up": _TRENDING, + "trending_down": _TRENDING, + "trending": _TRENDING, + "ranging": { + "ema_trend": 0.3, "rsi": 2.0, "macd": 1.0, + "bollinger": 2.0, "keltner": 1.5, "roc": 0.5, + "candlestick": 0.8, "divergence": 2.5, "adx": 0.5, + }, + "transitioning": { + "ema_trend": 1.5, "rsi": 1.5, "macd": 2.0, + "bollinger": 1.0, "keltner": 1.5, "roc": 2.0, + "candlestick": 0.8, "divergence": 2.0, "adx": 1.0, + }, +} + + +def make_config( + primary_tf: Timeframe, + confirmation_tfs: Optional[List[Timeframe]] = None, +) -> PlatformConfig: + config = PlatformConfig() + config.symbol = EURUSD + config.display_name = "EUR/USD" + config.pip_value = 0.0001 + config.primary_timeframe = primary_tf + config.confirmation_timeframes = ( + confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4]) + ) + + config.xau_strategy.enabled = False + + config.backtest.session_start_hour = 7 + config.backtest.session_end_hour = 20 + config.backtest.blocked_hours_utc = [22, 23, 0, 1, 2, 3] + config.backtest.allowed_weekdays = [0, 1, 2, 3, 4] + + config.technical.regime_weights_override = EUR_REGIME_WEIGHTS + + return config diff --git a/pair_signals/xauusd.py b/pair_signals/xauusd.py new file mode 100644 index 0000000..07662df --- /dev/null +++ b/pair_signals/xauusd.py @@ -0,0 +1,23 @@ +"""XAU/USD signal producer config.""" + +from typing import Optional, List +from config import PlatformConfig, Timeframe + +_CONFIRMATIONS = { + Timeframe.M5: [Timeframe.M15, Timeframe.H1], + Timeframe.M15: [Timeframe.M5, Timeframe.H1], + Timeframe.H1: [Timeframe.M15, Timeframe.H4], + Timeframe.H4: [Timeframe.H1, Timeframe.D1], +} + + +def make_config( + primary_tf: Timeframe, + confirmation_tfs: Optional[List[Timeframe]] = None, +) -> PlatformConfig: + config = PlatformConfig() + config.primary_timeframe = primary_tf + config.confirmation_timeframes = ( + confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4]) + ) + return config diff --git a/signal_aggregator.py b/signal_aggregator.py index 07cdf9f..ec43398 100755 --- a/signal_aggregator.py +++ b/signal_aggregator.py @@ -417,6 +417,7 @@ class SignalAggregator: or (report.final_signal == SignalStrength.NEUTRAL and report.final_score < 0) ) if (self.config.xau_strategy.enabled + and report.display_name == "XAU/USD" and xau_is_sell_dir and report.signal_quality not in self.config.xau_strategy.sell_min_quality): report.warnings.append( @@ -426,7 +427,7 @@ class SignalAggregator: report.trade_setup = None # ── 7. Trade setup for actionable signals ────────────────── - if report.trade_setup is None and report.signal_quality in ("A+", "A"): + if report.trade_setup is None and report.signal_quality in ("A+", "A") and report.display_name == "XAU/USD": atr = (report.primary_analysis.key_levels.get("atr", 0) if report.primary_analysis else 0) if atr > 0: diff --git a/test_all.py b/test_all.py index 14f7f0b..acd412e 100644 --- a/test_all.py +++ b/test_all.py @@ -88,8 +88,8 @@ print("\n═══ Module 1: config.py ═══") from config import ( Timeframe, SignalStrength, MarketSession, PlatformConfig, TechnicalConfig, SignalConfig, NewsConfig, - FilterConfig, XauStrategyConfig, - XAUUSD, PAIR_INFO, + FilterConfig, XauStrategyConfig, EurUsdStrategyConfig, + XAUUSD, EURUSD, PAIR_INFO, ) @@ -1313,12 +1313,12 @@ def _(): # ══════════════════════════════════════════════════════════════════════ -# MODULE 7: telegram_bot.py +# MODULE 7: testing_telegram_bot.py # ══════════════════════════════════════════════════════════════════════ -print("\n═══ Module 7: telegram_bot.py ═══") +print("\n═══ Module 7: testing_telegram_bot.py ═══") -from telegram_bot import ( +from testing_telegram_bot import ( _escape_md, _signal_label, _mtf_line, format_signal_message, _format_scan_summary, _seconds_until_boundary, _check_level_breach, @@ -1491,7 +1491,7 @@ def _(): @test("SCAN_BUTTONS and SCAN_LOOKUP consistency") def _(): - assert len(SCAN_BUTTONS) == 5 # 4 TFs + "Scan All" + assert len(SCAN_BUTTONS) == 10 # 5 per pair (4 TFs + All) × 2 pairs for btn in SCAN_BUTTONS: assert btn["cb"] in SCAN_LOOKUP assert "symbol" in btn @@ -1531,12 +1531,12 @@ def _(): @test("_format_stats_dashboard returns waiting message when empty") def _(): - import telegram_bot - old_stats = telegram_bot._signal_stats.copy() - telegram_bot._signal_stats.clear() + import testing_telegram_bot + old_stats = testing_telegram_bot._signal_stats.copy() + testing_telegram_bot._signal_stats.clear() text = _format_stats_dashboard() assert "Waiting" in text or "waiting" in text.lower() - telegram_bot._signal_stats.update(old_stats) + testing_telegram_bot._signal_stats.update(old_stats) # ══════════════════════════════════════════════════════════════════════ @@ -1653,6 +1653,111 @@ def _(): assert r1.market_regime == r2.market_regime +# ══════════════════════════════════════════════════════════════════════ +# MODULE 9: EUR/USD pair support +# ══════════════════════════════════════════════════════════════════════ + +print("\n═══ Module 9: EUR/USD pair support ═══") + + +@test("PAIR_INFO includes EUR/USD with correct pip_value") +def _(): + assert EURUSD in PAIR_INFO + assert PAIR_INFO[EURUSD]["pip_value"] == 0.0001 + assert PAIR_INFO[EURUSD]["is_jpy_pair"] is False + assert PAIR_INFO[EURUSD]["display"] == "EUR/USD" + + +@test("EurUsdStrategyConfig defaults are sensible") +def _(): + cfg = EurUsdStrategyConfig() + assert cfg.enabled is True + assert 0 in cfg.blocked_hours_utc + assert 5 not in cfg.allowed_weekdays # No Saturday + assert 6 not in cfg.allowed_weekdays # No Sunday + assert cfg.min_atr_threshold == 0.0003 + + +@test("PlatformConfig has eur_strategy field") +def _(): + config = PlatformConfig() + assert hasattr(config, "eur_strategy") + assert isinstance(config.eur_strategy, EurUsdStrategyConfig) + + +@test("EUR/USD config factory produces correct config") +def _(): + from pair_signals.eurusd import make_config + config = make_config(Timeframe.H1) + assert config.symbol == EURUSD + assert config.display_name == "EUR/USD" + assert config.pip_value == 0.0001 + assert config.xau_strategy.enabled is False + assert config.technical.regime_weights_override + assert "trending" in config.technical.regime_weights_override + assert "ranging" in config.technical.regime_weights_override + + +@test("XAU/USD config factory unchanged") +def _(): + from pair_signals.xauusd import make_config + config = make_config(Timeframe.H1) + assert config.symbol == XAUUSD + assert config.display_name == "XAU/USD" + assert config.pip_value == 0.10 + assert config.xau_strategy.enabled is True + + +@test("pair_signals registry includes both pairs") +def _(): + from pair_signals import get_supported_pairs + pairs = get_supported_pairs() + assert XAUUSD in pairs + assert EURUSD in pairs + + +@test("EUR/USD signal analysis produces valid report (mocked data)") +def _(): + from pair_signals.eurusd import make_config + from signal_aggregator import SignalAggregator + config = make_config(Timeframe.H1) + config.backtest.session_filter = False + agg = SignalAggregator(config) + df = make_ohlcv(n=300, start_price=1.0800, trend=0.00005) + dfs = { + Timeframe.H1: df, + Timeframe.M15: df, + Timeframe.H4: df, + } + report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False) + assert report.display_name == "EUR/USD" + assert report.signal_quality in ("A+", "A", "B", "C", "D", "F") + assert not any("SELL FILTER" in w for w in report.warnings) + + +@test("XAU sell gate does not apply to EUR/USD") +def _(): + from pair_signals.eurusd import make_config + from signal_aggregator import SignalAggregator + config = make_config(Timeframe.H1) + config.backtest.session_filter = False + agg = SignalAggregator(config) + df = make_ohlcv(n=300, start_price=1.0800, trend=-0.0005, seed=99) + dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df} + report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False) + sell_filter_warnings = [w for w in report.warnings if "SELL FILTER" in w] + assert len(sell_filter_warnings) == 0 + + +@test("NewsEngine._get_affected_pairs includes EUR/USD for EUR and USD events") +def _(): + from news_engine import NewsEngine + assert "EUR/USD" in NewsEngine._get_affected_pairs("EUR") + assert "EUR/USD" in NewsEngine._get_affected_pairs("USD") + assert "XAU/USD" in NewsEngine._get_affected_pairs("USD") + assert "EUR/USD" not in NewsEngine._get_affected_pairs("GBP") + + # ══════════════════════════════════════════════════════════════════════ # SUMMARY # ══════════════════════════════════════════════════════════════════════ diff --git a/telegram_bot.py b/testing_telegram_bot.py similarity index 94% rename from telegram_bot.py rename to testing_telegram_bot.py index 2bdfc97..85dd68b 100755 --- a/telegram_bot.py +++ b/testing_telegram_bot.py @@ -5,7 +5,7 @@ ForexBot2 — Telegram Bot Async Telegram interface with candle-boundary auto-scanner and real-time WebSocket level-breach monitor. -Manual scan: XAU/USD × 3 timeframes (M15/H1/H4) via inline buttons. +Manual scan: XAU/USD + EUR/USD via inline buttons. Auto-scan: Starts automatically on boot if TELEGRAM_CHAT_ID is set. /autoscan toggles for additional chats or to stop. M5 fires at :00/:05/:10/… + 10s buffer (data only, no signals). @@ -19,8 +19,8 @@ WebSocket: Streams live ticks from TwelveData for XAU/USD. 5-minute cooldown prevents spam. Usage: - python telegram_bot.py - TELEGRAM_TOKEN= TELEGRAM_CHAT_ID= python telegram_bot.py + python testing_telegram_bot.py + TELEGRAM_TOKEN= TELEGRAM_CHAT_ID= python testing_telegram_bot.py """ import asyncio @@ -48,10 +48,11 @@ from telegram.ext import ( from config import ( PlatformConfig, SignalStrength, Timeframe, - XAUUSD, PAIR_INFO, + XAUUSD, EURUSD, PAIR_INFO, ) from data_provider import ForexDataProvider from signal_aggregator import SignalAggregator, SignalReport +from pair_signals import produce_signal # ── Force UTF-8 on Windows ──────────────────────────────────────────── if hasattr(sys.stdout, "reconfigure"): @@ -66,12 +67,12 @@ logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) logger = logging.getLogger("telegram_bot") -TOKEN = os.environ.get("TELEGRAM_TOKEN", "8228251480:AAFW2OAyQ2idMicql2t_CIXk1r9-qfRTfNE") +TOKEN = os.environ.get("TELEGRAM_TOKEN", "8882954582:AAEOi2MzoMbJjozDMiRtFBHc2muix_MtaQQ") CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022") # ── Pairs & buttons ─────────────────────────────────────────────────── -SCAN_PAIRS = [XAUUSD] +SCAN_PAIRS = [XAUUSD, EURUSD] _WS_TO_PAIR = { "XAU/USD": XAUUSD, @@ -91,11 +92,16 @@ _signal_stats: Dict[str, Dict[str, SignalReport]] = {} _stats_updated: Optional[float] = None SCAN_BUTTONS = [ - {"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "5 Min"}, - {"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "15 Min"}, - {"cb": "scan_xauusd_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "1 Hour"}, - {"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "4 Hour"}, - {"cb": "scan_xauusd_all", "symbol": XAUUSD, "tf": None, "label": "Scan All TFs"}, + {"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "XAU 5m"}, + {"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "XAU 15m"}, + {"cb": "scan_xauusd_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "XAU 1h"}, + {"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "XAU 4h"}, + {"cb": "scan_xauusd_all", "symbol": XAUUSD, "tf": None, "label": "XAU All"}, + {"cb": "scan_eurusd_m5", "symbol": EURUSD, "tf": Timeframe.M5, "label": "EUR 5m"}, + {"cb": "scan_eurusd_m15", "symbol": EURUSD, "tf": Timeframe.M15, "label": "EUR 15m"}, + {"cb": "scan_eurusd_h1", "symbol": EURUSD, "tf": Timeframe.H1, "label": "EUR 1h"}, + {"cb": "scan_eurusd_h4", "symbol": EURUSD, "tf": Timeframe.H4, "label": "EUR 4h"}, + {"cb": "scan_eurusd_all", "symbol": EURUSD, "tf": None, "label": "EUR All"}, ] SCAN_LOOKUP = {b["cb"]: b for b in SCAN_BUTTONS} @@ -408,14 +414,14 @@ async def _dashboard_loop(bot) -> None: # ── Keyboard helper ─────────────────────────────────────────────────── def scan_keyboard() -> InlineKeyboardMarkup: - tf_buttons = [b for b in SCAN_BUTTONS if b["tf"] is not None] - all_button = next((b for b in SCAN_BUTTONS if b["tf"] is None), None) - rows = [[ - InlineKeyboardButton(b["label"], callback_data=b["cb"]) - for b in tf_buttons - ]] - if all_button: - rows.append([InlineKeyboardButton(all_button["label"], callback_data=all_button["cb"])]) + rows = [] + for pair in SCAN_PAIRS: + pair_btns = [b for b in SCAN_BUTTONS if b["symbol"] == pair] + row = [ + InlineKeyboardButton(b["label"], callback_data=b["cb"]) + for b in pair_btns + ] + rows.append(row) return InlineKeyboardMarkup(rows) @@ -430,12 +436,12 @@ def _run_signal_sync( confirmation_tfs = TF_CONFIRMATIONS.get( timeframe, [Timeframe.M15, Timeframe.H4] ) - config = PlatformConfig() - config.primary_timeframe = timeframe - config.confirmation_timeframes = confirmation_tfs - aggregator = SignalAggregator(config) - aggregator.data_provider = _get_shared_dp() - return aggregator.analyze_pair(symbol=symbol) + return produce_signal( + symbol=symbol, + primary_tf=timeframe, + data_provider=_get_shared_dp(), + confirmation_tfs=confirmation_tfs, + ) async def run_signal( @@ -709,8 +715,8 @@ def _stop_scheduler() -> None: async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: scanning = "running" if _autoscan_chats else "off" text = ( - "👋 *ForexBot2 — XAU/USD Signal Intelligence*\n\n" - "Pick a timeframe to scan gold.\n" + "👋 *ForexBot2 — Signal Intelligence*\n\n" + "Pick a pair and timeframe to scan.\n" "Signals include a full trade setup with entry, SL and TP.\n\n" f"Auto-scan: *{scanning}* — /autoscan to toggle\n" "/scan — Quick scan buttons\n" @@ -728,7 +734,7 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: text = ( "📖 *ForexBot2 Commands*\n\n" "/start — Welcome & scan buttons\n" - "/scan — Quick XAU/USD scan buttons\n" + "/scan — Quick scan buttons (XAU/USD + EUR/USD)\n" "/autoscan — Toggle auto-scan on/off\n" "/status — Bot status, WS connection, live prices\n" "/cache — Show data cache age per pair/TF\n" @@ -851,7 +857,7 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No next_h4 = _seconds_until_boundary(240, buffer=10) await update.message.reply_text( "✅ *Auto-scan enabled*\n\n" - f"Scanning XAU/USD on M5/M15/H1/H4 at candle close.\n" + f"Scanning XAU/USD + EUR/USD on M5/M15/H1/H4 at candle close.\n" f"📊 Live dashboard refreshes every {_DASHBOARD_REFRESH_INTERVAL}s.\n" f"📡 Live price monitoring active.\n" f"Next: M5 {next_m5 / 60:.0f}m | M15 {next_m15 / 60:.0f}m | " @@ -863,7 +869,7 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No async def cmd_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text( - "🔍 *XAU/USD — Pick a timeframe*", + "🔍 *Pick a pair & timeframe*", parse_mode=ParseMode.MARKDOWN, reply_markup=scan_keyboard(), ) @@ -878,11 +884,12 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: return if btn["tf"] is None: - await _cb_scan_all(query) + await _cb_scan_all(query, symbol=btn["symbol"]) return + display = PAIR_INFO.get(btn["symbol"], {}).get("display", btn["symbol"]) await query.edit_message_text( - f"⏳ Scanning XAU/USD {btn['label']}…", + f"⏳ Scanning {display} {btn['label']}…", parse_mode=ParseMode.MARKDOWN, ) @@ -900,14 +907,15 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: ) -async def _cb_scan_all(query) -> None: +async def _cb_scan_all(query, symbol: str = XAUUSD) -> None: + display = PAIR_INFO.get(symbol, {}).get("display", symbol) await query.edit_message_text( - "⏳ Scanning XAU/USD across M5 / M15 / H1 / H4…", + f"⏳ Scanning {display} across M5 / M15 / H1 / H4…", parse_mode=ParseMode.MARKDOWN, ) tfs = [Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4] - tasks = [run_signal(XAUUSD, tf) for tf in tfs] + tasks = [run_signal(symbol, tf) for tf in tfs] results = await asyncio.gather(*tasks, return_exceptions=True) reports = [] @@ -923,7 +931,7 @@ async def _cb_scan_all(query) -> None: _update_level_cache(reports) lines = [ - "📊 *XAU/USD — All Timeframes*", + f"📊 *{display} — All Timeframes*", f"🕐 {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", "", ] @@ -967,7 +975,7 @@ async def _post_init(app: Application) -> None: """Register command menu and start scheduler on boot.""" await app.bot.set_my_commands([ ("start", "Welcome & scan buttons"), - ("scan", "Quick XAU/USD scan buttons"), + ("scan", "Quick scan buttons (XAU/USD + EUR/USD)"), ("autoscan", "Toggle auto-scan on/off"), ("status", "Bot status, WS connection, live prices"), ("cache", "Show data cache age per pair/TF"), @@ -1016,7 +1024,7 @@ async def _post_init(app: Application) -> None: await app.bot.send_message( chat_id, f"🤖 *ForexBot2 started*\n\n" - f"Auto-scanning XAU/USD on M5/M15/H1/H4.\n" + f"Auto-scanning XAU/USD + EUR/USD on M5/M15/H1/H4.\n" f"📊 Live dashboard refreshes every {_DASHBOARD_REFRESH_INTERVAL}s.\n" f"📡 Live price monitoring active.\n" f"Next: M5 {next_m5 / 60:.0f}m | M15 {next_m15 / 60:.0f}m | "