Simplify Telegram bot — mirror main.py output exactly
- Remove /all, /watch, /unwatch, /credits, /status commands - Keep only /scan, /pair, /tf, /help - Output exact same format as main.py terminal output (box-drawing characters, score bars, table, detail report) inside <pre> blocks - /help explains all timeframes with recommended use cases - /scan TF and /pair EURUSD TF accept inline timeframe override - Auto-split messages at newlines to stay under Telegram 4096-char limit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+267
-448
@@ -2,22 +2,17 @@
|
|||||||
"""
|
"""
|
||||||
Forex Signal Intelligence Platform - Telegram Bot
|
Forex Signal Intelligence Platform - Telegram Bot
|
||||||
===================================================
|
===================================================
|
||||||
Exposes all platform functionality through a Telegram bot interface.
|
Mirrors main.py output exactly via Telegram.
|
||||||
|
|
||||||
Usage:
|
Setup:
|
||||||
export TELEGRAM_BOT_TOKEN="your_bot_token_here"
|
export TELEGRAM_BOT_TOKEN="your_token_here"
|
||||||
python telegram_bot.py
|
python telegram_bot.py
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
/scan [TF] — Scan all major pairs
|
/scan [TF] — Full signal scan (all major pairs)
|
||||||
/signals [TF] — Only actionable signals (grade B+)
|
/pair EURUSD [TF] — Detailed analysis for one pair
|
||||||
/pair EURUSD [TF] — Detailed analysis of one pair
|
|
||||||
/all [TF] — Scan major + cross pairs
|
|
||||||
/tf H1 — Set default timeframe
|
/tf H1 — Set default timeframe
|
||||||
/watch [minutes] — Enable periodic scan alerts
|
/help — Commands + timeframe guide
|
||||||
/unwatch — Disable alerts
|
|
||||||
/status — Current settings
|
|
||||||
/help — Full command reference
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -25,21 +20,21 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Dict, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from telegram import BotCommand, Update
|
from telegram import BotCommand, Update
|
||||||
from telegram.constants import ParseMode
|
from telegram.constants import ParseMode
|
||||||
from telegram.ext import Application, CallbackContext, CommandHandler, ContextTypes
|
from telegram.ext import Application, CommandHandler, ContextTypes
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
CROSS_PAIRS, MAJOR_PAIRS, SYMBOL_DISPLAY,
|
MAJOR_PAIRS, SYMBOL_DISPLAY,
|
||||||
PlatformConfig, SignalStrength, Timeframe,
|
PlatformConfig, SignalStrength, Timeframe,
|
||||||
)
|
)
|
||||||
from signal_aggregator import SignalAggregator, SignalReport
|
from signal_aggregator import SignalAggregator, SignalReport
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ── Constants ──────────────────────────────────────────────────────
|
# ── Timeframe config ───────────────────────────────────────────────
|
||||||
|
|
||||||
TF_MAP = {
|
TF_MAP = {
|
||||||
"M5": Timeframe.M5,
|
"M5": Timeframe.M5,
|
||||||
@@ -59,34 +54,162 @@ TF_CONFIRMATIONS = {
|
|||||||
"D1": [Timeframe.H4, Timeframe.W1],
|
"D1": [Timeframe.H4, Timeframe.W1],
|
||||||
}
|
}
|
||||||
|
|
||||||
SIGNAL_EMOJI = {
|
# ── Formatting (mirrors main.py, no ANSI colours) ──────────────────
|
||||||
SignalStrength.STRONG_BUY: "🟢⬆⬆",
|
|
||||||
SignalStrength.BUY: "🟢⬆",
|
|
||||||
SignalStrength.NEUTRAL: "⬜↔",
|
|
||||||
SignalStrength.SELL: "🔴⬇",
|
|
||||||
SignalStrength.STRONG_SELL: "🔴⬇⬇",
|
|
||||||
}
|
|
||||||
|
|
||||||
SIGNAL_NAME = {
|
WIDTH = 80
|
||||||
SignalStrength.STRONG_BUY: "STRONG BUY",
|
|
||||||
SignalStrength.BUY: "BUY",
|
|
||||||
SignalStrength.NEUTRAL: "NEUTRAL",
|
|
||||||
SignalStrength.SELL: "SELL",
|
|
||||||
SignalStrength.STRONG_SELL: "STRONG SELL",
|
|
||||||
}
|
|
||||||
|
|
||||||
GRADE_EMOJI = {
|
|
||||||
"A+": "⭐⭐",
|
|
||||||
"A": "⭐",
|
|
||||||
"B": "🔵",
|
|
||||||
"C": "🟡",
|
|
||||||
"D": "🟠",
|
|
||||||
"F": "⚫",
|
|
||||||
}
|
|
||||||
|
|
||||||
WATCH_MIN_INTERVAL = 5 # minimum watch interval in minutes
|
def _score_bar(score: float, width: int = 30) -> str:
|
||||||
WATCH_MAX_INTERVAL = 120 # maximum watch interval in minutes
|
normalized = (score + 100) / 200
|
||||||
WATCH_DEFAULT = 5
|
filled = int(normalized * width)
|
||||||
|
center = width // 2
|
||||||
|
bar = ""
|
||||||
|
for i in range(width):
|
||||||
|
if i == center:
|
||||||
|
bar += "│"
|
||||||
|
elif (score >= 0 and center < i <= filled) or \
|
||||||
|
(score < 0 and filled <= i < center):
|
||||||
|
bar += "█"
|
||||||
|
else:
|
||||||
|
bar += "░"
|
||||||
|
return bar
|
||||||
|
|
||||||
|
|
||||||
|
def _format_header(timeframe: str) -> str:
|
||||||
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
lines = [
|
||||||
|
"",
|
||||||
|
"═" * WIDTH,
|
||||||
|
" ╔═══════════════════════════════════════════════════════╗",
|
||||||
|
" ║ FOREX SIGNAL INTELLIGENCE PLATFORM ║",
|
||||||
|
" ║ Multi-Layer Signal Analysis Engine ║",
|
||||||
|
" ╚═══════════════════════════════════════════════════════╝",
|
||||||
|
f" Scan Time: {now} TF: {timeframe}",
|
||||||
|
"═" * WIDTH,
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_table(reports: List[SignalReport]) -> str:
|
||||||
|
lines = [
|
||||||
|
" ┌─────────────┬───────────┬──────────────────────────────────┬────────┬────────────┐",
|
||||||
|
" │ Pair │ Signal │ Score │ Grade │ Confidence │",
|
||||||
|
" ├─────────────┼───────────┼──────────────────────────────────┼────────┼────────────┤",
|
||||||
|
]
|
||||||
|
for r in reports:
|
||||||
|
sig = r.final_signal.name.replace("_", " ")
|
||||||
|
bar = _score_bar(r.final_score, 30)
|
||||||
|
pair = f"{r.display_name:<11}"
|
||||||
|
signal_str = f"{sig:<9}"
|
||||||
|
grade_str = f"{r.signal_quality:<6}"
|
||||||
|
conf_str = f"{r.final_confidence:5.1f}%"
|
||||||
|
warn = " ⚠" if r.event_risk == "danger" else (" △" if r.event_risk == "caution" else " ")
|
||||||
|
lines.append(f" │ {pair} │ {signal_str} │ {bar} │ {grade_str} │ {conf_str:>8}{warn} │")
|
||||||
|
lines.append(
|
||||||
|
" └─────────────┴───────────┴──────────────────────────────────┴────────┴────────────┘"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
" Legend: ██ Bullish | ██ Bearish | │ Center (0) | ⚠ Event Risk | △ Caution",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_calendar(reports: List[SignalReport]) -> str:
|
||||||
|
events_shown = set()
|
||||||
|
lines = []
|
||||||
|
for r in reports:
|
||||||
|
if r.event_risk != "clear" and r.event_description not in events_shown:
|
||||||
|
events_shown.add(r.event_description)
|
||||||
|
lines.append(f" ● {r.event_description} → affects {r.display_name}")
|
||||||
|
if lines:
|
||||||
|
return "\n 📅 ECONOMIC CALENDAR ALERTS\n" + "\n".join(lines)
|
||||||
|
return "\n 📅 No high-impact events in the near term"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_detail(r: SignalReport) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of message strings for one pair's detail report.
|
||||||
|
Split into two messages so each stays under Telegram's 4096-char limit.
|
||||||
|
"""
|
||||||
|
sig_name = r.final_signal.name.replace("_", " ")
|
||||||
|
|
||||||
|
# ── Part 1: header, signal, MTF, indicators ────────────────────
|
||||||
|
p1 = ["─" * WIDTH]
|
||||||
|
p1.append(
|
||||||
|
f" {r.display_name} │ Price: {r.current_price:.5f}"
|
||||||
|
f" │ Regime: {r.market_regime} │ Vol: {r.volatility_regime}"
|
||||||
|
)
|
||||||
|
p1.append(
|
||||||
|
f"\n SIGNAL: {sig_name} │ Score: {r.final_score:+.1f}"
|
||||||
|
f" │ Grade: {r.signal_quality} │ Confidence: {r.final_confidence:.0f}%"
|
||||||
|
)
|
||||||
|
p1.append(f" Action: {r.action_recommendation}")
|
||||||
|
|
||||||
|
if r.mtf_signals:
|
||||||
|
agree = "✓ ALIGNED" if r.mtf_agreement else "✗ DIVERGENT"
|
||||||
|
p1.append(f"\n Multi-Timeframe: {agree}")
|
||||||
|
for tf, sig in r.mtf_signals.items():
|
||||||
|
score = r.mtf_scores.get(tf, 0)
|
||||||
|
p1.append(f" {tf:>5}: {sig.name.replace('_', ' '):<12} ({score:+.1f})")
|
||||||
|
|
||||||
|
if r.primary_analysis:
|
||||||
|
p1.append(f"\n Indicators ({r.primary_analysis.timeframe}):")
|
||||||
|
for name, ind in r.primary_analysis.indicators.items():
|
||||||
|
if name.startswith("_") or ind.weight == 0:
|
||||||
|
continue
|
||||||
|
ind_name = f"{ind.name:<18}"
|
||||||
|
sig_str = f"{ind.signal.name.replace('_', ' '):<12}"
|
||||||
|
desc = ind.description[:60]
|
||||||
|
p1.append(f" {ind_name} {sig_str} ({ind.confidence:.0f}%) │ {desc}")
|
||||||
|
|
||||||
|
# ── Part 2: reasons, warnings, trade setup, key levels ─────────
|
||||||
|
p2 = []
|
||||||
|
|
||||||
|
if r.bullish_reasons:
|
||||||
|
p2.append(" ▲ Bullish Factors:")
|
||||||
|
for reason in r.bullish_reasons[:5]:
|
||||||
|
p2.append(f" + {reason}")
|
||||||
|
|
||||||
|
if r.bearish_reasons:
|
||||||
|
p2.append("\n ▼ Bearish Factors:")
|
||||||
|
for reason in r.bearish_reasons[:5]:
|
||||||
|
p2.append(f" - {reason}")
|
||||||
|
|
||||||
|
if r.warnings:
|
||||||
|
p2.append("\n ⚠ Warnings:")
|
||||||
|
for w in r.warnings:
|
||||||
|
p2.append(f" ! {w}")
|
||||||
|
|
||||||
|
if r.news_description and r.news_description != "Neutral news sentiment":
|
||||||
|
p2.append(f"\n 📰 News: {r.news_description}")
|
||||||
|
if r.event_description and r.event_risk != "clear":
|
||||||
|
p2.append(f" 📅 Event: {r.event_description}")
|
||||||
|
|
||||||
|
if r.trade_setup:
|
||||||
|
ts = r.trade_setup
|
||||||
|
p2 += [
|
||||||
|
"\n Trade Setup:",
|
||||||
|
f" Direction: {ts.direction}",
|
||||||
|
f" Entry: {ts.entry_price:.5f}",
|
||||||
|
f" Stop Loss: {ts.stop_loss:.5f} ({ts.risk_pips:.1f} pips)",
|
||||||
|
f" Take Profit 1: {ts.take_profit_1:.5f} ({ts.reward_pips:.1f} pips)",
|
||||||
|
f" Take Profit 2: {ts.take_profit_2:.5f}",
|
||||||
|
f" Risk:Reward: 1:{ts.risk_reward:.1f}",
|
||||||
|
]
|
||||||
|
|
||||||
|
if r.primary_analysis and r.primary_analysis.key_levels:
|
||||||
|
lvls = r.primary_analysis.key_levels
|
||||||
|
p2.append("\n Key Levels:")
|
||||||
|
for k in ("r3", "r2", "r1", "pivot", "s1", "s2", "s3"):
|
||||||
|
if k in lvls:
|
||||||
|
p2.append(f" {k.upper():>6}: {lvls[k]:.5f}")
|
||||||
|
|
||||||
|
result = ["\n".join(p1)]
|
||||||
|
if p2:
|
||||||
|
result.append("\n".join(p2))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# ── Per-chat state ─────────────────────────────────────────────────
|
# ── Per-chat state ─────────────────────────────────────────────────
|
||||||
@@ -94,274 +217,88 @@ WATCH_DEFAULT = 5
|
|||||||
class ChatState:
|
class ChatState:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.timeframe: str = "H1"
|
self.timeframe: str = "H1"
|
||||||
self.pairs: list = list(MAJOR_PAIRS)
|
|
||||||
self.watch_enabled: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
# ── HTML helpers ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _esc(text: str) -> str:
|
|
||||||
"""Escape HTML special characters for Telegram HTML parse mode."""
|
|
||||||
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
||||||
|
|
||||||
|
|
||||||
def _split_messages(lines: list[str], limit: int = 4000) -> list[str]:
|
|
||||||
"""Join lines into messages that stay under Telegram's 4096-char limit."""
|
|
||||||
messages, current, current_len = [], [], 0
|
|
||||||
for line in lines:
|
|
||||||
line_len = len(line) + 1 # +1 for newline
|
|
||||||
if current and current_len + line_len > limit:
|
|
||||||
messages.append("\n".join(current))
|
|
||||||
current, current_len = [], 0
|
|
||||||
current.append(line)
|
|
||||||
current_len += line_len
|
|
||||||
if current:
|
|
||||||
messages.append("\n".join(current))
|
|
||||||
return messages
|
|
||||||
|
|
||||||
|
|
||||||
# ── Message formatters ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _format_scan(reports: list[SignalReport], timeframe: str, title: str = "FOREX SIGNAL SCAN") -> str:
|
|
||||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
||||||
lines = [
|
|
||||||
f"<b>📊 {title} — {timeframe}</b>",
|
|
||||||
"<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>",
|
|
||||||
f"<code>🕐 {now}</code>\n",
|
|
||||||
]
|
|
||||||
|
|
||||||
for r in reports:
|
|
||||||
sig_em = SIGNAL_EMOJI.get(r.final_signal, "❔")
|
|
||||||
grade_em = GRADE_EMOJI.get(r.signal_quality, "")
|
|
||||||
sig_name = SIGNAL_NAME.get(r.final_signal, "?")
|
|
||||||
event_flag = " ⚠️" if r.event_risk == "danger" else (" ⚡" if r.event_risk == "caution" else "")
|
|
||||||
|
|
||||||
lines.append(
|
|
||||||
f"{sig_em} <b>{_esc(r.display_name)}</b> "
|
|
||||||
f"{grade_em}<code>{r.signal_quality}</code> "
|
|
||||||
f"<code>{_esc(sig_name):<11}</code> "
|
|
||||||
f"<code>{r.final_confidence:.0f}%</code>{event_flag}"
|
|
||||||
)
|
|
||||||
|
|
||||||
lines += ["", "<i>Use /pair EURUSD for detailed analysis</i>"]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def _format_pair_detail(r: SignalReport) -> list[str]:
|
|
||||||
"""
|
|
||||||
Return a list of message strings for a single pair.
|
|
||||||
Split across messages so each stays under Telegram's 4096-char limit.
|
|
||||||
"""
|
|
||||||
sig_em = SIGNAL_EMOJI.get(r.final_signal, "❔")
|
|
||||||
grade_em = GRADE_EMOJI.get(r.signal_quality, "")
|
|
||||||
sig_name = SIGNAL_NAME.get(r.final_signal, "?")
|
|
||||||
|
|
||||||
# ── Message 1: Header, MTF, Trade Setup, Key Levels ───────────
|
|
||||||
m1 = [
|
|
||||||
f"{sig_em} <b>{_esc(r.display_name)}</b> — <b>{_esc(sig_name)}</b>",
|
|
||||||
"<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>",
|
|
||||||
f"<b>Price:</b> <code>{r.current_price:.5f}</code>",
|
|
||||||
f"<b>Regime:</b> {_esc(r.market_regime)} <b>Vol:</b> {_esc(r.volatility_regime)}",
|
|
||||||
"",
|
|
||||||
f"<b>Score:</b> <code>{r.final_score:+.1f}</code> "
|
|
||||||
f"<b>Grade:</b> {grade_em}<code>{r.signal_quality}</code> "
|
|
||||||
f"<b>Conf:</b> <code>{r.final_confidence:.0f}%</code>",
|
|
||||||
f"<b>Action:</b> {_esc(r.action_recommendation)}",
|
|
||||||
]
|
|
||||||
|
|
||||||
if r.event_risk != "clear":
|
|
||||||
icon = "⚠️" if r.event_risk == "danger" else "⚡"
|
|
||||||
m1.append(f"{icon} <b>Event:</b> {_esc(r.event_description)}")
|
|
||||||
|
|
||||||
if r.news_description and r.news_description != "Neutral news sentiment":
|
|
||||||
m1.append(f"📰 <b>News:</b> {_esc(r.news_description)}")
|
|
||||||
|
|
||||||
# Multi-timeframe
|
|
||||||
if r.mtf_signals:
|
|
||||||
agree = "✅ ALIGNED" if r.mtf_agreement else "⚡ DIVERGENT"
|
|
||||||
m1 += ["", f"<b>📊 Multi-Timeframe:</b> {agree}"]
|
|
||||||
for tf, sig in r.mtf_signals.items():
|
|
||||||
score = r.mtf_scores.get(tf, 0)
|
|
||||||
m1.append(
|
|
||||||
f" <code>{tf:>5}</code> {SIGNAL_EMOJI.get(sig, '?')} "
|
|
||||||
f"<code>{score:+.1f}</code>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Trade setup
|
|
||||||
if r.trade_setup:
|
|
||||||
ts = r.trade_setup
|
|
||||||
dir_em = "🟢" if ts.direction == "BUY" else "🔴"
|
|
||||||
m1 += [
|
|
||||||
"",
|
|
||||||
"<b>💼 Trade Setup</b>",
|
|
||||||
f" <b>Direction:</b> {dir_em} {ts.direction}",
|
|
||||||
f" <b>Entry:</b> <code>{ts.entry_price:.5f}</code>",
|
|
||||||
f" <b>Stop Loss:</b> <code>{ts.stop_loss:.5f}</code> ({ts.risk_pips:.1f} pips)",
|
|
||||||
f" <b>Take Profit 1:</b> <code>{ts.take_profit_1:.5f}</code> ({ts.reward_pips:.1f} pips)",
|
|
||||||
f" <b>Take Profit 2:</b> <code>{ts.take_profit_2:.5f}</code>",
|
|
||||||
f" <b>Risk:Reward:</b> 1:{ts.risk_reward:.1f}",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Key levels
|
|
||||||
if r.primary_analysis and r.primary_analysis.key_levels:
|
|
||||||
lvls = r.primary_analysis.key_levels
|
|
||||||
shown = [(k, lvls[k]) for k in ("r3", "r2", "r1", "pivot", "s1", "s2", "s3") if k in lvls]
|
|
||||||
if shown:
|
|
||||||
m1 += ["", "<b>📍 Key Levels:</b>"]
|
|
||||||
for k, v in shown:
|
|
||||||
arrow = "🔴" if v > r.current_price else "🟢"
|
|
||||||
m1.append(f" {arrow} <code>{k.upper():>6}:</code> <code>{v:.5f}</code>")
|
|
||||||
|
|
||||||
# ── Message 2: Indicators + Reasons ───────────────────────────
|
|
||||||
m2 = []
|
|
||||||
|
|
||||||
if r.primary_analysis:
|
|
||||||
ind_lines = []
|
|
||||||
for name, ind in r.primary_analysis.indicators.items():
|
|
||||||
if name.startswith("_") or ind.weight == 0:
|
|
||||||
continue
|
|
||||||
em = SIGNAL_EMOJI.get(ind.signal, "↔")
|
|
||||||
# Trim long descriptions to keep message readable
|
|
||||||
desc = _esc(ind.description[:90])
|
|
||||||
ind_lines.append(
|
|
||||||
f" {em} <b>{_esc(ind.name)}</b> <code>{ind.confidence:.0f}%</code>"
|
|
||||||
f"\n <i>{desc}</i>"
|
|
||||||
)
|
|
||||||
if ind_lines:
|
|
||||||
m2 += ["<b>📉 Indicators:</b>"] + ind_lines
|
|
||||||
|
|
||||||
if r.bullish_reasons:
|
|
||||||
if m2:
|
|
||||||
m2.append("")
|
|
||||||
m2.append("<b>🟢 Bullish Factors:</b>")
|
|
||||||
for reason in r.bullish_reasons[:5]:
|
|
||||||
m2.append(f" ▲ {_esc(reason)}")
|
|
||||||
|
|
||||||
if r.bearish_reasons:
|
|
||||||
if m2:
|
|
||||||
m2.append("")
|
|
||||||
m2.append("<b>🔴 Bearish Factors:</b>")
|
|
||||||
for reason in r.bearish_reasons[:5]:
|
|
||||||
m2.append(f" ▼ {_esc(reason)}")
|
|
||||||
|
|
||||||
if r.warnings:
|
|
||||||
if m2:
|
|
||||||
m2.append("")
|
|
||||||
m2.append("<b>⚠️ Warnings:</b>")
|
|
||||||
for w in r.warnings:
|
|
||||||
m2.append(f" ! {_esc(w)}")
|
|
||||||
|
|
||||||
result = _split_messages(m1)
|
|
||||||
if m2:
|
|
||||||
result += _split_messages(m2)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════
|
||||||
# BOT CLASS
|
# BOT
|
||||||
# ══════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
class ForexTelegramBot:
|
class ForexTelegramBot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._chat_states: Dict[int, ChatState] = {}
|
self._states: Dict[int, ChatState] = {}
|
||||||
self._executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="forex")
|
self._executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="forex")
|
||||||
|
|
||||||
def _state(self, chat_id: int) -> ChatState:
|
def _state(self, chat_id: int) -> ChatState:
|
||||||
if chat_id not in self._chat_states:
|
if chat_id not in self._states:
|
||||||
self._chat_states[chat_id] = ChatState()
|
self._states[chat_id] = ChatState()
|
||||||
return self._chat_states[chat_id]
|
return self._states[chat_id]
|
||||||
|
|
||||||
def _build_config(self, state: ChatState) -> PlatformConfig:
|
def _build_config(self, tf: str, pairs: list = None) -> PlatformConfig:
|
||||||
config = PlatformConfig()
|
config = PlatformConfig()
|
||||||
config.primary_timeframe = TF_MAP[state.timeframe]
|
config.primary_timeframe = TF_MAP[tf]
|
||||||
config.confirmation_timeframes = TF_CONFIRMATIONS.get(
|
config.confirmation_timeframes = TF_CONFIRMATIONS.get(tf, [Timeframe.M15, Timeframe.H4])
|
||||||
state.timeframe, [Timeframe.M15, Timeframe.H4]
|
config.watch_pairs = list(pairs or MAJOR_PAIRS)
|
||||||
)
|
|
||||||
config.watch_pairs = list(state.pairs)
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
# ── Blocking analysis (run in thread pool) ─────────────────────
|
def _run_scan(self, tf: str, pairs: list) -> List[SignalReport]:
|
||||||
|
return SignalAggregator(self._build_config(tf, pairs)).analyze_all_pairs()
|
||||||
def _run_scan(self, state: ChatState) -> list[SignalReport]:
|
|
||||||
config = self._build_config(state)
|
|
||||||
return SignalAggregator(config).analyze_all_pairs()
|
|
||||||
|
|
||||||
def _run_pair(self, symbol: str, state: ChatState) -> Optional[SignalReport]:
|
|
||||||
config = self._build_config(state)
|
|
||||||
config.watch_pairs = [symbol]
|
|
||||||
reports = SignalAggregator(config).analyze_all_pairs()
|
|
||||||
return reports[0] if reports else None
|
|
||||||
|
|
||||||
async def _analyze(self, func, *args):
|
async def _analyze(self, func, *args):
|
||||||
"""Run a blocking analysis function in the thread pool."""
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
return await loop.run_in_executor(self._executor, func, *args)
|
return await loop.run_in_executor(self._executor, func, *args)
|
||||||
|
|
||||||
# ── Parse helpers ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_tf(args: list[str]) -> Optional[str]:
|
def _parse_tf(args: list) -> Optional[str]:
|
||||||
"""Return the first arg that is a valid timeframe key, or None."""
|
|
||||||
for a in args:
|
for a in args:
|
||||||
if a.upper() in TF_MAP:
|
if a.upper() in TF_MAP:
|
||||||
return a.upper()
|
return a.upper()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _send_pre(update: Update, text: str):
|
||||||
|
"""Send text as a monospace pre block, splitting if over 4096 chars."""
|
||||||
|
LIMIT = 3800 # leave room for <pre> tags
|
||||||
|
chunks = []
|
||||||
|
while text:
|
||||||
|
if len(text) <= LIMIT:
|
||||||
|
chunks.append(text)
|
||||||
|
break
|
||||||
|
# Split at last newline before limit
|
||||||
|
split_at = text.rfind("\n", 0, LIMIT)
|
||||||
|
if split_at == -1:
|
||||||
|
split_at = LIMIT
|
||||||
|
chunks.append(text[:split_at])
|
||||||
|
text = text[split_at:].lstrip("\n")
|
||||||
|
for chunk in chunks:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"<pre>{chunk}</pre>", parse_mode=ParseMode.HTML
|
||||||
|
)
|
||||||
|
|
||||||
# ── Commands ───────────────────────────────────────────────────
|
# ── Commands ───────────────────────────────────────────────────
|
||||||
|
|
||||||
async def cmd_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def cmd_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
text = (
|
await self.cmd_help(update, context)
|
||||||
"<b>🌐 Forex Signal Intelligence Platform</b>\n"
|
|
||||||
"<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>\n\n"
|
|
||||||
"Multi-layer technical + fundamental signal analysis across major Forex pairs.\n\n"
|
|
||||||
"<b>Commands:</b>\n"
|
|
||||||
" /scan — Scan all major pairs\n"
|
|
||||||
" /signals — Actionable signals only (grade B+)\n"
|
|
||||||
" /pair EURUSD — Detailed single-pair analysis\n"
|
|
||||||
" /all — Include cross pairs in scan\n"
|
|
||||||
" /tf H1 — Set default timeframe\n"
|
|
||||||
" /watch — Periodic scan alerts (every 5 min)\n"
|
|
||||||
" /unwatch — Disable alerts\n"
|
|
||||||
" /status — Current settings\n"
|
|
||||||
" /help — Full command reference\n\n"
|
|
||||||
"<i>⚠️ Educational/research use only. Not financial advice.</i>"
|
|
||||||
)
|
|
||||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
text = (
|
|
||||||
"<b>📖 Command Reference</b>\n\n"
|
|
||||||
"<b>/scan</b> — Scan all major pairs (uses default TF)\n"
|
|
||||||
"<b>/scan H4</b> — Scan with a specific timeframe\n\n"
|
|
||||||
"<b>/signals</b> — Grade B+ signals only\n"
|
|
||||||
"<b>/signals H4</b> — With a specific timeframe\n\n"
|
|
||||||
"<b>/pair EURUSD</b> — Detailed analysis (uses default TF)\n"
|
|
||||||
"<b>/pair EURUSD H4</b> — With a specific timeframe\n\n"
|
|
||||||
"<b>/all</b> — Scan major + cross pairs\n"
|
|
||||||
"<b>/all H4</b> — With a specific timeframe\n\n"
|
|
||||||
"<b>/tf H1</b> — Set your default timeframe\n"
|
|
||||||
" Options: <code>M5 M15 M30 H1 H4 D1</code>\n\n"
|
|
||||||
"<b>/watch</b> — Alert every 5 min (actionable signals only)\n"
|
|
||||||
"<b>/watch 15</b> — Alert every 15 min (5–120 min range)\n"
|
|
||||||
"<b>/unwatch</b> — Stop alerts\n\n"
|
|
||||||
"<b>/status</b> — View current settings\n\n"
|
|
||||||
"<b>Signal key:</b>\n"
|
|
||||||
" 🟢⬆⬆ Strong Buy 🟢⬆ Buy ⬜↔ Neutral\n"
|
|
||||||
" 🔴⬇ Sell 🔴⬇⬇ Strong Sell\n\n"
|
|
||||||
"<b>Grade key:</b>\n"
|
|
||||||
" A+ ⭐⭐ A ⭐ B 🔵 C 🟡 D 🟠 F ⚫"
|
|
||||||
)
|
|
||||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
async def cmd_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
state = self._state(update.effective_chat.id)
|
state = self._state(update.effective_chat.id)
|
||||||
pairs_display = ", ".join(SYMBOL_DISPLAY.get(p, p) for p in state.pairs)
|
|
||||||
watch_str = "✅ enabled" if state.watch_enabled else "❌ disabled"
|
|
||||||
text = (
|
text = (
|
||||||
"<b>⚙️ Current Settings</b>\n\n"
|
"<b>🌐 Forex Signal Intelligence Platform</b>\n\n"
|
||||||
f"<b>Timeframe:</b> {state.timeframe}\n"
|
"<b>Commands:</b>\n"
|
||||||
f"<b>Pairs:</b> {_esc(pairs_display)}\n"
|
" /scan — Scan all major pairs\n"
|
||||||
f"<b>Watch alerts:</b> {watch_str}"
|
" /scan H4 — Scan with specific timeframe\n"
|
||||||
|
" /pair EURUSD — Detailed analysis\n"
|
||||||
|
" /pair EURUSD H4 — With specific timeframe\n"
|
||||||
|
" /tf H1 — Set default timeframe\n"
|
||||||
|
" /help — This message\n\n"
|
||||||
|
"<b>Timeframes:</b>\n"
|
||||||
|
" <code>M5 </code> — 5 minutes\n"
|
||||||
|
" <code>M15</code> — 15 minutes\n"
|
||||||
|
" <code>M30</code> — 30 minutes\n"
|
||||||
|
" <code>H1 </code> — 1 hour ← recommended for intraday\n"
|
||||||
|
" <code>H4 </code> — 4 hours ← recommended for swing trading\n"
|
||||||
|
" <code>D1 </code> — Daily ← recommended for position trading\n\n"
|
||||||
|
f"Current default: <b>{state.timeframe}</b>\n\n"
|
||||||
|
"<i>⚠️ Educational use only. Not financial advice.</i>"
|
||||||
)
|
)
|
||||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
|
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
@@ -370,7 +307,8 @@ class ForexTelegramBot:
|
|||||||
if not tf:
|
if not tf:
|
||||||
opts = " ".join(TF_MAP.keys())
|
opts = " ".join(TF_MAP.keys())
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
f"Usage: <b>/tf <timeframe></b>\nOptions: <code>{opts}</code>",
|
f"Usage: <b>/tf <timeframe></b>\n"
|
||||||
|
f"Options: <code>{opts}</code>",
|
||||||
parse_mode=ParseMode.HTML,
|
parse_mode=ParseMode.HTML,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -382,45 +320,42 @@ class ForexTelegramBot:
|
|||||||
async def cmd_scan(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def cmd_scan(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
state = self._state(chat_id)
|
state = self._state(chat_id)
|
||||||
|
tf = self._parse_tf(context.args or []) or state.timeframe
|
||||||
|
|
||||||
# Allow inline TF override: /scan H4
|
msg = await update.message.reply_text(f"🔄 Scanning markets ({tf})…")
|
||||||
scan_state = ChatState()
|
|
||||||
scan_state.timeframe = self._parse_tf(context.args or []) or state.timeframe
|
|
||||||
scan_state.pairs = list(state.pairs)
|
|
||||||
|
|
||||||
msg = await update.message.reply_text("🔄 Scanning markets, please wait…")
|
|
||||||
try:
|
try:
|
||||||
reports = await self._analyze(self._run_scan, scan_state)
|
reports = await self._analyze(self._run_scan, tf, list(MAJOR_PAIRS))
|
||||||
text = _format_scan(reports, scan_state.timeframe)
|
|
||||||
await msg.edit_text(text, parse_mode=ParseMode.HTML)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Scan error: {e}")
|
|
||||||
await msg.edit_text(f"❌ Scan failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
async def cmd_signals(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
# ── summary block (header + table + calendar) ──────────
|
||||||
chat_id = update.effective_chat.id
|
summary = (
|
||||||
state = self._state(chat_id)
|
_format_header(tf)
|
||||||
|
+ _format_table(reports)
|
||||||
|
+ _format_calendar(reports)
|
||||||
|
)
|
||||||
|
await msg.edit_text(f"<pre>{summary}</pre>", parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
scan_state = ChatState()
|
# ── detailed block for each actionable signal ──────────
|
||||||
scan_state.timeframe = self._parse_tf(context.args or []) or state.timeframe
|
|
||||||
scan_state.pairs = list(state.pairs)
|
|
||||||
|
|
||||||
msg = await update.message.reply_text("🔄 Scanning for signals…")
|
|
||||||
try:
|
|
||||||
reports = await self._analyze(self._run_scan, scan_state)
|
|
||||||
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
|
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
|
||||||
if actionable:
|
if actionable:
|
||||||
text = _format_scan(actionable, scan_state.timeframe, title="🎯 ACTIONABLE SIGNALS")
|
header_line = "\n ═══ ACTIONABLE SIGNALS (Grade B or above) ═══\n"
|
||||||
|
first = True
|
||||||
|
for r in actionable:
|
||||||
|
parts = _format_detail(r)
|
||||||
|
if first:
|
||||||
|
parts[0] = header_line + parts[0]
|
||||||
|
first = False
|
||||||
|
for part in parts:
|
||||||
|
await self._send_pre(update, part)
|
||||||
else:
|
else:
|
||||||
text = (
|
await self._send_pre(
|
||||||
f"<b>🎯 ACTIONABLE SIGNALS — {scan_state.timeframe}</b>\n\n"
|
update,
|
||||||
"No grade B+ signals right now.\n"
|
"\n No high-grade signals detected.\n"
|
||||||
"Use /scan to see all pairs."
|
" Use /scan with a different timeframe to see more.\n",
|
||||||
)
|
)
|
||||||
await msg.edit_text(text, parse_mode=ParseMode.HTML)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Signals error: {e}")
|
logger.error(f"Scan error: {e}")
|
||||||
await msg.edit_text(f"❌ Scan failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
|
await msg.edit_text(f"❌ Scan failed: {e}")
|
||||||
|
|
||||||
async def cmd_pair(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def cmd_pair(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
@@ -438,127 +373,26 @@ class ForexTelegramBot:
|
|||||||
raw = args[0].upper().replace("/", "").replace("_", "")
|
raw = args[0].upper().replace("/", "").replace("_", "")
|
||||||
if not raw.endswith("=X"):
|
if not raw.endswith("=X"):
|
||||||
raw += "=X"
|
raw += "=X"
|
||||||
|
tf = self._parse_tf(args[1:]) or state.timeframe
|
||||||
pair_state = ChatState()
|
|
||||||
pair_state.timeframe = self._parse_tf(args[1:]) or state.timeframe
|
|
||||||
|
|
||||||
display = SYMBOL_DISPLAY.get(raw, raw)
|
display = SYMBOL_DISPLAY.get(raw, raw)
|
||||||
msg = await update.message.reply_text(f"🔄 Analyzing {_esc(display)}…")
|
|
||||||
|
msg = await update.message.reply_text(f"🔄 Analyzing {display} ({tf})…")
|
||||||
try:
|
try:
|
||||||
report = await self._analyze(self._run_pair, raw, pair_state)
|
reports = await self._analyze(self._run_scan, tf, [raw])
|
||||||
if report is None:
|
if not reports:
|
||||||
await msg.edit_text(
|
await msg.edit_text(f"❌ No data returned for {raw}")
|
||||||
f"❌ No data returned for <code>{_esc(raw)}</code>.",
|
|
||||||
parse_mode=ParseMode.HTML,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
messages = _format_pair_detail(report)
|
|
||||||
await msg.edit_text(messages[0], parse_mode=ParseMode.HTML)
|
r = reports[0]
|
||||||
for m in messages[1:]:
|
summary = _format_header(tf) + _format_table([r])
|
||||||
await update.message.reply_text(m, parse_mode=ParseMode.HTML)
|
await msg.edit_text(f"<pre>{summary}</pre>", parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
for part in _format_detail(r):
|
||||||
|
await self._send_pre(update, part)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Pair analysis error for {raw}: {e}")
|
logger.error(f"Pair error for {raw}: {e}")
|
||||||
await msg.edit_text(f"❌ Analysis failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
|
await msg.edit_text(f"❌ Analysis failed: {e}")
|
||||||
|
|
||||||
async def cmd_all(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
chat_id = update.effective_chat.id
|
|
||||||
state = self._state(chat_id)
|
|
||||||
|
|
||||||
all_state = ChatState()
|
|
||||||
all_state.timeframe = self._parse_tf(context.args or []) or state.timeframe
|
|
||||||
all_state.pairs = list(MAJOR_PAIRS) + list(CROSS_PAIRS)
|
|
||||||
|
|
||||||
msg = await update.message.reply_text("🔄 Scanning all pairs (major + cross)…")
|
|
||||||
try:
|
|
||||||
reports = await self._analyze(self._run_scan, all_state)
|
|
||||||
text = _format_scan(reports, all_state.timeframe, title="FULL PAIR SCAN")
|
|
||||||
await msg.edit_text(text, parse_mode=ParseMode.HTML)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"All-pairs scan error: {e}")
|
|
||||||
await msg.edit_text(f"❌ Scan failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
async def cmd_watch(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
chat_id = update.effective_chat.id
|
|
||||||
state = self._state(chat_id)
|
|
||||||
|
|
||||||
interval_minutes = WATCH_DEFAULT
|
|
||||||
if context.args:
|
|
||||||
try:
|
|
||||||
interval_minutes = int(context.args[0])
|
|
||||||
interval_minutes = max(WATCH_MIN_INTERVAL, min(WATCH_MAX_INTERVAL, interval_minutes))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Remove any existing watch job for this chat
|
|
||||||
for job in context.job_queue.get_jobs_by_name(f"watch_{chat_id}"):
|
|
||||||
job.schedule_removal()
|
|
||||||
|
|
||||||
state.watch_enabled = True
|
|
||||||
context.job_queue.run_repeating(
|
|
||||||
self._watch_job,
|
|
||||||
interval=interval_minutes * 60,
|
|
||||||
first=5,
|
|
||||||
chat_id=chat_id,
|
|
||||||
name=f"watch_{chat_id}",
|
|
||||||
)
|
|
||||||
await update.message.reply_text(
|
|
||||||
f"✅ Watch alerts enabled — scanning every <b>{interval_minutes} min</b>.\n"
|
|
||||||
"Alerts fire when grade B+ signals appear. Use /unwatch to stop.",
|
|
||||||
parse_mode=ParseMode.HTML,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def cmd_unwatch(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
chat_id = update.effective_chat.id
|
|
||||||
self._state(chat_id).watch_enabled = False
|
|
||||||
for job in context.job_queue.get_jobs_by_name(f"watch_{chat_id}"):
|
|
||||||
job.schedule_removal()
|
|
||||||
await update.message.reply_text("❌ Watch alerts disabled.")
|
|
||||||
|
|
||||||
async def cmd_credits(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Show API credit usage against the free-tier daily limit."""
|
|
||||||
from data_provider import ForexDataProvider
|
|
||||||
config = self._build_config(self._state(update.effective_chat.id))
|
|
||||||
provider = ForexDataProvider(config)
|
|
||||||
used = provider._daily_credits
|
|
||||||
limit = provider.DAILY_LIMIT
|
|
||||||
remaining = limit - used
|
|
||||||
pct = (used / limit) * 100 if limit else 0
|
|
||||||
bar_filled = int(pct / 5)
|
|
||||||
bar = "█" * bar_filled + "░" * (20 - bar_filled)
|
|
||||||
status = "🟢" if pct < 60 else ("🟡" if pct < 80 else "🔴")
|
|
||||||
text = (
|
|
||||||
"<b>📊 API Credit Usage</b>\n\n"
|
|
||||||
f"<code>{bar}</code> {status}\n"
|
|
||||||
f"<b>Used:</b> {used} / {limit} credits\n"
|
|
||||||
f"<b>Remaining:</b> {remaining} credits\n\n"
|
|
||||||
"<i>Free tier: 8 credits/min, 800 credits/day\n"
|
|
||||||
"Each full scan uses ~21 credits (7 pairs × 3 timeframes)\n"
|
|
||||||
"≈ 38 scans available per day</i>"
|
|
||||||
)
|
|
||||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
# ── Watch job ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async def _watch_job(self, context: CallbackContext):
|
|
||||||
chat_id = context.job.chat_id
|
|
||||||
state = self._state(chat_id)
|
|
||||||
if not state.watch_enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
reports = await self._analyze(self._run_scan, state)
|
|
||||||
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
|
|
||||||
if actionable:
|
|
||||||
text = (
|
|
||||||
"🔔 <b>SIGNAL ALERT</b>\n\n"
|
|
||||||
+ _format_scan(actionable, state.timeframe, title="ACTIONABLE SIGNALS")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
now = datetime.now(timezone.utc).strftime("%H:%M UTC")
|
|
||||||
text = f"📊 <b>Watch scan {now}</b> — No grade B+ signals detected."
|
|
||||||
await context.bot.send_message(chat_id, text, parse_mode=ParseMode.HTML)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Watch job error for chat {chat_id}: {e}")
|
|
||||||
|
|
||||||
# ── Application builder ────────────────────────────────────────
|
# ── Application builder ────────────────────────────────────────
|
||||||
|
|
||||||
@@ -572,18 +406,11 @@ class ForexTelegramBot:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _register_commands(app: Application) -> None:
|
async def _register_commands(app: Application) -> None:
|
||||||
"""Register commands with Telegram so they appear in the / menu."""
|
|
||||||
await app.bot.set_my_commands([
|
await app.bot.set_my_commands([
|
||||||
BotCommand("scan", "Scan all major pairs"),
|
BotCommand("scan", "Scan all major pairs"),
|
||||||
BotCommand("signals", "Actionable signals only (grade B+)"),
|
BotCommand("pair", "Detailed pair analysis — /pair EURUSD"),
|
||||||
BotCommand("pair", "Detailed pair analysis — /pair EURUSD"),
|
BotCommand("tf", "Set default timeframe — /tf H4"),
|
||||||
BotCommand("all", "Scan major + cross pairs"),
|
BotCommand("help", "Commands and timeframe guide"),
|
||||||
BotCommand("tf", "Set timeframe — /tf H4"),
|
|
||||||
BotCommand("watch", "Enable periodic alerts"),
|
|
||||||
BotCommand("unwatch", "Disable alerts"),
|
|
||||||
BotCommand("credits", "View API credit usage"),
|
|
||||||
BotCommand("status", "View current settings"),
|
|
||||||
BotCommand("help", "Full command reference"),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
app = (
|
app = (
|
||||||
@@ -593,17 +420,11 @@ class ForexTelegramBot:
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_handler(CommandHandler("start", self.cmd_start))
|
app.add_handler(CommandHandler("start", self.cmd_start))
|
||||||
app.add_handler(CommandHandler("help", self.cmd_help))
|
app.add_handler(CommandHandler("help", self.cmd_help))
|
||||||
app.add_handler(CommandHandler("status", self.cmd_status))
|
app.add_handler(CommandHandler("tf", self.cmd_tf))
|
||||||
app.add_handler(CommandHandler("tf", self.cmd_tf))
|
app.add_handler(CommandHandler("scan", self.cmd_scan))
|
||||||
app.add_handler(CommandHandler("scan", self.cmd_scan))
|
app.add_handler(CommandHandler("pair", self.cmd_pair))
|
||||||
app.add_handler(CommandHandler("signals", self.cmd_signals))
|
|
||||||
app.add_handler(CommandHandler("pair", self.cmd_pair))
|
|
||||||
app.add_handler(CommandHandler("all", self.cmd_all))
|
|
||||||
app.add_handler(CommandHandler("watch", self.cmd_watch))
|
|
||||||
app.add_handler(CommandHandler("unwatch", self.cmd_unwatch))
|
|
||||||
app.add_handler(CommandHandler("credits", self.cmd_credits))
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
@@ -616,10 +437,8 @@ def main():
|
|||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
datefmt="%H:%M:%S",
|
datefmt="%H:%M:%S",
|
||||||
)
|
)
|
||||||
|
|
||||||
bot = ForexTelegramBot()
|
bot = ForexTelegramBot()
|
||||||
app = bot.build_application()
|
app = bot.build_application()
|
||||||
|
|
||||||
print("🤖 Forex Signal Bot started. Press Ctrl+C to stop.")
|
print("🤖 Forex Signal Bot started. Press Ctrl+C to stop.")
|
||||||
app.run_polling(drop_pending_updates=True)
|
app.run_polling(drop_pending_updates=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user