Enable M5 auto-scan alerts and remove live dashboard

M5 already broadcast through the shared _scan_timeframe path but was
documented as data-only (docstring, CLAUDE.md, DOCUMENTATION.md, plus a
phantom `if tf == M5: return` step that never existed in code). Align all
docs/comments so M5 alerting at C+ grade is the intended, documented behavior.

Remove the live dashboard entirely per request: the 30s refresh loop,
per-scan refresh, boot send, and the dashboard-only stats layer
(_signal_stats / _stats_updated / _update_signal_stats and its call sites,
_format_stats_dashboard and its helper constants). Drop now-unused ZoneInfo
import and dashboard mentions from help/autoscan/boot messages and the test
suite. The WebSocket level-breach monitor and level cache are untouched.

Tests: 136 passed, 0 failed.
This commit is contained in:
Harwinder
2026-05-20 12:45:27 -04:00
parent cc53448c34
commit c46bb85012
4 changed files with 12 additions and 183 deletions
+4 -158
View File
@@ -8,7 +8,7 @@ real-time WebSocket level-breach monitor.
Manual scan: XAU/USD × 3 timeframes (M15/H1/H4) 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).
M5 fires at :00/:05/:10/… + 10s buffer.
M15 fires at :00/:15/:30/:45 + 10s buffer.
H1 fires at :00 + 10s buffer.
H4 fires at :00 every 4 hours + 10s buffer.
@@ -32,7 +32,6 @@ import sys
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from typing import Dict, List, Optional, Set
import websockets
@@ -85,11 +84,6 @@ _LEVEL_KEYS = (
"kc_upper", "kc_lower",
)
# ── Live signal stats cache ──────────────────────────────────────────
# symbol → { tf_label → SignalReport }
_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"},
@@ -275,136 +269,6 @@ def _format_scan_summary(reports: List[SignalReport], tf: Timeframe) -> str:
return "\n".join(lines)
def _update_signal_stats(reports: List[SignalReport], tf: Timeframe) -> None:
global _stats_updated
for r in reports:
if r.symbol not in _signal_stats:
_signal_stats[r.symbol] = {}
_signal_stats[r.symbol][tf.value] = r
_stats_updated = time.time()
_SHORT_SIGNAL = {
SignalStrength.STRONG_BUY: "Buy",
SignalStrength.BUY: "Buy",
SignalStrength.NEUTRAL: "Neutral",
SignalStrength.SELL: "Sell",
SignalStrength.STRONG_SELL: "Sell",
}
_TF_DISPLAY = {"5min": "M5", "15min": "M15", "1h": "H1", "4h": "H4", "1d": "D1"}
_DASH_TFS = [Timeframe.M15, Timeframe.H1, Timeframe.H4]
def _format_stats_dashboard() -> str:
if not _signal_stats:
return "📊 *Live Dashboard*\n\n_Waiting for first scan…_"
_ny_tz = ZoneInfo("America/New_York")
now = datetime.now(_ny_tz).strftime("%H:%M:%S %Z")
lines = [f"📊 *Live Dashboard* — {now}", ""]
for symbol in SCAN_PAIRS:
display = PAIR_INFO.get(symbol, {}).get("display", symbol)
pair_data = _signal_stats.get(symbol, {})
ws_price = _last_prices.get(symbol)
latest = None
for tf in reversed(_DASH_TFS):
if tf.value in pair_data:
latest = pair_data[tf.value]
break
price = ws_price or (latest.current_price if latest else 0)
price_str = f"`{_fmt_price(price)}`"
regime = latest.market_regime.replace("_", " ").title() if latest else ""
tf_parts = []
for tf in _DASH_TFS:
label = _TF_DISPLAY.get(tf.value, tf.value)
r = pair_data.get(tf.value)
if not r:
tf_parts.append(f"{label}: ⬜—")
continue
emoji = _SIGNAL_EMOJI.get(r.final_signal, "⬜")
sig = _SHORT_SIGNAL.get(r.final_signal, "Neutral")
tf_parts.append(f"{label}: {emoji}{sig}")
lines.append(f"*{display}* {price_str} _{_escape_md(regime)}_")
lines.append(" | ".join(tf_parts))
if latest and latest.trade_setup and latest.final_signal not in (SignalStrength.NEUTRAL,):
ts = latest.trade_setup
d = "📈" if ts.direction == "BUY" else "📉"
lines.append(
f"{d} {ts.direction} `{_fmt_price(ts.entry_price)}` "
f"SL `{_fmt_price(ts.stop_loss)}` TP `{_fmt_price(ts.take_profit_1)}` "
f"RR 1:{ts.risk_reward:.1f}"
)
lines.append("")
lines.append("_🔄 live refresh_")
return "\n".join(lines)
# ── Live dashboard (edit-in-place) ───────────────────────────────────
_dashboard_msg_ids: Dict[int, int] = {}
_dashboard_task: Optional[asyncio.Task] = None
_last_dashboard_refresh: float = 0
_DASHBOARD_REFRESH_INTERVAL = 30
_dashboard_lock = asyncio.Lock()
async def _send_dashboard(bot, chat_id: int) -> None:
text = _format_stats_dashboard()
try:
msg = await bot.send_message(chat_id, text, parse_mode=ParseMode.MARKDOWN)
_dashboard_msg_ids[chat_id] = msg.message_id
except Exception as exc:
logger.warning(f"Dashboard send failed for chat {chat_id}: {exc}")
async def _refresh_dashboard(bot, force: bool = False) -> None:
global _last_dashboard_refresh
async with _dashboard_lock:
now = time.time()
if not force and (now - _last_dashboard_refresh) < _DASHBOARD_REFRESH_INTERVAL:
return
_last_dashboard_refresh = now
text = _format_stats_dashboard()
for chat_id in list(_autoscan_chats):
msg_id = _dashboard_msg_ids.get(chat_id)
if not msg_id:
await _send_dashboard(bot, chat_id)
continue
try:
await bot.edit_message_text(
text, chat_id=chat_id, message_id=msg_id,
parse_mode=ParseMode.MARKDOWN,
)
except Exception as exc:
if "message is not modified" not in str(exc).lower():
logger.warning(f"Dashboard edit failed for chat {chat_id}: {exc}")
async def _dashboard_loop(bot) -> None:
try:
while _autoscan_chats:
await asyncio.sleep(_DASHBOARD_REFRESH_INTERVAL)
if not _autoscan_chats:
break
await _refresh_dashboard(bot)
except asyncio.CancelledError:
pass
finally:
logger.info("Dashboard refresh loop stopped")
# ── Keyboard helper ───────────────────────────────────────────────────
def scan_keyboard() -> InlineKeyboardMarkup:
@@ -487,9 +351,6 @@ async def _scan_timeframe(
return
_update_level_cache(reports)
_update_signal_stats(reports, tf)
await _refresh_dashboard(bot)
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
for chat_id in list(_autoscan_chats):
@@ -571,7 +432,6 @@ async def _handle_breach(bot, ws_symbol: str, pair_symbol: str, price: float, le
try:
report = await run_signal(pair_symbol, Timeframe.M15)
_update_level_cache([report])
_update_signal_stats([report], Timeframe.M15)
except Exception:
logger.exception(f"Breach scan failed for {display}")
@@ -667,7 +527,7 @@ _ws_task: Optional[asyncio.Task] = None
def _start_scheduler(bot) -> None:
global _ws_task, _dashboard_task
global _ws_task
alive = [t for t in _autoscan_tasks if not t.done()]
if not alive:
_autoscan_tasks.clear()
@@ -681,27 +541,19 @@ def _start_scheduler(bot) -> None:
_ws_task = asyncio.create_task(_ws_monitor(bot))
logger.info("WebSocket level-breach monitor started")
if _dashboard_task is None or _dashboard_task.done():
_dashboard_task = asyncio.create_task(_dashboard_loop(bot))
logger.info(f"Dashboard {_DASHBOARD_REFRESH_INTERVAL}s refresh loop started")
def _stop_scheduler() -> None:
global _ws_task, _dashboard_task
global _ws_task
for t in _autoscan_tasks:
t.cancel()
_autoscan_tasks.clear()
if _ws_task and not _ws_task.done():
_ws_task.cancel()
_ws_task = None
if _dashboard_task and not _dashboard_task.done():
_dashboard_task.cancel()
_dashboard_task = None
_dashboard_msg_ids.clear()
for t in list(_breach_tasks):
t.cancel()
_breach_tasks.clear()
logger.info("Scheduler, WebSocket, and dashboard loop stopped")
logger.info("Scheduler and WebSocket monitor stopped")
# ── Handlers ──────────────────────────────────────────────────────────
@@ -735,7 +587,6 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"/clearcache — Wipe data cache, force fresh fetch\n"
"/help — This message\n\n"
"*Auto-scan* fires at candle close: M5/M15/H1/H4.\n"
f"Live dashboard auto-refreshes every {_DASHBOARD_REFRESH_INTERVAL}s.\n"
"*WebSocket* monitors live prices and triggers breach scans."
)
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
@@ -852,7 +703,6 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
await update.message.reply_text(
"✅ *Auto-scan enabled*\n\n"
f"Scanning XAU/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 | "
f"H1 {next_h1 / 60:.0f}m | H4 {next_h4 / 60:.0f}m\n\n"
@@ -917,7 +767,6 @@ async def _cb_scan_all(query) -> None:
errors.append(f"❌ {tf.value}: {result}")
else:
reports.append(result)
_update_signal_stats([result], tf)
if reports:
_update_level_cache(reports)
@@ -996,7 +845,6 @@ async def _post_init(app: Application) -> None:
logger.error(f"Initial scan failed for {SCAN_PAIRS[i]}: {r}")
if init_reports:
_update_level_cache(init_reports)
_update_signal_stats(init_reports, Timeframe.M15)
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")
@@ -1017,14 +865,12 @@ async def _post_init(app: Application) -> None:
chat_id,
f"🤖 *ForexBot2 started*\n\n"
f"Auto-scanning XAU/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 | "
f"H1 {next_h1 / 60:.0f}m | H4 {next_h4 / 60:.0f}m\n\n"
f"/autoscan to stop",
parse_mode=ParseMode.MARKDOWN,
)
await _send_dashboard(app.bot, chat_id)
except Exception:
logger.exception("Failed to send boot message")