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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,7 @@ Twelve Data REST API + WebSocket
|
|||||||
telegram_bot.py main.py
|
telegram_bot.py main.py
|
||||||
(Telegram alerts, (CLI single-pair
|
(Telegram alerts, (CLI single-pair
|
||||||
auto-scanner, analysis)
|
auto-scanner, analysis)
|
||||||
WS monitor,
|
WS monitor)
|
||||||
live dashboard)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Files
|
## Core Files
|
||||||
@@ -31,7 +30,7 @@ telegram_bot.py main.py
|
|||||||
| `engine.py` | `TechnicalEngine` — computes EMA, RSI, BB, ADX, ATR, MACD, ROC, Keltner, divergences, candlestick patterns, swing S/R |
|
| `engine.py` | `TechnicalEngine` — computes EMA, RSI, BB, ADX, ATR, MACD, ROC, Keltner, divergences, candlestick patterns, swing S/R |
|
||||||
| `signal_aggregator.py` | `SignalAggregator` — MTF analysis, news integration, diminishing penalty cap, trade setup generation |
|
| `signal_aggregator.py` | `SignalAggregator` — MTF analysis, news integration, diminishing penalty cap, trade setup generation |
|
||||||
| `news_engine.py` | Economic calendar (Forex Factory JSON) + RSS sentiment (keyword-based) |
|
| `news_engine.py` | Economic calendar (Forex Factory JSON) + RSS sentiment (keyword-based) |
|
||||||
| `telegram_bot.py` | Async Telegram bot with auto-scanner (M5/M15/H1/H4), WebSocket price monitor, live dashboard |
|
| `telegram_bot.py` | Async Telegram bot with auto-scanner (M5/M15/H1/H4), WebSocket price monitor |
|
||||||
| `main.py` | CLI entry point for XAU/USD analysis |
|
| `main.py` | CLI entry point for XAU/USD analysis |
|
||||||
| `test_all.py` | Comprehensive QA suite (~137 tests, no live API needed) |
|
| `test_all.py` | Comprehensive QA suite (~137 tests, no live API needed) |
|
||||||
|
|
||||||
@@ -91,10 +90,9 @@ python backtest/gpu_backtest.py --pair XAUUSD --days 365
|
|||||||
|
|
||||||
## Telegram Bot
|
## Telegram Bot
|
||||||
|
|
||||||
- **Auto-scan:** M5 (data-only, no alerts), M15, H1, H4 at candle boundaries + 10s buffer
|
- **Auto-scan:** M5, M15, H1, H4 at candle boundaries + 10s buffer (all timeframes alert)
|
||||||
- **Manual scan:** Buttons for M15/H1/H4 individual scans + "Scan All TFs" multi-timeframe summary
|
- **Manual scan:** Buttons for M15/H1/H4 individual scans + "Scan All TFs" multi-timeframe summary
|
||||||
- **WebSocket:** Streams live ticks for XAU/USD; triggers immediate M15 scan on key level breach (swing S/R, BB, KC, EMA200) with 5-min cooldown
|
- **WebSocket:** Streams live ticks for XAU/USD; triggers immediate M15 scan on key level breach (swing S/R, BB, KC, EMA200) with 5-min cooldown
|
||||||
- **Live dashboard:** Edit-in-place message, 30s refresh, shows XAU/USD across timeframes
|
|
||||||
- **Commands:** `/start`, `/scan`, `/autoscan`, `/status`, `/cache`, `/clearcache`, `/help`
|
- **Commands:** `/start`, `/scan`, `/autoscan`, `/status`, `/cache`, `/clearcache`, `/help`
|
||||||
|
|
||||||
## Backtesting
|
## Backtesting
|
||||||
|
|||||||
+5
-9
@@ -22,12 +22,12 @@ The bot looks at the same pair across multiple zoom levels, like looking at a ma
|
|||||||
|
|
||||||
| Timeframe | What It Shows | How Often It Scans |
|
| Timeframe | What It Shows | How Often It Scans |
|
||||||
|-----------|--------------|-------------------|
|
|-----------|--------------|-------------------|
|
||||||
| 5-Minute (M5) | Very short-term price action | Every 5 minutes — **data collection only**, no alerts |
|
| 5-Minute (M5) | Very short-term price action | Every 5 minutes |
|
||||||
| 15-Minute (M15) | Short-term moves | Every 15 minutes |
|
| 15-Minute (M15) | Short-term moves | Every 15 minutes |
|
||||||
| 1-Hour (H1) | Intraday trends | Every hour |
|
| 1-Hour (H1) | Intraday trends | Every hour |
|
||||||
| 4-Hour (H4) | Swing-level trends | Every 4 hours |
|
| 4-Hour (H4) | Swing-level trends | Every 4 hours |
|
||||||
|
|
||||||
The M5 scan runs silently in the background — it gathers data to keep the bot's internal picture fresh, but it does not send you any messages. Only M15, H1, and H4 scans produce alerts.
|
All four timeframes (M5, M15, H1, H4) produce alerts at candle close when the signal grade is C or better.
|
||||||
|
|
||||||
## How Does It Decide What To Signal?
|
## How Does It Decide What To Signal?
|
||||||
|
|
||||||
@@ -911,12 +911,8 @@ def _run_signal_sync(symbol, timeframe, confirmation_tfs):
|
|||||||
1. For each symbol in SCAN_PAIRS:
|
1. For each symbol in SCAN_PAIRS:
|
||||||
report = await run_signal(symbol, tf, confirmation_tfs)
|
report = await run_signal(symbol, tf, confirmation_tfs)
|
||||||
2. _update_level_cache(reports) # Refresh cached levels for WS breach detection
|
2. _update_level_cache(reports) # Refresh cached levels for WS breach detection
|
||||||
3. If tf == M5: return # Data only, no Telegram output
|
3. Filter actionable: quality in (A+, A, B, C) # applies to every timeframe, M5 included
|
||||||
4. Filter actionable: quality in (A+, A, B, C)
|
4. For each autoscan chat:
|
||||||
5. If none actionable: log and return silently
|
|
||||||
6. Format scan summary (one-liner per pair)
|
|
||||||
7. For each autoscan chat:
|
|
||||||
Send summary message
|
|
||||||
Send detail message per actionable signal (includes trade setup)
|
Send detail message per actionable signal (includes trade setup)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1029,7 +1025,7 @@ Terminal output includes color-coded sections:
|
|||||||
|
|
||||||
3. **WebSocket for XAU/USD only:** The bot streams live prices via WebSocket for real-time level breach detection on gold.
|
3. **WebSocket for XAU/USD only:** The bot streams live prices via WebSocket for real-time level breach detection on gold.
|
||||||
|
|
||||||
4. **M5 is data-only:** The M5 scan runs every 5 minutes but only updates the level cache (used by WebSocket breach detection). It does not send Telegram messages — this reduces noise while keeping the internal picture current.
|
4. **M5 alerts like every timeframe:** The M5 scan runs every 5 minutes, refreshes the level cache (used by WebSocket breach detection), and sends a Telegram alert whenever the signal grade is C or better — the same path as M15/H1/H4.
|
||||||
|
|
||||||
5. **Graceful degradation everywhere:** If news APIs fail, signals proceed without fundamental context. If WebSocket disconnects, it reconnects with exponential backoff. If a single pair's scan fails, others continue.
|
5. **Graceful degradation everywhere:** If news APIs fail, signals proceed without fundamental context. If WebSocket disconnects, it reconnects with exponential backoff. If a single pair's scan fails, others continue.
|
||||||
|
|
||||||
|
|||||||
+4
-158
@@ -8,7 +8,7 @@ real-time WebSocket level-breach monitor.
|
|||||||
Manual scan: XAU/USD × 3 timeframes (M15/H1/H4) via inline buttons.
|
Manual scan: XAU/USD × 3 timeframes (M15/H1/H4) via inline buttons.
|
||||||
Auto-scan: Starts automatically on boot if TELEGRAM_CHAT_ID is set.
|
Auto-scan: Starts automatically on boot if TELEGRAM_CHAT_ID is set.
|
||||||
/autoscan toggles for additional chats or to stop.
|
/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.
|
M15 fires at :00/:15/:30/:45 + 10s buffer.
|
||||||
H1 fires at :00 + 10s buffer.
|
H1 fires at :00 + 10s buffer.
|
||||||
H4 fires at :00 every 4 hours + 10s buffer.
|
H4 fires at :00 every 4 hours + 10s buffer.
|
||||||
@@ -32,7 +32,6 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
from typing import Dict, List, Optional, Set
|
from typing import Dict, List, Optional, Set
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
@@ -85,11 +84,6 @@ _LEVEL_KEYS = (
|
|||||||
"kc_upper", "kc_lower",
|
"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 = [
|
SCAN_BUTTONS = [
|
||||||
{"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "5 Min"},
|
{"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_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)
|
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 ───────────────────────────────────────────────────
|
# ── Keyboard helper ───────────────────────────────────────────────────
|
||||||
|
|
||||||
def scan_keyboard() -> InlineKeyboardMarkup:
|
def scan_keyboard() -> InlineKeyboardMarkup:
|
||||||
@@ -487,9 +351,6 @@ async def _scan_timeframe(
|
|||||||
return
|
return
|
||||||
|
|
||||||
_update_level_cache(reports)
|
_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")]
|
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
|
||||||
for chat_id in list(_autoscan_chats):
|
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:
|
try:
|
||||||
report = await run_signal(pair_symbol, Timeframe.M15)
|
report = await run_signal(pair_symbol, Timeframe.M15)
|
||||||
_update_level_cache([report])
|
_update_level_cache([report])
|
||||||
_update_signal_stats([report], Timeframe.M15)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(f"Breach scan failed for {display}")
|
logger.exception(f"Breach scan failed for {display}")
|
||||||
|
|
||||||
@@ -667,7 +527,7 @@ _ws_task: Optional[asyncio.Task] = None
|
|||||||
|
|
||||||
|
|
||||||
def _start_scheduler(bot) -> 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()]
|
alive = [t for t in _autoscan_tasks if not t.done()]
|
||||||
if not alive:
|
if not alive:
|
||||||
_autoscan_tasks.clear()
|
_autoscan_tasks.clear()
|
||||||
@@ -681,27 +541,19 @@ def _start_scheduler(bot) -> None:
|
|||||||
_ws_task = asyncio.create_task(_ws_monitor(bot))
|
_ws_task = asyncio.create_task(_ws_monitor(bot))
|
||||||
logger.info("WebSocket level-breach monitor started")
|
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:
|
def _stop_scheduler() -> None:
|
||||||
global _ws_task, _dashboard_task
|
global _ws_task
|
||||||
for t in _autoscan_tasks:
|
for t in _autoscan_tasks:
|
||||||
t.cancel()
|
t.cancel()
|
||||||
_autoscan_tasks.clear()
|
_autoscan_tasks.clear()
|
||||||
if _ws_task and not _ws_task.done():
|
if _ws_task and not _ws_task.done():
|
||||||
_ws_task.cancel()
|
_ws_task.cancel()
|
||||||
_ws_task = None
|
_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):
|
for t in list(_breach_tasks):
|
||||||
t.cancel()
|
t.cancel()
|
||||||
_breach_tasks.clear()
|
_breach_tasks.clear()
|
||||||
logger.info("Scheduler, WebSocket, and dashboard loop stopped")
|
logger.info("Scheduler and WebSocket monitor stopped")
|
||||||
|
|
||||||
|
|
||||||
# ── Handlers ──────────────────────────────────────────────────────────
|
# ── Handlers ──────────────────────────────────────────────────────────
|
||||||
@@ -735,7 +587,6 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
"/clearcache — Wipe data cache, force fresh fetch\n"
|
"/clearcache — Wipe data cache, force fresh fetch\n"
|
||||||
"/help — This message\n\n"
|
"/help — This message\n\n"
|
||||||
"*Auto-scan* fires at candle close: M5/M15/H1/H4.\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."
|
"*WebSocket* monitors live prices and triggers breach scans."
|
||||||
)
|
)
|
||||||
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
|
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(
|
await update.message.reply_text(
|
||||||
"✅ *Auto-scan enabled*\n\n"
|
"✅ *Auto-scan enabled*\n\n"
|
||||||
f"Scanning XAU/USD on M5/M15/H1/H4 at candle close.\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"📡 Live price monitoring active.\n"
|
||||||
f"Next: M5 {next_m5 / 60:.0f}m | M15 {next_m15 / 60:.0f}m | "
|
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"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}")
|
errors.append(f"❌ {tf.value}: {result}")
|
||||||
else:
|
else:
|
||||||
reports.append(result)
|
reports.append(result)
|
||||||
_update_signal_stats([result], tf)
|
|
||||||
|
|
||||||
if reports:
|
if reports:
|
||||||
_update_level_cache(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}")
|
logger.error(f"Initial scan failed for {SCAN_PAIRS[i]}: {r}")
|
||||||
if init_reports:
|
if init_reports:
|
||||||
_update_level_cache(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")
|
logger.info(f"Level cache populated: {sum(len(v) for v in _level_cache.values())} levels")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Initial scan failed — WS will wait for first candle scan")
|
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,
|
chat_id,
|
||||||
f"🤖 *ForexBot2 started*\n\n"
|
f"🤖 *ForexBot2 started*\n\n"
|
||||||
f"Auto-scanning XAU/USD on M5/M15/H1/H4.\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"📡 Live price monitoring active.\n"
|
||||||
f"Next: M5 {next_m5 / 60:.0f}m | M15 {next_m15 / 60:.0f}m | "
|
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"H1 {next_h1 / 60:.0f}m | H4 {next_h4 / 60:.0f}m\n\n"
|
||||||
f"/autoscan to stop",
|
f"/autoscan to stop",
|
||||||
parse_mode=ParseMode.MARKDOWN,
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
)
|
)
|
||||||
await _send_dashboard(app.bot, chat_id)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to send boot message")
|
logger.exception("Failed to send boot message")
|
||||||
|
|
||||||
|
|||||||
-11
@@ -1322,7 +1322,6 @@ from telegram_bot import (
|
|||||||
_escape_md, _signal_label, _mtf_line,
|
_escape_md, _signal_label, _mtf_line,
|
||||||
format_signal_message, _format_scan_summary,
|
format_signal_message, _format_scan_summary,
|
||||||
_seconds_until_boundary, _check_level_breach,
|
_seconds_until_boundary, _check_level_breach,
|
||||||
_format_stats_dashboard,
|
|
||||||
scan_keyboard, SCAN_BUTTONS, SCAN_LOOKUP,
|
scan_keyboard, SCAN_BUTTONS, SCAN_LOOKUP,
|
||||||
_update_level_cache, _last_prices, _level_cache, _level_cache_ts,
|
_update_level_cache, _last_prices, _level_cache, _level_cache_ts,
|
||||||
_WS_TO_PAIR,
|
_WS_TO_PAIR,
|
||||||
@@ -1529,16 +1528,6 @@ def _():
|
|||||||
assert _WS_TO_PAIR["XAU/USD"] == XAUUSD
|
assert _WS_TO_PAIR["XAU/USD"] == XAUUSD
|
||||||
|
|
||||||
|
|
||||||
@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()
|
|
||||||
text = _format_stats_dashboard()
|
|
||||||
assert "Waiting" in text or "waiting" in text.lower()
|
|
||||||
telegram_bot._signal_stats.update(old_stats)
|
|
||||||
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
# MODULE 8: Integration / Cross-module tests
|
# MODULE 8: Integration / Cross-module tests
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user