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
This commit is contained in:
harwinder
2026-05-11 17:16:07 -04:00
parent 8ead05432e
commit 02eedcc875
+80 -3
View File
@@ -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_"))