Live path: config-driven multi-pair scanning (Telegram bot + CLI)
- telegram_bot: SCAN_PAIRS/_WS_TO_PAIR/SCAN_BUTTONS all derive from PlatformConfig.scan_pairs (the single pair on/off switch); buttons and keyboards are generated per pair; _run_signal_sync builds per-pair configs via pair_signals factories (was: XAU defaults for any symbol); scan-all works per pair; PB badge matches any *_PB tag; daily-rotating file logs in logs/telegram.log (30-day retention) - main.py: --pair argument (XAUUSD | EURUSD + aliases) routed through pair_signals factories; price display is precision-aware (2dp gold, 5dp FX) scan_pairs default stays [XAUUSD] until the EUR M5 re-tune validates.
This commit is contained in:
@@ -21,7 +21,8 @@ if hasattr(sys.stdout, "reconfigure"):
|
|||||||
if hasattr(sys.stderr, "reconfigure"):
|
if hasattr(sys.stderr, "reconfigure"):
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
from config import PlatformConfig, Timeframe, SignalStrength, XAUUSD
|
from config import PlatformConfig, Timeframe, SignalStrength, XAUUSD, EURUSD
|
||||||
|
from pair_signals import PAIR_PRODUCERS
|
||||||
from signal_aggregator import SignalAggregator, SignalReport
|
from signal_aggregator import SignalAggregator, SignalReport
|
||||||
|
|
||||||
|
|
||||||
@@ -97,6 +98,11 @@ def _compact_reason(reason: str) -> str:
|
|||||||
return s.strip()
|
return s.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_price(price: float) -> str:
|
||||||
|
"""2dp for gold-scale prices, 5dp for FX-scale (EUR/USD etc.)."""
|
||||||
|
return f"{price:.2f}" if price >= 10 else f"{price:.5f}"
|
||||||
|
|
||||||
|
|
||||||
def print_signal_report(report: SignalReport, detail: bool = False):
|
def print_signal_report(report: SignalReport, detail: bool = False):
|
||||||
sc = signal_color(report.final_signal)
|
sc = signal_color(report.final_signal)
|
||||||
qc = quality_color(report.signal_quality)
|
qc = quality_color(report.signal_quality)
|
||||||
@@ -116,16 +122,16 @@ def print_signal_report(report: SignalReport, detail: bool = False):
|
|||||||
ts = report.trade_setup
|
ts = report.trade_setup
|
||||||
dc = Colors.GREEN if ts.direction == "BUY" else Colors.RED
|
dc = Colors.GREEN if ts.direction == "BUY" else Colors.RED
|
||||||
print(f"\n {Colors.BOLD}Setup{Colors.RESET} "
|
print(f"\n {Colors.BOLD}Setup{Colors.RESET} "
|
||||||
f"{dc}{ts.direction}{Colors.RESET} @ {Colors.BOLD}{ts.entry_price:.3f}{Colors.RESET} "
|
f"{dc}{ts.direction}{Colors.RESET} @ {Colors.BOLD}{_fmt_price(ts.entry_price)}{Colors.RESET} "
|
||||||
f"→ SL {Colors.RED}{ts.stop_loss:.3f}{Colors.RESET} "
|
f"→ SL {Colors.RED}{_fmt_price(ts.stop_loss)}{Colors.RESET} "
|
||||||
f"({ts.risk_pips:.1f}p) "
|
f"({ts.risk_pips:.1f}p) "
|
||||||
f"→ TP {Colors.GREEN}{ts.take_profit_1:.3f}{Colors.RESET} "
|
f"→ TP {Colors.GREEN}{_fmt_price(ts.take_profit_1)}{Colors.RESET} "
|
||||||
f"({ts.reward_pips:.1f}p) "
|
f"({ts.reward_pips:.1f}p) "
|
||||||
f"RR 1:{ts.risk_reward:.1f}")
|
f"RR 1:{ts.risk_reward:.1f}")
|
||||||
|
|
||||||
# ── 3. CONTEXT (price · regime · vol · MTF breakdown) ──────────
|
# ── 3. CONTEXT (price · regime · vol · MTF breakdown) ──────────
|
||||||
print(f"\n {Colors.WHITE}{report.display_name}{Colors.RESET} "
|
print(f"\n {Colors.WHITE}{report.display_name}{Colors.RESET} "
|
||||||
f"{Colors.BOLD}{report.current_price:.3f}{Colors.RESET} · "
|
f"{Colors.BOLD}{_fmt_price(report.current_price)}{Colors.RESET} · "
|
||||||
f"{Colors.CYAN}{report.market_regime}{Colors.RESET} · "
|
f"{Colors.CYAN}{report.market_regime}{Colors.RESET} · "
|
||||||
f"vol {report.volatility_regime}")
|
f"vol {report.volatility_regime}")
|
||||||
|
|
||||||
@@ -250,6 +256,8 @@ Examples:
|
|||||||
python main.py --json JSON output
|
python main.py --json JSON output
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
parser.add_argument("--pair", "-p", default="XAUUSD",
|
||||||
|
help="Pair to analyze: XAUUSD | EURUSD (default: XAUUSD)")
|
||||||
parser.add_argument("--timeframe", "-tf", default="H1",
|
parser.add_argument("--timeframe", "-tf", default="H1",
|
||||||
choices=["M5", "M15", "M30", "H1", "H4", "D1"],
|
choices=["M5", "M15", "M30", "H1", "H4", "D1"],
|
||||||
help="Primary analysis timeframe (default: H1)")
|
help="Primary analysis timeframe (default: H1)")
|
||||||
@@ -263,14 +271,25 @@ Examples:
|
|||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_pair(s: str) -> str:
|
||||||
|
aliases = {
|
||||||
|
"XAUUSD": XAUUSD, "XAU/USD": XAUUSD, "GOLD": XAUUSD,
|
||||||
|
"EURUSD": EURUSD, "EUR/USD": EURUSD, "EUR": EURUSD,
|
||||||
|
}
|
||||||
|
key = s.upper().strip()
|
||||||
|
if key in aliases:
|
||||||
|
return aliases[key]
|
||||||
|
raise SystemExit(f"Unknown pair '{s}'. Try one of: {', '.join(sorted(aliases))}")
|
||||||
|
|
||||||
|
|
||||||
def build_config(args) -> PlatformConfig:
|
def build_config(args) -> PlatformConfig:
|
||||||
config = PlatformConfig()
|
pair = _resolve_pair(args.pair)
|
||||||
|
|
||||||
tf_map = {
|
tf_map = {
|
||||||
"M5": Timeframe.M5, "M15": Timeframe.M15, "M30": Timeframe.M30,
|
"M5": Timeframe.M5, "M15": Timeframe.M15, "M30": Timeframe.M30,
|
||||||
"H1": Timeframe.H1, "H4": Timeframe.H4, "D1": Timeframe.D1,
|
"H1": Timeframe.H1, "H4": Timeframe.H4, "D1": Timeframe.D1,
|
||||||
}
|
}
|
||||||
config.primary_timeframe = tf_map[args.timeframe]
|
primary_tf = tf_map[args.timeframe]
|
||||||
|
|
||||||
tf_confirmations = {
|
tf_confirmations = {
|
||||||
"M5": [Timeframe.M1, Timeframe.M15],
|
"M5": [Timeframe.M1, Timeframe.M15],
|
||||||
@@ -280,23 +299,31 @@ def build_config(args) -> PlatformConfig:
|
|||||||
"H4": [Timeframe.H1, Timeframe.D1],
|
"H4": [Timeframe.H1, Timeframe.D1],
|
||||||
"D1": [Timeframe.H4, Timeframe.W1],
|
"D1": [Timeframe.H4, Timeframe.W1],
|
||||||
}
|
}
|
||||||
config.confirmation_timeframes = tf_confirmations.get(
|
confirmation_tfs = tf_confirmations.get(
|
||||||
args.timeframe, [Timeframe.M15, Timeframe.H4]
|
args.timeframe, [Timeframe.M15, Timeframe.H4]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Per-pair tuned configs from the pair_signals factories.
|
||||||
|
factory = PAIR_PRODUCERS.get(pair)
|
||||||
|
if factory is not None:
|
||||||
|
return factory(primary_tf, confirmation_tfs)
|
||||||
|
|
||||||
|
config = PlatformConfig()
|
||||||
|
config.primary_timeframe = primary_tf
|
||||||
|
config.confirmation_timeframes = confirmation_tfs
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def run_live(args, config: PlatformConfig):
|
def run_live(args, config: PlatformConfig):
|
||||||
print(f"{Colors.GRAY} Fetching market data and running analysis...{Colors.RESET}\n")
|
print(f"{Colors.GRAY} Fetching market data and running analysis...{Colors.RESET}\n")
|
||||||
aggregator = SignalAggregator(config)
|
aggregator = SignalAggregator(config)
|
||||||
report = aggregator.analyze_pair()
|
report = aggregator.analyze_pair(symbol=config.symbol)
|
||||||
|
|
||||||
if args.json:
|
if args.json:
|
||||||
print(json.dumps(report_to_dict(report), indent=2))
|
print(json.dumps(report_to_dict(report), indent=2))
|
||||||
return
|
return
|
||||||
|
|
||||||
print_header("XAU/USD SIGNAL INTELLIGENCE")
|
print_header(f"{config.display_name} SIGNAL INTELLIGENCE")
|
||||||
print_signal_report(report, detail=args.detail)
|
print_signal_report(report, detail=args.detail)
|
||||||
print(f"\n{Colors.CYAN}{'═' * 80}{Colors.RESET}")
|
print(f"\n{Colors.CYAN}{'═' * 80}{Colors.RESET}")
|
||||||
print(f"{Colors.GRAY} Disclaimer: For educational/research purposes only.{Colors.RESET}")
|
print(f"{Colors.GRAY} Disclaimer: For educational/research purposes only.{Colors.RESET}")
|
||||||
|
|||||||
+89
-43
@@ -26,6 +26,7 @@ Usage:
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
@@ -51,17 +52,26 @@ from config import (
|
|||||||
XAUUSD, PAIR_INFO,
|
XAUUSD, PAIR_INFO,
|
||||||
)
|
)
|
||||||
from data_provider import ForexDataProvider
|
from data_provider import ForexDataProvider
|
||||||
|
from pair_signals import PAIR_PRODUCERS
|
||||||
from signal_aggregator import SignalAggregator, SignalReport
|
from signal_aggregator import SignalAggregator, SignalReport
|
||||||
|
|
||||||
# ── Force UTF-8 on Windows ────────────────────────────────────────────
|
# ── Force UTF-8 on Windows ────────────────────────────────────────────
|
||||||
if hasattr(sys.stdout, "reconfigure"):
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
logging.basicConfig(
|
_LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
|
||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
os.makedirs(_LOG_DIR, exist_ok=True)
|
||||||
level=logging.INFO,
|
_log_formatter = logging.Formatter(
|
||||||
datefmt="%H:%M:%S",
|
"%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%H:%M:%S"
|
||||||
)
|
)
|
||||||
|
_console_handler = logging.StreamHandler()
|
||||||
|
_console_handler.setFormatter(_log_formatter)
|
||||||
|
_file_handler = logging.handlers.TimedRotatingFileHandler(
|
||||||
|
os.path.join(_LOG_DIR, "telegram.log"),
|
||||||
|
when="midnight", backupCount=30, encoding="utf-8",
|
||||||
|
)
|
||||||
|
_file_handler.setFormatter(_log_formatter)
|
||||||
|
logging.basicConfig(level=logging.INFO, handlers=[_console_handler, _file_handler])
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
logger = logging.getLogger("telegram_bot")
|
logger = logging.getLogger("telegram_bot")
|
||||||
@@ -70,12 +80,15 @@ TOKEN = os.environ.get("TELEGRAM_TOKEN", "8228251480:AAFW2OAyQ2idMicql2t_CIXk1r9
|
|||||||
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022")
|
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022")
|
||||||
|
|
||||||
# ── Pairs & buttons ───────────────────────────────────────────────────
|
# ── Pairs & buttons ───────────────────────────────────────────────────
|
||||||
|
# The scanned pair list is config-driven: PlatformConfig.scan_pairs is
|
||||||
|
# the single switch for single- vs multi-pair operation. Buttons, WS
|
||||||
|
# subscriptions, and scan loops all derive from it.
|
||||||
|
|
||||||
SCAN_PAIRS = [XAUUSD]
|
SCAN_PAIRS = list(PlatformConfig().scan_pairs)
|
||||||
|
|
||||||
_WS_TO_PAIR = {
|
_PAIRS_LABEL = " + ".join(PAIR_INFO[p]["display"] for p in SCAN_PAIRS)
|
||||||
"XAU/USD": XAUUSD,
|
|
||||||
}
|
_WS_TO_PAIR = {PAIR_INFO[p]["display"]: p for p in SCAN_PAIRS}
|
||||||
_WS_SYMBOLS = list(_WS_TO_PAIR.keys())
|
_WS_SYMBOLS = list(_WS_TO_PAIR.keys())
|
||||||
|
|
||||||
_LEVEL_KEYS = (
|
_LEVEL_KEYS = (
|
||||||
@@ -85,14 +98,36 @@ _LEVEL_KEYS = (
|
|||||||
"kc_upper", "kc_lower",
|
"kc_upper", "kc_lower",
|
||||||
)
|
)
|
||||||
|
|
||||||
SCAN_BUTTONS = [
|
_TF_BUTTON_LABELS = [
|
||||||
{"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "5 Min"},
|
(Timeframe.M5, "5 Min"),
|
||||||
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "15 Min"},
|
(Timeframe.M15, "15 Min"),
|
||||||
{"cb": "scan_xauusd_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "1 Hour"},
|
(Timeframe.H1, "1 Hour"),
|
||||||
{"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "4 Hour"},
|
(Timeframe.H4, "4 Hour"),
|
||||||
{"cb": "scan_xauusd_all", "symbol": XAUUSD, "tf": None, "label": "Scan All TFs"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_scan_buttons() -> List[dict]:
|
||||||
|
"""One row of TF buttons + a scan-all button per scanned pair."""
|
||||||
|
multi = len(SCAN_PAIRS) > 1
|
||||||
|
buttons = []
|
||||||
|
for symbol in SCAN_PAIRS:
|
||||||
|
slug = symbol.replace("/", "").lower()
|
||||||
|
base = PAIR_INFO[symbol]["display"].split("/")[0] # XAU, EUR
|
||||||
|
prefix = f"{base} " if multi else ""
|
||||||
|
for tf, label in _TF_BUTTON_LABELS:
|
||||||
|
buttons.append({
|
||||||
|
"cb": f"scan_{slug}_{tf.value}", "symbol": symbol,
|
||||||
|
"tf": tf, "label": f"{prefix}{label}",
|
||||||
|
})
|
||||||
|
buttons.append({
|
||||||
|
"cb": f"scan_{slug}_all", "symbol": symbol, "tf": None,
|
||||||
|
"label": f"Scan All TFs — {base}" if multi else "Scan All TFs",
|
||||||
|
})
|
||||||
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
|
SCAN_BUTTONS = _build_scan_buttons()
|
||||||
|
|
||||||
SCAN_LOOKUP = {b["cb"]: b for b in SCAN_BUTTONS}
|
SCAN_LOOKUP = {b["cb"]: b for b in SCAN_BUTTONS}
|
||||||
|
|
||||||
TF_CONFIRMATIONS = {
|
TF_CONFIRMATIONS = {
|
||||||
@@ -258,7 +293,7 @@ def format_signal_message(report: SignalReport) -> str:
|
|||||||
# TP1 is advisory, the trail is the exit). Pullback setups run
|
# TP1 is advisory, the trail is the exit). Pullback setups run
|
||||||
# tighter stops and arm later.
|
# tighter stops and arm later.
|
||||||
sig_cfg = PlatformConfig().signals
|
sig_cfg = PlatformConfig().signals
|
||||||
is_pb = getattr(ts, "strategy", "GENERIC") == "XAU_PB"
|
is_pb = getattr(ts, "strategy", "GENERIC").endswith("_PB")
|
||||||
arm_r = sig_cfg.pb_trail_arm_r if is_pb else sig_cfg.trail_arm_r
|
arm_r = sig_cfg.pb_trail_arm_r if is_pb else sig_cfg.trail_arm_r
|
||||||
dist_r = sig_cfg.pb_trail_dist_r if is_pb else sig_cfg.trail_dist_r
|
dist_r = sig_cfg.pb_trail_dist_r if is_pb else sig_cfg.trail_dist_r
|
||||||
if arm_r > 0:
|
if arm_r > 0:
|
||||||
@@ -312,29 +347,31 @@ def _format_scan_summary(reports: List[SignalReport], tf: Timeframe) -> str:
|
|||||||
|
|
||||||
# ── Keyboard helper ───────────────────────────────────────────────────
|
# ── Keyboard helper ───────────────────────────────────────────────────
|
||||||
|
|
||||||
def scan_keyboard() -> InlineKeyboardMarkup:
|
def _scan_button_rows() -> List[List[InlineKeyboardButton]]:
|
||||||
tf_buttons = [b for b in SCAN_BUTTONS if b["tf"] is not None]
|
"""Per pair: one row of TF buttons + one scan-all row."""
|
||||||
all_button = next((b for b in SCAN_BUTTONS if b["tf"] is None), None)
|
rows = []
|
||||||
rows = [[
|
for symbol in SCAN_PAIRS:
|
||||||
InlineKeyboardButton(b["label"], callback_data=b["cb"])
|
tf_buttons = [b for b in SCAN_BUTTONS
|
||||||
for b in tf_buttons
|
if b["symbol"] == symbol and b["tf"] is not None]
|
||||||
]]
|
rows.append([InlineKeyboardButton(b["label"], callback_data=b["cb"])
|
||||||
|
for b in tf_buttons])
|
||||||
|
all_button = next((b for b in SCAN_BUTTONS
|
||||||
|
if b["symbol"] == symbol and b["tf"] is None), None)
|
||||||
if all_button:
|
if all_button:
|
||||||
rows.append([InlineKeyboardButton(all_button["label"], callback_data=all_button["cb"])])
|
rows.append([InlineKeyboardButton(all_button["label"],
|
||||||
return InlineKeyboardMarkup(rows)
|
callback_data=all_button["cb"])])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def scan_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(_scan_button_rows())
|
||||||
|
|
||||||
|
|
||||||
def main_menu_keyboard() -> InlineKeyboardMarkup:
|
def main_menu_keyboard() -> InlineKeyboardMarkup:
|
||||||
"""Full interactive menu: scans + settings + status in one keyboard."""
|
"""Full interactive menu: scans + settings + status in one keyboard."""
|
||||||
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)
|
|
||||||
autoscan_label = "🔔 Auto-scan: ON" if _autoscan_chats else "🔕 Auto-scan: OFF"
|
autoscan_label = "🔔 Auto-scan: ON" if _autoscan_chats else "🔕 Auto-scan: OFF"
|
||||||
quality_label = f"🎚 Alerts: {_alert_min_quality}+" if _alert_min_quality != "A" else "🎚 Alerts: A only"
|
quality_label = f"🎚 Alerts: {_alert_min_quality}+" if _alert_min_quality != "A" else "🎚 Alerts: A only"
|
||||||
rows = [
|
rows = _scan_button_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"])])
|
|
||||||
rows.append([
|
rows.append([
|
||||||
InlineKeyboardButton(autoscan_label, callback_data="menu_autoscan"),
|
InlineKeyboardButton(autoscan_label, callback_data="menu_autoscan"),
|
||||||
InlineKeyboardButton(quality_label, callback_data="menu_quality"),
|
InlineKeyboardButton(quality_label, callback_data="menu_quality"),
|
||||||
@@ -374,6 +411,13 @@ def _run_signal_sync(
|
|||||||
confirmation_tfs = TF_CONFIRMATIONS.get(
|
confirmation_tfs = TF_CONFIRMATIONS.get(
|
||||||
timeframe, [Timeframe.M15, Timeframe.H4]
|
timeframe, [Timeframe.M15, Timeframe.H4]
|
||||||
)
|
)
|
||||||
|
# Per-pair tuned configs (regime weights, gates, sessions) come from
|
||||||
|
# the pair_signals factories; a plain PlatformConfig would silently
|
||||||
|
# run every pair with XAU defaults.
|
||||||
|
factory = PAIR_PRODUCERS.get(symbol)
|
||||||
|
if factory is not None:
|
||||||
|
config = factory(timeframe, confirmation_tfs)
|
||||||
|
else:
|
||||||
config = PlatformConfig()
|
config = PlatformConfig()
|
||||||
config.primary_timeframe = timeframe
|
config.primary_timeframe = timeframe
|
||||||
config.confirmation_timeframes = confirmation_tfs
|
config.confirmation_timeframes = confirmation_tfs
|
||||||
@@ -717,7 +761,7 @@ def _stop_scheduler() -> None:
|
|||||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
scanning = "running" if _autoscan_chats else "off"
|
scanning = "running" if _autoscan_chats else "off"
|
||||||
text = (
|
text = (
|
||||||
"👋 *ForexBot2 — XAU/USD Signal Intelligence*\n\n"
|
f"👋 *ForexBot2 — {_PAIRS_LABEL} Signal Intelligence*\n\n"
|
||||||
"Pick a timeframe to scan gold.\n"
|
"Pick a timeframe to scan gold.\n"
|
||||||
"Signals include a full trade setup with entry, SL and TP.\n\n"
|
"Signals include a full trade setup with entry, SL and TP.\n\n"
|
||||||
f"Auto-scan: *{scanning}* — /autoscan to toggle\n"
|
f"Auto-scan: *{scanning}* — /autoscan to toggle\n"
|
||||||
@@ -735,7 +779,7 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
_HELP_TEXT = (
|
_HELP_TEXT = (
|
||||||
"📖 *ForexBot2 Commands*\n\n"
|
"📖 *ForexBot2 Commands*\n\n"
|
||||||
"/start — Welcome & interactive menu\n"
|
"/start — Welcome & interactive menu\n"
|
||||||
"/scan — Quick XAU/USD scan menu\n"
|
"/scan — Quick scan menu\n"
|
||||||
"/autoscan — Toggle auto-scan on/off\n"
|
"/autoscan — Toggle auto-scan on/off\n"
|
||||||
"/status — Bot status, WS connection, live prices\n"
|
"/status — Bot status, WS connection, live prices\n"
|
||||||
"/cache — Show data cache age per pair/TF\n"
|
"/cache — Show data cache age per pair/TF\n"
|
||||||
@@ -868,7 +912,7 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
|||||||
next_h4 = _seconds_until_boundary(240, buffer=10)
|
next_h4 = _seconds_until_boundary(240, buffer=10)
|
||||||
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 {_PAIRS_LABEL} on M5/M15/H1/H4 at candle close.\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"
|
||||||
@@ -879,7 +923,7 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
|||||||
|
|
||||||
async def cmd_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"🔍 *XAU/USD — Pick a timeframe*",
|
f"🔍 *{_PAIRS_LABEL} — Pick a timeframe*",
|
||||||
parse_mode=ParseMode.MARKDOWN,
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
reply_markup=main_menu_keyboard(),
|
reply_markup=main_menu_keyboard(),
|
||||||
)
|
)
|
||||||
@@ -894,10 +938,11 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if btn["tf"] is None:
|
if btn["tf"] is None:
|
||||||
await _cb_scan_all(query)
|
await _cb_scan_all(query, btn["symbol"])
|
||||||
return
|
return
|
||||||
|
|
||||||
await _safe_edit(query, f"⏳ Scanning XAU/USD {btn['label']}…")
|
display = PAIR_INFO[btn["symbol"]]["display"]
|
||||||
|
await _safe_edit(query, f"⏳ Scanning {display} {btn['label']}…")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
report = await run_signal(btn["symbol"], btn["tf"])
|
report = await run_signal(btn["symbol"], btn["tf"])
|
||||||
@@ -949,11 +994,12 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
await query.message.reply_text(_HELP_TEXT, parse_mode=ParseMode.MARKDOWN)
|
await query.message.reply_text(_HELP_TEXT, parse_mode=ParseMode.MARKDOWN)
|
||||||
|
|
||||||
|
|
||||||
async def _cb_scan_all(query) -> None:
|
async def _cb_scan_all(query, symbol: str = XAUUSD) -> None:
|
||||||
await _safe_edit(query, "⏳ Scanning XAU/USD across M5 / M15 / H1 / H4…")
|
display = PAIR_INFO[symbol]["display"]
|
||||||
|
await _safe_edit(query, f"⏳ Scanning {display} across M5 / M15 / H1 / H4…")
|
||||||
|
|
||||||
tfs = [Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4]
|
tfs = [Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4]
|
||||||
tasks = [run_signal(XAUUSD, tf) for tf in tfs]
|
tasks = [run_signal(symbol, tf) for tf in tfs]
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
reports = []
|
reports = []
|
||||||
@@ -968,7 +1014,7 @@ async def _cb_scan_all(query) -> None:
|
|||||||
_update_level_cache(reports)
|
_update_level_cache(reports)
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"📊 *XAU/USD — All Timeframes*",
|
f"📊 *{display} — All Timeframes*",
|
||||||
f"🕐 {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
|
f"🕐 {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
@@ -1008,7 +1054,7 @@ async def _post_init(app: Application) -> None:
|
|||||||
"""Register command menu and start scheduler on boot."""
|
"""Register command menu and start scheduler on boot."""
|
||||||
await app.bot.set_my_commands([
|
await app.bot.set_my_commands([
|
||||||
("start", "Welcome & scan buttons"),
|
("start", "Welcome & scan buttons"),
|
||||||
("scan", "Quick XAU/USD scan buttons"),
|
("scan", "Quick scan buttons"),
|
||||||
("autoscan", "Toggle auto-scan on/off"),
|
("autoscan", "Toggle auto-scan on/off"),
|
||||||
("status", "Bot status, WS connection, live prices"),
|
("status", "Bot status, WS connection, live prices"),
|
||||||
("cache", "Show data cache age per pair/TF"),
|
("cache", "Show data cache age per pair/TF"),
|
||||||
@@ -1056,7 +1102,7 @@ async def _post_init(app: Application) -> None:
|
|||||||
await app.bot.send_message(
|
await app.bot.send_message(
|
||||||
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 {_PAIRS_LABEL} on M5/M15/H1/H4.\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"
|
||||||
|
|||||||
+21
-2
@@ -1415,6 +1415,7 @@ def _():
|
|||||||
@test("build_config sets correct timeframe and confirmations")
|
@test("build_config sets correct timeframe and confirmations")
|
||||||
def _():
|
def _():
|
||||||
class FakeArgs:
|
class FakeArgs:
|
||||||
|
pair = "XAUUSD"
|
||||||
timeframe = "H4"
|
timeframe = "H4"
|
||||||
config = build_config(FakeArgs())
|
config = build_config(FakeArgs())
|
||||||
assert config.primary_timeframe == Timeframe.H4
|
assert config.primary_timeframe == Timeframe.H4
|
||||||
@@ -1426,11 +1427,23 @@ def _():
|
|||||||
def _():
|
def _():
|
||||||
for tf_str in ["M5", "M15", "M30", "H1", "H4", "D1"]:
|
for tf_str in ["M5", "M15", "M30", "H1", "H4", "D1"]:
|
||||||
class FakeArgs:
|
class FakeArgs:
|
||||||
|
pair = "XAUUSD"
|
||||||
timeframe = tf_str
|
timeframe = tf_str
|
||||||
config = build_config(FakeArgs())
|
config = build_config(FakeArgs())
|
||||||
assert config.primary_timeframe is not None
|
assert config.primary_timeframe is not None
|
||||||
|
|
||||||
|
|
||||||
|
@test("build_config resolves EURUSD pair to EUR factory config")
|
||||||
|
def _():
|
||||||
|
class FakeArgs:
|
||||||
|
pair = "EURUSD"
|
||||||
|
timeframe = "M15"
|
||||||
|
config = build_config(FakeArgs())
|
||||||
|
assert config.symbol == "EUR/USD"
|
||||||
|
assert config.pip_value == 0.0001
|
||||||
|
assert config.primary_timeframe == Timeframe.M15
|
||||||
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
# MODULE 7: telegram_bot.py
|
# MODULE 7: telegram_bot.py
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
@@ -1609,10 +1622,13 @@ def _():
|
|||||||
|
|
||||||
@test("SCAN_BUTTONS and SCAN_LOOKUP consistency")
|
@test("SCAN_BUTTONS and SCAN_LOOKUP consistency")
|
||||||
def _():
|
def _():
|
||||||
assert len(SCAN_BUTTONS) == 5 # 4 TFs + "Scan All"
|
from telegram_bot import SCAN_PAIRS
|
||||||
|
# 4 TF buttons + one "Scan All" per scanned pair
|
||||||
|
assert len(SCAN_BUTTONS) == 5 * len(SCAN_PAIRS)
|
||||||
for btn in SCAN_BUTTONS:
|
for btn in SCAN_BUTTONS:
|
||||||
assert btn["cb"] in SCAN_LOOKUP
|
assert btn["cb"] in SCAN_LOOKUP
|
||||||
assert "symbol" in btn
|
assert "symbol" in btn
|
||||||
|
assert btn["symbol"] in SCAN_PAIRS
|
||||||
assert "tf" in btn
|
assert "tf" in btn
|
||||||
assert "label" in btn
|
assert "label" in btn
|
||||||
|
|
||||||
@@ -1642,8 +1658,11 @@ def _():
|
|||||||
_level_cache_ts.clear()
|
_level_cache_ts.clear()
|
||||||
|
|
||||||
|
|
||||||
@test("_WS_TO_PAIR mapping is correct")
|
@test("_WS_TO_PAIR mapping covers every scanned pair")
|
||||||
def _():
|
def _():
|
||||||
|
from telegram_bot import SCAN_PAIRS
|
||||||
|
for p in SCAN_PAIRS:
|
||||||
|
assert _WS_TO_PAIR[PAIR_INFO[p]["display"]] == p
|
||||||
assert _WS_TO_PAIR["XAU/USD"] == XAUUSD
|
assert _WS_TO_PAIR["XAU/USD"] == XAUUSD
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user