From 02eedcc875260ccccab8d08ce2f2d77c6810dec9 Mon Sep 17 00:00:00 2001 From: harwinder Date: Mon, 11 May 2026 17:16:07 -0400 Subject: [PATCH] QA fixes: H4 timing bug, /status + /help commands, WS cold-start - Fix H4 boundary calculation using total minutes (hour*60+min) not just minute - Add /help command listing all bot commands - Add /status command showing WS connection, live prices, level cache, cooldowns - Run initial M15 scan on boot to populate level cache before WS starts - Update /start message to mention new commands --- telegram_bot.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/telegram_bot.py b/telegram_bot.py index 38cc340..36de48f 100755 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -361,7 +361,8 @@ _autoscan_tasks: list[asyncio.Task] = [] def _seconds_until_boundary(interval_minutes: int, buffer: int = 10) -> float: """Seconds until the next candle close + buffer.""" now = datetime.now(timezone.utc) - elapsed_minutes = now.minute % interval_minutes + total_minutes = now.hour * 60 + now.minute + elapsed_minutes = total_minutes % interval_minutes elapsed_seconds = elapsed_minutes * 60 + now.second + now.microsecond / 1e6 interval_seconds = interval_minutes * 60 remaining = interval_seconds - elapsed_seconds + buffer @@ -580,8 +581,10 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: text = ( "👋 *ForexBot2 — Signal Intelligence*\n\n" "Pick a pair and timeframe to scan.\n" - "Strong signals include a full trade setup with entry, SL and TP.\n\n" - f"Auto-scan: *{scanning}* — /autoscan to toggle" + "Signals include a full trade setup with entry, SL and TP.\n\n" + f"Auto-scan: *{scanning}* — /autoscan to toggle\n" + "/status — Bot & WebSocket status\n" + "/help — All commands" ) await update.message.reply_text( text, @@ -590,6 +593,66 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: ) +async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + text = ( + "📖 *ForexBot2 Commands*\n\n" + "/start — Show scan buttons (4 pairs × 4 TFs)\n" + "/autoscan — Toggle auto-scan on/off\n" + "/status — Bot status, WS connection, live prices\n" + "/help — This message\n\n" + "*Auto-scan* fires at candle close: M5/M15/H1/H4.\n" + "*WebSocket* monitors live prices and sends Level Break Alerts " + "when price crosses swing S/R, Bollinger bands, or EMA200." + ) + await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN) + + +async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + lines = ["📊 *ForexBot2 Status*\n"] + + if _autoscan_chats: + lines.append(f"✅ Auto-scan: *running* ({len(_autoscan_chats)} chat)") + tasks_alive = sum(1 for t in _autoscan_tasks if not t.done()) + lines.append(f" Candle loops: {tasks_alive}/{len(_AUTOSCAN_LOOPS)}") + else: + lines.append("⏹ Auto-scan: *stopped*") + + if _ws_task and not _ws_task.done(): + lines.append("📡 WebSocket: *connected*") + else: + lines.append("❌ WebSocket: *disconnected*") + + if _last_prices: + lines.append("\n*Live Prices:*") + for pair in SCAN_PAIRS: + price = _last_prices.get(pair) + display = PAIR_INFO.get(pair, {}).get("display", pair) + if price is None: + lines.append(f" {display}: waiting...") + elif price < 10: + lines.append(f" {display}: `{price:.5f}`") + else: + lines.append(f" {display}: `{price:.3f}`") + else: + lines.append("\n_No live prices yet (WS not streaming)_") + + cached_pairs = len(_level_cache) + cached_levels = sum(len(v) for v in _level_cache.values()) + if cached_levels: + lines.append(f"\n*Level Cache:* {cached_levels} levels across {cached_pairs} pairs") + else: + lines.append("\n*Level Cache:* empty — breach alerts start after first candle scan") + + active_cooldowns = sum( + 1 for v in _scan_cooldowns.values() + if time.time() - v < _WS_COOLDOWN + ) + if active_cooldowns: + lines.append(f"*Cooldowns:* {active_cooldowns} pair(s) in cooldown") + + await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.MARKDOWN) + + async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = update.effective_chat.id @@ -660,6 +723,18 @@ async def _post_init(app: Application) -> None: chat_id = int(CHAT_ID) _autoscan_chats.add(chat_id) + + logger.info("Running initial scan to populate level cache for WebSocket monitor...") + try: + init_reports = [] + for symbol in SCAN_PAIRS: + report = await run_signal(symbol, Timeframe.M15) + init_reports.append(report) + _update_level_cache(init_reports) + logger.info(f"Level cache populated: {sum(len(v) for v in _level_cache.values())} levels") + except Exception: + logger.exception("Initial scan failed — WS will wait for first candle scan") + _start_scheduler(app.bot) next_m5 = _seconds_until_boundary(5, buffer=10) @@ -702,6 +777,8 @@ def main() -> None: ) app.add_handler(CommandHandler("start", cmd_start)) + app.add_handler(CommandHandler("help", cmd_help)) + app.add_handler(CommandHandler("status", cmd_status)) app.add_handler(CommandHandler("autoscan", cmd_autoscan)) app.add_handler(CallbackQueryHandler(cb_scan, pattern="^scan_"))