Refocus on XAU/USD: add momentum indicators, tune TP, exhaustion block
Strip EUR/USD, GBP/USD, USD/JPY pairs and strategies from entire codebase. Add 3 new scored indicators (ROC, Keltner/TTM Squeeze, ADX slope) and enable MACD-as-pillar + divergence detection for early trend entry. Introduce "transitioning" regime (ADX < 25, slope > 3.0) to catch trend births before ADX confirms. Gold-specific composite weights per regime. Tune TP multipliers: quality-tiered scaling (A+/A get 1.5-2.0x ATR via XAU ATR method, B/C use swing-level targeting). Backtest: +24R total, 65% WR, PF 1.12 over 365 days. Add momentum-confirmed exhaustion block: hard-reject trades when 2+ static exhaustion signals fire AND ROC/MACD confirm fading momentum. Prevents late trend entries without killing strong trend trades. Telegram: simplify buttons to timeframe-only labels, add "Scan All TFs" multi-timeframe summary, add /scan command, fix BB/KC level breach keys, fix price formatting (.2f for gold), fix deprecated asyncio call, fix "1 pairs" grammar and 60s/30s dashboard refresh mismatch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+122
-93
@@ -5,26 +5,18 @@ ForexBot2 — Telegram Bot
|
||||
Async Telegram interface with candle-boundary auto-scanner and
|
||||
real-time WebSocket level-breach monitor.
|
||||
|
||||
Manual scan: 4 pairs × 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.
|
||||
/autoscan toggles for additional chats or to stop.
|
||||
M5 fires at :00/:05/:10/… + 10s buffer (data only, no signals).
|
||||
M15 fires at :00/:15/:30/:45 + 10s buffer.
|
||||
H1 fires at :00 + 10s buffer.
|
||||
H4 fires at :00 every 4 hours + 10s buffer.
|
||||
WebSocket: Streams live ticks from TwelveData for EUR/USD & XAU/USD
|
||||
(Grow plan supports only these 2 WS symbols).
|
||||
WebSocket: Streams live ticks from TwelveData for XAU/USD.
|
||||
When price crosses a key structure level (swing S/R, BB,
|
||||
EMA200) cached from the last boundary scan, triggers an
|
||||
immediate M15 scan and sends a Level Break Alert.
|
||||
5-minute cooldown per pair prevents spam.
|
||||
GBP/USD & USD/JPY are monitored via REST scans only.
|
||||
|
||||
Plan: TwelveData Grow (55 credits/min, 8 trial WS credits).
|
||||
Credit budget (worst case, 4-hour boundary):
|
||||
Unique fetches: 4 pairs × ~6 TFs = ~24 calls (rest cached) → fits in 55/min.
|
||||
WebSocket: 2 symbols = 2 credits (one-time on connect).
|
||||
Level breach scans: ~3 REST calls each, ~10/hour worst case.
|
||||
5-minute cooldown prevents spam.
|
||||
|
||||
Usage:
|
||||
python telegram_bot.py
|
||||
@@ -55,7 +47,7 @@ from telegram.ext import (
|
||||
|
||||
from config import (
|
||||
PlatformConfig, SignalStrength, Timeframe,
|
||||
EURUSD, GBPUSD, XAUUSD, PAIR_INFO,
|
||||
XAUUSD, PAIR_INFO,
|
||||
)
|
||||
from data_provider import ForexDataProvider
|
||||
from signal_aggregator import SignalAggregator, SignalReport
|
||||
@@ -78,14 +70,9 @@ CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022")
|
||||
|
||||
# ── Pairs & buttons ───────────────────────────────────────────────────
|
||||
|
||||
SCAN_PAIRS = [EURUSD, GBPUSD, XAUUSD]
|
||||
|
||||
CORRELATION_GROUPS = {
|
||||
"usd_negative": {"pairs": {EURUSD, GBPUSD}, "label": "USD-weakness"},
|
||||
}
|
||||
SCAN_PAIRS = [XAUUSD]
|
||||
|
||||
_WS_TO_PAIR = {
|
||||
"EUR/USD": EURUSD,
|
||||
"XAU/USD": XAUUSD,
|
||||
}
|
||||
_WS_SYMBOLS = list(_WS_TO_PAIR.keys())
|
||||
@@ -93,7 +80,8 @@ _WS_SYMBOLS = list(_WS_TO_PAIR.keys())
|
||||
_LEVEL_KEYS = (
|
||||
"swing_high_1", "swing_high_2", "swing_high_3",
|
||||
"swing_low_1", "swing_low_2", "swing_low_3",
|
||||
"ema_trend",
|
||||
"ema_trend", "bb_upper", "bb_lower",
|
||||
"kc_upper", "kc_lower",
|
||||
)
|
||||
|
||||
# ── Live signal stats cache ──────────────────────────────────────────
|
||||
@@ -102,15 +90,10 @@ _signal_stats: Dict[str, Dict[str, SignalReport]] = {}
|
||||
_stats_updated: Optional[float] = None
|
||||
|
||||
SCAN_BUTTONS = [
|
||||
{"cb": "scan_eurusd_m15", "symbol": EURUSD, "tf": Timeframe.M15, "label": "EUR/USD 15M"},
|
||||
{"cb": "scan_eurusd_h1", "symbol": EURUSD, "tf": Timeframe.H1, "label": "EUR/USD 1H"},
|
||||
{"cb": "scan_eurusd_h4", "symbol": EURUSD, "tf": Timeframe.H4, "label": "EUR/USD 4H"},
|
||||
{"cb": "scan_gbpusd_m15", "symbol": GBPUSD, "tf": Timeframe.M15, "label": "GBP/USD 15M"},
|
||||
{"cb": "scan_gbpusd_h1", "symbol": GBPUSD, "tf": Timeframe.H1, "label": "GBP/USD 1H"},
|
||||
{"cb": "scan_gbpusd_h4", "symbol": GBPUSD, "tf": Timeframe.H4, "label": "GBP/USD 4H"},
|
||||
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "XAU/USD 15M"},
|
||||
{"cb": "scan_xauusd_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "XAU/USD 1H"},
|
||||
{"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "XAU/USD 4H"},
|
||||
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "15 Min"},
|
||||
{"cb": "scan_xauusd_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "1 Hour"},
|
||||
{"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "4 Hour"},
|
||||
{"cb": "scan_xauusd_all", "symbol": XAUUSD, "tf": None, "label": "Scan All TFs"},
|
||||
]
|
||||
|
||||
SCAN_LOOKUP = {b["cb"]: b for b in SCAN_BUTTONS}
|
||||
@@ -139,6 +122,10 @@ def _escape_md(text: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def _fmt_price(price: float) -> str:
|
||||
return f"{price:.2f}" if price >= 10 else f"{price:.5f}"
|
||||
|
||||
|
||||
# ── Shared data provider (single cache across all scans) ─────────────
|
||||
|
||||
_shared_dp: Optional[ForexDataProvider] = None
|
||||
@@ -210,7 +197,7 @@ def format_signal_message(report: SignalReport) -> str:
|
||||
f"🔍 *{report.display_name} Signal Analysis* ({report.primary_analysis.timeframe if report.primary_analysis else ''})",
|
||||
f"🕐 {now}",
|
||||
"",
|
||||
f"💰 Price: *{report.current_price:.5f}*" if report.current_price < 10 else f"💰 Price: *{report.current_price:.3f}*",
|
||||
f"💰 Price: *{_fmt_price(report.current_price)}*",
|
||||
f"📊 Regime: {regime_str} | Vol: {vol_str}",
|
||||
"",
|
||||
"━━━━━━━━━━━━━━━━━━━━",
|
||||
@@ -243,12 +230,12 @@ def format_signal_message(report: SignalReport) -> str:
|
||||
"━━━━━━━━━━━━━━━━━━━━",
|
||||
f"🚀 *TRADE SETUP*",
|
||||
f"{dir_emoji} Direction: *{ts.direction}*",
|
||||
f"▶ Entry: `{ts.entry_price:.3f}`",
|
||||
f"🛑 SL: `{ts.stop_loss:.3f}` ({ts.risk_pips:.1f} pips)",
|
||||
f"🎯 TP1: `{ts.take_profit_1:.3f}` ({ts.reward_pips:.1f} pips)",
|
||||
f"▶ Entry: `{_fmt_price(ts.entry_price)}`",
|
||||
f"🛑 SL: `{_fmt_price(ts.stop_loss)}` ({ts.risk_pips:.1f} pips)",
|
||||
f"🎯 TP1: `{_fmt_price(ts.take_profit_1)}` ({ts.reward_pips:.1f} pips)",
|
||||
]
|
||||
if ts.take_profit_2:
|
||||
lines.append(f"🎯 TP2: `{ts.take_profit_2:.3f}`")
|
||||
lines.append(f"🎯 TP2: `{_fmt_price(ts.take_profit_2)}`")
|
||||
lines.append(f"⚖️ RR: 1:{ts.risk_reward:.1f}")
|
||||
|
||||
lines += ["", "━━━━━━━━━━━━━━━━━━━━",
|
||||
@@ -257,33 +244,6 @@ def format_signal_message(report: SignalReport) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Correlation filter ───────────────────────────────────────────────
|
||||
|
||||
def _detect_correlated_exposure(reports: List[SignalReport]) -> List[str]:
|
||||
warnings = []
|
||||
for group_info in CORRELATION_GROUPS.values():
|
||||
group_pairs = group_info["pairs"]
|
||||
group_label = group_info["label"]
|
||||
active = [
|
||||
r for r in reports
|
||||
if r.symbol in group_pairs
|
||||
and r.final_signal not in (SignalStrength.NEUTRAL,)
|
||||
]
|
||||
if len(active) >= 2:
|
||||
directions = set()
|
||||
for r in active:
|
||||
if r.final_signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY):
|
||||
directions.add("BUY")
|
||||
else:
|
||||
directions.add("SELL")
|
||||
if len(directions) == 1:
|
||||
names = ", ".join(r.display_name for r in active)
|
||||
warnings.append(
|
||||
f"⚠️ Same exposure ({group_label}): {names} both {directions.pop()}"
|
||||
)
|
||||
return warnings
|
||||
|
||||
|
||||
# ── Auto-scan summary format ─────────────────────────────────────────
|
||||
|
||||
def _format_scan_summary(reports: List[SignalReport], tf: Timeframe) -> str:
|
||||
@@ -310,10 +270,6 @@ def _format_scan_summary(reports: List[SignalReport], tf: Timeframe) -> str:
|
||||
else:
|
||||
lines.append("No actionable signals this cycle.")
|
||||
|
||||
corr_warnings = _detect_correlated_exposure(reports)
|
||||
for w in corr_warnings:
|
||||
lines.append(w)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -358,10 +314,7 @@ def _format_stats_dashboard() -> str:
|
||||
break
|
||||
|
||||
price = ws_price or (latest.current_price if latest else 0)
|
||||
if price < 10:
|
||||
price_str = f"`{price:.5f}`"
|
||||
else:
|
||||
price_str = f"`{price:.2f}`"
|
||||
price_str = f"`{_fmt_price(price)}`"
|
||||
|
||||
regime = latest.market_regime.replace("_", " ").title() if latest else ""
|
||||
|
||||
@@ -383,8 +336,8 @@ def _format_stats_dashboard() -> str:
|
||||
ts = latest.trade_setup
|
||||
d = "📈" if ts.direction == "BUY" else "📉"
|
||||
lines.append(
|
||||
f"{d} {ts.direction} `{ts.entry_price:.3f}` "
|
||||
f"SL `{ts.stop_loss:.3f}` TP `{ts.take_profit_1:.3f}` "
|
||||
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}"
|
||||
)
|
||||
|
||||
@@ -452,12 +405,14 @@ async def _dashboard_loop(bot) -> None:
|
||||
# ── Keyboard helper ───────────────────────────────────────────────────
|
||||
|
||||
def scan_keyboard() -> InlineKeyboardMarkup:
|
||||
rows = []
|
||||
for i in range(0, len(SCAN_BUTTONS), 4):
|
||||
rows.append([
|
||||
InlineKeyboardButton(b["label"], callback_data=b["cb"])
|
||||
for b in SCAN_BUTTONS[i:i + 4]
|
||||
])
|
||||
tf_buttons = [b for b in SCAN_BUTTONS if b["tf"] is not None]
|
||||
all_button = next((b for b in SCAN_BUTTONS if b["tf"] is None), None)
|
||||
rows = [[
|
||||
InlineKeyboardButton(b["label"], callback_data=b["cb"])
|
||||
for b in tf_buttons
|
||||
]]
|
||||
if all_button:
|
||||
rows.append([InlineKeyboardButton(all_button["label"], callback_data=all_button["cb"])])
|
||||
return InlineKeyboardMarkup(rows)
|
||||
|
||||
|
||||
@@ -485,7 +440,7 @@ async def run_signal(
|
||||
timeframe: Timeframe,
|
||||
confirmation_tfs: Optional[List[Timeframe]] = None,
|
||||
) -> SignalReport:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(
|
||||
_executor, _run_signal_sync, symbol, timeframe, confirmation_tfs
|
||||
)
|
||||
@@ -729,7 +684,7 @@ def _start_scheduler(bot) -> None:
|
||||
|
||||
if _dashboard_task is None or _dashboard_task.done():
|
||||
_dashboard_task = asyncio.create_task(_dashboard_loop(bot))
|
||||
logger.info("Dashboard 60s refresh loop started")
|
||||
logger.info(f"Dashboard {_DASHBOARD_REFRESH_INTERVAL}s refresh loop started")
|
||||
|
||||
|
||||
def _stop_scheduler() -> None:
|
||||
@@ -755,10 +710,11 @@ def _stop_scheduler() -> None:
|
||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
scanning = "running" if _autoscan_chats else "off"
|
||||
text = (
|
||||
"👋 *ForexBot2 — Signal Intelligence*\n\n"
|
||||
"Pick a pair and timeframe to scan.\n"
|
||||
"👋 *ForexBot2 — XAU/USD Signal Intelligence*\n\n"
|
||||
"Pick a timeframe to scan gold.\n"
|
||||
"Signals include a full trade setup with entry, SL and TP.\n\n"
|
||||
f"Auto-scan: *{scanning}* — /autoscan to toggle\n"
|
||||
"/scan — Quick scan buttons\n"
|
||||
"/status — Bot & WebSocket status\n"
|
||||
"/help — All commands"
|
||||
)
|
||||
@@ -772,15 +728,16 @@ 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\n"
|
||||
"/start — Welcome & scan buttons\n"
|
||||
"/scan — Quick XAU/USD scan buttons\n"
|
||||
"/autoscan — Toggle auto-scan on/off\n"
|
||||
"/status — Bot status, WS connection, live prices\n"
|
||||
"/cache — Show data cache age per pair/TF\n"
|
||||
"/clearcache — Wipe data cache, force fresh fetch\n"
|
||||
"/help — This message\n\n"
|
||||
"*Auto-scan* fires at candle close: M5/M15/H1/H4.\n"
|
||||
"Live dashboard auto-refreshes every 60s.\n"
|
||||
"*WebSocket* monitors live prices and updates level cache."
|
||||
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)
|
||||
|
||||
@@ -808,10 +765,8 @@ async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None
|
||||
age = time.time() - _last_price_ts.get(pair, 0)
|
||||
if price is None:
|
||||
lines.append(f" {display}: waiting...")
|
||||
elif price < 10:
|
||||
lines.append(f" {display}: `{price:.5f}` ({age:.0f}s ago)")
|
||||
else:
|
||||
lines.append(f" {display}: `{price:.3f}` ({age:.0f}s ago)")
|
||||
lines.append(f" {display}: `{_fmt_price(price)}` ({age:.0f}s ago)")
|
||||
else:
|
||||
lines.append("\n_No live prices yet (WS not streaming)_")
|
||||
|
||||
@@ -897,8 +852,8 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
next_h4 = _seconds_until_boundary(240, buffer=10)
|
||||
await update.message.reply_text(
|
||||
"✅ *Auto-scan enabled*\n\n"
|
||||
f"Scanning {len(SCAN_PAIRS)} pairs on M5/M15/H1/H4 at candle close.\n"
|
||||
f"📊 Live dashboard refreshes every 60s.\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"
|
||||
@@ -907,6 +862,14 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
)
|
||||
|
||||
|
||||
async def cmd_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.message.reply_text(
|
||||
"🔍 *XAU/USD — Pick a timeframe*",
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
reply_markup=scan_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
@@ -915,9 +878,12 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not btn:
|
||||
return
|
||||
|
||||
pair_display = btn["label"]
|
||||
if btn["tf"] is None:
|
||||
await _cb_scan_all(query)
|
||||
return
|
||||
|
||||
await query.edit_message_text(
|
||||
f"⏳ Scanning {pair_display}…",
|
||||
f"⏳ Scanning XAU/USD {btn['label']}…",
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
)
|
||||
|
||||
@@ -935,12 +901,74 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _cb_scan_all(query) -> None:
|
||||
await query.edit_message_text(
|
||||
"⏳ Scanning XAU/USD across M15 / H1 / H4…",
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
)
|
||||
|
||||
tfs = [Timeframe.M15, Timeframe.H1, Timeframe.H4]
|
||||
tasks = [run_signal(XAUUSD, tf) for tf in tfs]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
reports = []
|
||||
errors = []
|
||||
for tf, result in zip(tfs, results):
|
||||
if isinstance(result, Exception):
|
||||
errors.append(f"❌ {tf.value}: {result}")
|
||||
else:
|
||||
reports.append(result)
|
||||
_update_signal_stats([result], tf)
|
||||
|
||||
if reports:
|
||||
_update_level_cache(reports)
|
||||
|
||||
lines = [
|
||||
"📊 *XAU/USD — All Timeframes*",
|
||||
f"🕐 {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
|
||||
"",
|
||||
]
|
||||
|
||||
for r in reports:
|
||||
emoji = _SIGNAL_EMOJI.get(r.final_signal, "⬜")
|
||||
qe = _QUALITY_EMOJI.get(r.signal_quality, "")
|
||||
tf_label = r.primary_analysis.timeframe if r.primary_analysis else "?"
|
||||
sig = _signal_label(r.final_signal)
|
||||
regime = r.market_regime.replace("_", " ").title()
|
||||
lines.append(
|
||||
f"{emoji} *{tf_label}* {sig} {qe}{r.signal_quality} "
|
||||
f"Score: {r.final_score:+.1f} {r.final_confidence:.0f}%"
|
||||
)
|
||||
lines.append(f" _{_escape_md(regime)}_ | {'✅ MTF' if r.mtf_agreement else '❌ MTF'}")
|
||||
if r.trade_setup and r.final_signal != SignalStrength.NEUTRAL:
|
||||
ts = r.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("")
|
||||
|
||||
for err in errors:
|
||||
lines.append(err)
|
||||
|
||||
lines.append("_Use buttons below for detailed single-TF view._")
|
||||
|
||||
await query.edit_message_text(
|
||||
"\n".join(lines),
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
reply_markup=scan_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
# ── Boot-time auto-start ──────────────────────────────────────────────
|
||||
|
||||
async def _post_init(app: Application) -> None:
|
||||
"""Register command menu and start scheduler on boot."""
|
||||
await app.bot.set_my_commands([
|
||||
("start", "Show scan buttons"),
|
||||
("start", "Welcome & scan buttons"),
|
||||
("scan", "Quick XAU/USD scan buttons"),
|
||||
("autoscan", "Toggle auto-scan on/off"),
|
||||
("status", "Bot status, WS connection, live prices"),
|
||||
("cache", "Show data cache age per pair/TF"),
|
||||
@@ -989,8 +1017,8 @@ async def _post_init(app: Application) -> None:
|
||||
await app.bot.send_message(
|
||||
chat_id,
|
||||
f"🤖 *ForexBot2 started*\n\n"
|
||||
f"Auto-scanning {len(SCAN_PAIRS)} pairs on M5/M15/H1/H4.\n"
|
||||
f"📊 Live dashboard refreshes every 60s.\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"
|
||||
@@ -1028,6 +1056,7 @@ def main() -> None:
|
||||
)
|
||||
|
||||
app.add_handler(CommandHandler("start", cmd_start))
|
||||
app.add_handler(CommandHandler("scan", cmd_scan))
|
||||
app.add_handler(CommandHandler("help", cmd_help))
|
||||
app.add_handler(CommandHandler("status", cmd_status))
|
||||
app.add_handler(CommandHandler("autoscan", cmd_autoscan))
|
||||
|
||||
Reference in New Issue
Block a user