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 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+80
-3
@@ -361,7 +361,8 @@ _autoscan_tasks: list[asyncio.Task] = []
|
|||||||
def _seconds_until_boundary(interval_minutes: int, buffer: int = 10) -> float:
|
def _seconds_until_boundary(interval_minutes: int, buffer: int = 10) -> float:
|
||||||
"""Seconds until the next candle close + buffer."""
|
"""Seconds until the next candle close + buffer."""
|
||||||
now = datetime.now(timezone.utc)
|
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
|
elapsed_seconds = elapsed_minutes * 60 + now.second + now.microsecond / 1e6
|
||||||
interval_seconds = interval_minutes * 60
|
interval_seconds = interval_minutes * 60
|
||||||
remaining = interval_seconds - elapsed_seconds + buffer
|
remaining = interval_seconds - elapsed_seconds + buffer
|
||||||
@@ -580,8 +581,10 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
text = (
|
text = (
|
||||||
"👋 *ForexBot2 — Signal Intelligence*\n\n"
|
"👋 *ForexBot2 — Signal Intelligence*\n\n"
|
||||||
"Pick a pair and timeframe to scan.\n"
|
"Pick a pair and timeframe to scan.\n"
|
||||||
"Strong signals include a full trade setup with entry, SL and TP.\n\n"
|
"Signals include a full trade setup with entry, SL and TP.\n\n"
|
||||||
f"Auto-scan: *{scanning}* — /autoscan to toggle"
|
f"Auto-scan: *{scanning}* — /autoscan to toggle\n"
|
||||||
|
"/status — Bot & WebSocket status\n"
|
||||||
|
"/help — All commands"
|
||||||
)
|
)
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
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:
|
async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
|
|
||||||
@@ -660,6 +723,18 @@ async def _post_init(app: Application) -> None:
|
|||||||
|
|
||||||
chat_id = int(CHAT_ID)
|
chat_id = int(CHAT_ID)
|
||||||
_autoscan_chats.add(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)
|
_start_scheduler(app.bot)
|
||||||
|
|
||||||
next_m5 = _seconds_until_boundary(5, buffer=10)
|
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("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(CommandHandler("autoscan", cmd_autoscan))
|
||||||
app.add_handler(CallbackQueryHandler(cb_scan, pattern="^scan_"))
|
app.add_handler(CallbackQueryHandler(cb_scan, pattern="^scan_"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user