Add EUR/USD support with modular per-pair signal producer pattern
- Register EUR/USD in config (pip 0.0001, EurUsdStrategyConfig) - Create pair_signals/ module: registry + dispatcher with per-pair config factories (xauusd.py, eurusd.py) for easy add/remove - EUR/USD gets forex-tuned regime weights, London+NY session hours, XAU strategy gates disabled - Add defense-in-depth display_name guards in signal_aggregator - Rename telegram_bot.py → testing_telegram_bot.py with separate bot token, both pairs with dedicated button rows - Add EUR/USD to news engine affected pairs map - 9 new tests (146 total, all passing) - Zero changes to engine.py, data_provider.py, main.py, backtest/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,9 +37,11 @@ class MarketSession(Enum):
|
||||
|
||||
# ── Supported pairs ──────────────────────────────────────────────
|
||||
XAUUSD = "XAU/USD"
|
||||
EURUSD = "EUR/USD"
|
||||
|
||||
PAIR_INFO = {
|
||||
XAUUSD: {"display": "XAU/USD", "pip_value": 0.10, "is_jpy_pair": False},
|
||||
EURUSD: {"display": "EUR/USD", "pip_value": 0.0001, "is_jpy_pair": False},
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +125,7 @@ class SignalConfig:
|
||||
@dataclass
|
||||
class NewsConfig:
|
||||
"""Economic calendar & sentiment."""
|
||||
tracked_currencies: List[str] = field(default_factory=lambda: ["USD", "XAU"])
|
||||
tracked_currencies: List[str] = field(default_factory=lambda: ["USD", "XAU", "EUR"])
|
||||
min_impact: str = "medium"
|
||||
pre_event_buffer_minutes: int = 30
|
||||
post_event_buffer_minutes: int = 15
|
||||
@@ -159,6 +161,18 @@ class XauStrategyConfig:
|
||||
sell_min_quality: List[str] = field(default_factory=lambda: ["A+", "A", "B"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class EurUsdStrategyConfig:
|
||||
"""EUR/USD strategy config — separate from XAU for independent tuning."""
|
||||
enabled: bool = True
|
||||
blocked_hours_utc: List[int] = field(default_factory=lambda: [22, 23, 0, 1, 2, 3])
|
||||
weekend_filter: bool = True
|
||||
allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4])
|
||||
min_atr_threshold: float = 0.0003
|
||||
sl_atr_multiplier: float = 1.5
|
||||
tp_atr_multiplier: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilterConfig:
|
||||
"""Regime and session filters for live signals."""
|
||||
@@ -182,6 +196,7 @@ class PlatformConfig:
|
||||
news: NewsConfig = field(default_factory=NewsConfig)
|
||||
backtest: FilterConfig = field(default_factory=FilterConfig)
|
||||
xau_strategy: XauStrategyConfig = field(default_factory=XauStrategyConfig)
|
||||
eur_strategy: EurUsdStrategyConfig = field(default_factory=EurUsdStrategyConfig)
|
||||
|
||||
symbol: str = XAUUSD
|
||||
display_name: str = "XAU/USD"
|
||||
|
||||
+2
-1
@@ -541,7 +541,8 @@ class NewsEngine:
|
||||
@staticmethod
|
||||
def _get_affected_pairs(currency: str) -> List[str]:
|
||||
_MAP = {
|
||||
"USD": ["XAU/USD"],
|
||||
"USD": ["XAU/USD", "EUR/USD"],
|
||||
"XAU": ["XAU/USD"],
|
||||
"EUR": ["EUR/USD"],
|
||||
}
|
||||
return _MAP.get(currency, [])
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Per-pair signal producers.
|
||||
|
||||
Each pair module exposes:
|
||||
make_config(primary_tf, confirmation_tfs) -> PlatformConfig
|
||||
|
||||
To add a new pair: create pair_signals/<pair>.py with make_config(),
|
||||
then register it in this file.
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Callable
|
||||
|
||||
from config import PlatformConfig, Timeframe
|
||||
from signal_aggregator import SignalAggregator, SignalReport
|
||||
from data_provider import ForexDataProvider
|
||||
|
||||
PAIR_PRODUCERS: Dict[str, Callable] = {}
|
||||
|
||||
|
||||
def register_pair(symbol: str, config_factory: Callable):
|
||||
PAIR_PRODUCERS[symbol] = config_factory
|
||||
|
||||
|
||||
def get_supported_pairs() -> List[str]:
|
||||
return list(PAIR_PRODUCERS.keys())
|
||||
|
||||
|
||||
def produce_signal(
|
||||
symbol: str,
|
||||
primary_tf: Timeframe,
|
||||
data_provider: ForexDataProvider,
|
||||
confirmation_tfs: Optional[List[Timeframe]] = None,
|
||||
) -> SignalReport:
|
||||
config_factory = PAIR_PRODUCERS.get(symbol)
|
||||
if config_factory is None:
|
||||
raise ValueError(
|
||||
f"No signal producer registered for {symbol}. "
|
||||
f"Available: {list(PAIR_PRODUCERS.keys())}"
|
||||
)
|
||||
|
||||
config = config_factory(primary_tf, confirmation_tfs)
|
||||
aggregator = SignalAggregator(config)
|
||||
aggregator.data_provider = data_provider
|
||||
return aggregator.analyze_pair(symbol=symbol)
|
||||
|
||||
|
||||
from pair_signals import xauusd as _xau_mod
|
||||
from pair_signals import eurusd as _eur_mod
|
||||
from config import XAUUSD, EURUSD
|
||||
|
||||
register_pair(XAUUSD, _xau_mod.make_config)
|
||||
register_pair(EURUSD, _eur_mod.make_config)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""EUR/USD signal producer config."""
|
||||
|
||||
from typing import Optional, List
|
||||
from config import PlatformConfig, Timeframe, EURUSD
|
||||
|
||||
_CONFIRMATIONS = {
|
||||
Timeframe.M5: [Timeframe.M15, Timeframe.H1],
|
||||
Timeframe.M15: [Timeframe.M5, Timeframe.H1],
|
||||
Timeframe.H1: [Timeframe.M15, Timeframe.H4],
|
||||
Timeframe.H4: [Timeframe.H1, Timeframe.D1],
|
||||
}
|
||||
|
||||
_TRENDING = {
|
||||
"ema_trend": 2.0, "rsi": 1.0, "macd": 2.0,
|
||||
"bollinger": 0.5, "keltner": 1.0, "roc": 1.5,
|
||||
"candlestick": 0.5, "divergence": 2.0, "adx": 1.0,
|
||||
}
|
||||
|
||||
EUR_REGIME_WEIGHTS = {
|
||||
"trending_up": _TRENDING,
|
||||
"trending_down": _TRENDING,
|
||||
"trending": _TRENDING,
|
||||
"ranging": {
|
||||
"ema_trend": 0.3, "rsi": 2.0, "macd": 1.0,
|
||||
"bollinger": 2.0, "keltner": 1.5, "roc": 0.5,
|
||||
"candlestick": 0.8, "divergence": 2.5, "adx": 0.5,
|
||||
},
|
||||
"transitioning": {
|
||||
"ema_trend": 1.5, "rsi": 1.5, "macd": 2.0,
|
||||
"bollinger": 1.0, "keltner": 1.5, "roc": 2.0,
|
||||
"candlestick": 0.8, "divergence": 2.0, "adx": 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def make_config(
|
||||
primary_tf: Timeframe,
|
||||
confirmation_tfs: Optional[List[Timeframe]] = None,
|
||||
) -> PlatformConfig:
|
||||
config = PlatformConfig()
|
||||
config.symbol = EURUSD
|
||||
config.display_name = "EUR/USD"
|
||||
config.pip_value = 0.0001
|
||||
config.primary_timeframe = primary_tf
|
||||
config.confirmation_timeframes = (
|
||||
confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4])
|
||||
)
|
||||
|
||||
config.xau_strategy.enabled = False
|
||||
|
||||
config.backtest.session_start_hour = 7
|
||||
config.backtest.session_end_hour = 20
|
||||
config.backtest.blocked_hours_utc = [22, 23, 0, 1, 2, 3]
|
||||
config.backtest.allowed_weekdays = [0, 1, 2, 3, 4]
|
||||
|
||||
config.technical.regime_weights_override = EUR_REGIME_WEIGHTS
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,23 @@
|
||||
"""XAU/USD signal producer config."""
|
||||
|
||||
from typing import Optional, List
|
||||
from config import PlatformConfig, Timeframe
|
||||
|
||||
_CONFIRMATIONS = {
|
||||
Timeframe.M5: [Timeframe.M15, Timeframe.H1],
|
||||
Timeframe.M15: [Timeframe.M5, Timeframe.H1],
|
||||
Timeframe.H1: [Timeframe.M15, Timeframe.H4],
|
||||
Timeframe.H4: [Timeframe.H1, Timeframe.D1],
|
||||
}
|
||||
|
||||
|
||||
def make_config(
|
||||
primary_tf: Timeframe,
|
||||
confirmation_tfs: Optional[List[Timeframe]] = None,
|
||||
) -> PlatformConfig:
|
||||
config = PlatformConfig()
|
||||
config.primary_timeframe = primary_tf
|
||||
config.confirmation_timeframes = (
|
||||
confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4])
|
||||
)
|
||||
return config
|
||||
@@ -417,6 +417,7 @@ class SignalAggregator:
|
||||
or (report.final_signal == SignalStrength.NEUTRAL and report.final_score < 0)
|
||||
)
|
||||
if (self.config.xau_strategy.enabled
|
||||
and report.display_name == "XAU/USD"
|
||||
and xau_is_sell_dir
|
||||
and report.signal_quality not in self.config.xau_strategy.sell_min_quality):
|
||||
report.warnings.append(
|
||||
@@ -426,7 +427,7 @@ class SignalAggregator:
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 7. Trade setup for actionable signals ──────────────────
|
||||
if report.trade_setup is None and report.signal_quality in ("A+", "A"):
|
||||
if report.trade_setup is None and report.signal_quality in ("A+", "A") and report.display_name == "XAU/USD":
|
||||
atr = (report.primary_analysis.key_levels.get("atr", 0)
|
||||
if report.primary_analysis else 0)
|
||||
if atr > 0:
|
||||
|
||||
+115
-10
@@ -88,8 +88,8 @@ print("\n═══ Module 1: config.py ═══")
|
||||
from config import (
|
||||
Timeframe, SignalStrength, MarketSession,
|
||||
PlatformConfig, TechnicalConfig, SignalConfig, NewsConfig,
|
||||
FilterConfig, XauStrategyConfig,
|
||||
XAUUSD, PAIR_INFO,
|
||||
FilterConfig, XauStrategyConfig, EurUsdStrategyConfig,
|
||||
XAUUSD, EURUSD, PAIR_INFO,
|
||||
)
|
||||
|
||||
|
||||
@@ -1313,12 +1313,12 @@ def _():
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# MODULE 7: telegram_bot.py
|
||||
# MODULE 7: testing_telegram_bot.py
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
print("\n═══ Module 7: telegram_bot.py ═══")
|
||||
print("\n═══ Module 7: testing_telegram_bot.py ═══")
|
||||
|
||||
from telegram_bot import (
|
||||
from testing_telegram_bot import (
|
||||
_escape_md, _signal_label, _mtf_line,
|
||||
format_signal_message, _format_scan_summary,
|
||||
_seconds_until_boundary, _check_level_breach,
|
||||
@@ -1491,7 +1491,7 @@ def _():
|
||||
|
||||
@test("SCAN_BUTTONS and SCAN_LOOKUP consistency")
|
||||
def _():
|
||||
assert len(SCAN_BUTTONS) == 5 # 4 TFs + "Scan All"
|
||||
assert len(SCAN_BUTTONS) == 10 # 5 per pair (4 TFs + All) × 2 pairs
|
||||
for btn in SCAN_BUTTONS:
|
||||
assert btn["cb"] in SCAN_LOOKUP
|
||||
assert "symbol" in btn
|
||||
@@ -1531,12 +1531,12 @@ def _():
|
||||
|
||||
@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()
|
||||
import testing_telegram_bot
|
||||
old_stats = testing_telegram_bot._signal_stats.copy()
|
||||
testing_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)
|
||||
testing_telegram_bot._signal_stats.update(old_stats)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
@@ -1653,6 +1653,111 @@ def _():
|
||||
assert r1.market_regime == r2.market_regime
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# MODULE 9: EUR/USD pair support
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
print("\n═══ Module 9: EUR/USD pair support ═══")
|
||||
|
||||
|
||||
@test("PAIR_INFO includes EUR/USD with correct pip_value")
|
||||
def _():
|
||||
assert EURUSD in PAIR_INFO
|
||||
assert PAIR_INFO[EURUSD]["pip_value"] == 0.0001
|
||||
assert PAIR_INFO[EURUSD]["is_jpy_pair"] is False
|
||||
assert PAIR_INFO[EURUSD]["display"] == "EUR/USD"
|
||||
|
||||
|
||||
@test("EurUsdStrategyConfig defaults are sensible")
|
||||
def _():
|
||||
cfg = EurUsdStrategyConfig()
|
||||
assert cfg.enabled is True
|
||||
assert 0 in cfg.blocked_hours_utc
|
||||
assert 5 not in cfg.allowed_weekdays # No Saturday
|
||||
assert 6 not in cfg.allowed_weekdays # No Sunday
|
||||
assert cfg.min_atr_threshold == 0.0003
|
||||
|
||||
|
||||
@test("PlatformConfig has eur_strategy field")
|
||||
def _():
|
||||
config = PlatformConfig()
|
||||
assert hasattr(config, "eur_strategy")
|
||||
assert isinstance(config.eur_strategy, EurUsdStrategyConfig)
|
||||
|
||||
|
||||
@test("EUR/USD config factory produces correct config")
|
||||
def _():
|
||||
from pair_signals.eurusd import make_config
|
||||
config = make_config(Timeframe.H1)
|
||||
assert config.symbol == EURUSD
|
||||
assert config.display_name == "EUR/USD"
|
||||
assert config.pip_value == 0.0001
|
||||
assert config.xau_strategy.enabled is False
|
||||
assert config.technical.regime_weights_override
|
||||
assert "trending" in config.technical.regime_weights_override
|
||||
assert "ranging" in config.technical.regime_weights_override
|
||||
|
||||
|
||||
@test("XAU/USD config factory unchanged")
|
||||
def _():
|
||||
from pair_signals.xauusd import make_config
|
||||
config = make_config(Timeframe.H1)
|
||||
assert config.symbol == XAUUSD
|
||||
assert config.display_name == "XAU/USD"
|
||||
assert config.pip_value == 0.10
|
||||
assert config.xau_strategy.enabled is True
|
||||
|
||||
|
||||
@test("pair_signals registry includes both pairs")
|
||||
def _():
|
||||
from pair_signals import get_supported_pairs
|
||||
pairs = get_supported_pairs()
|
||||
assert XAUUSD in pairs
|
||||
assert EURUSD in pairs
|
||||
|
||||
|
||||
@test("EUR/USD signal analysis produces valid report (mocked data)")
|
||||
def _():
|
||||
from pair_signals.eurusd import make_config
|
||||
from signal_aggregator import SignalAggregator
|
||||
config = make_config(Timeframe.H1)
|
||||
config.backtest.session_filter = False
|
||||
agg = SignalAggregator(config)
|
||||
df = make_ohlcv(n=300, start_price=1.0800, trend=0.00005)
|
||||
dfs = {
|
||||
Timeframe.H1: df,
|
||||
Timeframe.M15: df,
|
||||
Timeframe.H4: df,
|
||||
}
|
||||
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
|
||||
assert report.display_name == "EUR/USD"
|
||||
assert report.signal_quality in ("A+", "A", "B", "C", "D", "F")
|
||||
assert not any("SELL FILTER" in w for w in report.warnings)
|
||||
|
||||
|
||||
@test("XAU sell gate does not apply to EUR/USD")
|
||||
def _():
|
||||
from pair_signals.eurusd import make_config
|
||||
from signal_aggregator import SignalAggregator
|
||||
config = make_config(Timeframe.H1)
|
||||
config.backtest.session_filter = False
|
||||
agg = SignalAggregator(config)
|
||||
df = make_ohlcv(n=300, start_price=1.0800, trend=-0.0005, seed=99)
|
||||
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
|
||||
report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False)
|
||||
sell_filter_warnings = [w for w in report.warnings if "SELL FILTER" in w]
|
||||
assert len(sell_filter_warnings) == 0
|
||||
|
||||
|
||||
@test("NewsEngine._get_affected_pairs includes EUR/USD for EUR and USD events")
|
||||
def _():
|
||||
from news_engine import NewsEngine
|
||||
assert "EUR/USD" in NewsEngine._get_affected_pairs("EUR")
|
||||
assert "EUR/USD" in NewsEngine._get_affected_pairs("USD")
|
||||
assert "XAU/USD" in NewsEngine._get_affected_pairs("USD")
|
||||
assert "EUR/USD" not in NewsEngine._get_affected_pairs("GBP")
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# SUMMARY
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -5,7 +5,7 @@ ForexBot2 — Telegram Bot
|
||||
Async Telegram interface with candle-boundary auto-scanner and
|
||||
real-time WebSocket level-breach monitor.
|
||||
|
||||
Manual scan: XAU/USD × 3 timeframes (M15/H1/H4) via inline buttons.
|
||||
Manual scan: XAU/USD + EUR/USD 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).
|
||||
@@ -19,8 +19,8 @@ WebSocket: Streams live ticks from TwelveData for XAU/USD.
|
||||
5-minute cooldown prevents spam.
|
||||
|
||||
Usage:
|
||||
python telegram_bot.py
|
||||
TELEGRAM_TOKEN=<token> TELEGRAM_CHAT_ID=<id> python telegram_bot.py
|
||||
python testing_telegram_bot.py
|
||||
TELEGRAM_TOKEN=<token> TELEGRAM_CHAT_ID=<id> python testing_telegram_bot.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -48,10 +48,11 @@ from telegram.ext import (
|
||||
|
||||
from config import (
|
||||
PlatformConfig, SignalStrength, Timeframe,
|
||||
XAUUSD, PAIR_INFO,
|
||||
XAUUSD, EURUSD, PAIR_INFO,
|
||||
)
|
||||
from data_provider import ForexDataProvider
|
||||
from signal_aggregator import SignalAggregator, SignalReport
|
||||
from pair_signals import produce_signal
|
||||
|
||||
# ── Force UTF-8 on Windows ────────────────────────────────────────────
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
@@ -66,12 +67,12 @@ logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logger = logging.getLogger("telegram_bot")
|
||||
|
||||
TOKEN = os.environ.get("TELEGRAM_TOKEN", "8228251480:AAFW2OAyQ2idMicql2t_CIXk1r9-qfRTfNE")
|
||||
TOKEN = os.environ.get("TELEGRAM_TOKEN", "8882954582:AAEOi2MzoMbJjozDMiRtFBHc2muix_MtaQQ")
|
||||
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022")
|
||||
|
||||
# ── Pairs & buttons ───────────────────────────────────────────────────
|
||||
|
||||
SCAN_PAIRS = [XAUUSD]
|
||||
SCAN_PAIRS = [XAUUSD, EURUSD]
|
||||
|
||||
_WS_TO_PAIR = {
|
||||
"XAU/USD": XAUUSD,
|
||||
@@ -91,11 +92,16 @@ _signal_stats: Dict[str, Dict[str, SignalReport]] = {}
|
||||
_stats_updated: Optional[float] = None
|
||||
|
||||
SCAN_BUTTONS = [
|
||||
{"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "5 Min"},
|
||||
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "15 Min"},
|
||||
{"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"},
|
||||
{"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "XAU 5m"},
|
||||
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "XAU 15m"},
|
||||
{"cb": "scan_xauusd_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "XAU 1h"},
|
||||
{"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "XAU 4h"},
|
||||
{"cb": "scan_xauusd_all", "symbol": XAUUSD, "tf": None, "label": "XAU All"},
|
||||
{"cb": "scan_eurusd_m5", "symbol": EURUSD, "tf": Timeframe.M5, "label": "EUR 5m"},
|
||||
{"cb": "scan_eurusd_m15", "symbol": EURUSD, "tf": Timeframe.M15, "label": "EUR 15m"},
|
||||
{"cb": "scan_eurusd_h1", "symbol": EURUSD, "tf": Timeframe.H1, "label": "EUR 1h"},
|
||||
{"cb": "scan_eurusd_h4", "symbol": EURUSD, "tf": Timeframe.H4, "label": "EUR 4h"},
|
||||
{"cb": "scan_eurusd_all", "symbol": EURUSD, "tf": None, "label": "EUR All"},
|
||||
]
|
||||
|
||||
SCAN_LOOKUP = {b["cb"]: b for b in SCAN_BUTTONS}
|
||||
@@ -408,14 +414,14 @@ async def _dashboard_loop(bot) -> None:
|
||||
# ── Keyboard helper ───────────────────────────────────────────────────
|
||||
|
||||
def scan_keyboard() -> InlineKeyboardMarkup:
|
||||
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 = [[
|
||||
rows = []
|
||||
for pair in SCAN_PAIRS:
|
||||
pair_btns = [b for b in SCAN_BUTTONS if b["symbol"] == pair]
|
||||
row = [
|
||||
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"])])
|
||||
for b in pair_btns
|
||||
]
|
||||
rows.append(row)
|
||||
return InlineKeyboardMarkup(rows)
|
||||
|
||||
|
||||
@@ -430,12 +436,12 @@ def _run_signal_sync(
|
||||
confirmation_tfs = TF_CONFIRMATIONS.get(
|
||||
timeframe, [Timeframe.M15, Timeframe.H4]
|
||||
)
|
||||
config = PlatformConfig()
|
||||
config.primary_timeframe = timeframe
|
||||
config.confirmation_timeframes = confirmation_tfs
|
||||
aggregator = SignalAggregator(config)
|
||||
aggregator.data_provider = _get_shared_dp()
|
||||
return aggregator.analyze_pair(symbol=symbol)
|
||||
return produce_signal(
|
||||
symbol=symbol,
|
||||
primary_tf=timeframe,
|
||||
data_provider=_get_shared_dp(),
|
||||
confirmation_tfs=confirmation_tfs,
|
||||
)
|
||||
|
||||
|
||||
async def run_signal(
|
||||
@@ -709,8 +715,8 @@ def _stop_scheduler() -> None:
|
||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
scanning = "running" if _autoscan_chats else "off"
|
||||
text = (
|
||||
"👋 *ForexBot2 — XAU/USD Signal Intelligence*\n\n"
|
||||
"Pick a timeframe to scan gold.\n"
|
||||
"👋 *ForexBot2 — Signal Intelligence*\n\n"
|
||||
"Pick a pair and timeframe to scan.\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"
|
||||
@@ -728,7 +734,7 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
text = (
|
||||
"📖 *ForexBot2 Commands*\n\n"
|
||||
"/start — Welcome & scan buttons\n"
|
||||
"/scan — Quick XAU/USD scan buttons\n"
|
||||
"/scan — Quick scan buttons (XAU/USD + EUR/USD)\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"
|
||||
@@ -851,7 +857,7 @@ 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 XAU/USD on M5/M15/H1/H4 at candle close.\n"
|
||||
f"Scanning XAU/USD + EUR/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 | "
|
||||
@@ -863,7 +869,7 @@ 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*",
|
||||
"🔍 *Pick a pair & timeframe*",
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
reply_markup=scan_keyboard(),
|
||||
)
|
||||
@@ -878,11 +884,12 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
return
|
||||
|
||||
if btn["tf"] is None:
|
||||
await _cb_scan_all(query)
|
||||
await _cb_scan_all(query, symbol=btn["symbol"])
|
||||
return
|
||||
|
||||
display = PAIR_INFO.get(btn["symbol"], {}).get("display", btn["symbol"])
|
||||
await query.edit_message_text(
|
||||
f"⏳ Scanning XAU/USD {btn['label']}…",
|
||||
f"⏳ Scanning {display} {btn['label']}…",
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
)
|
||||
|
||||
@@ -900,14 +907,15 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _cb_scan_all(query) -> None:
|
||||
async def _cb_scan_all(query, symbol: str = XAUUSD) -> None:
|
||||
display = PAIR_INFO.get(symbol, {}).get("display", symbol)
|
||||
await query.edit_message_text(
|
||||
"⏳ Scanning XAU/USD across M5 / M15 / H1 / H4…",
|
||||
f"⏳ Scanning {display} across M5 / M15 / H1 / H4…",
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
reports = []
|
||||
@@ -923,7 +931,7 @@ async def _cb_scan_all(query) -> None:
|
||||
_update_level_cache(reports)
|
||||
|
||||
lines = [
|
||||
"📊 *XAU/USD — All Timeframes*",
|
||||
f"📊 *{display} — All Timeframes*",
|
||||
f"🕐 {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
|
||||
"",
|
||||
]
|
||||
@@ -967,7 +975,7 @@ async def _post_init(app: Application) -> None:
|
||||
"""Register command menu and start scheduler on boot."""
|
||||
await app.bot.set_my_commands([
|
||||
("start", "Welcome & scan buttons"),
|
||||
("scan", "Quick XAU/USD scan buttons"),
|
||||
("scan", "Quick scan buttons (XAU/USD + EUR/USD)"),
|
||||
("autoscan", "Toggle auto-scan on/off"),
|
||||
("status", "Bot status, WS connection, live prices"),
|
||||
("cache", "Show data cache age per pair/TF"),
|
||||
@@ -1016,7 +1024,7 @@ async def _post_init(app: Application) -> None:
|
||||
await app.bot.send_message(
|
||||
chat_id,
|
||||
f"🤖 *ForexBot2 started*\n\n"
|
||||
f"Auto-scanning XAU/USD on M5/M15/H1/H4.\n"
|
||||
f"Auto-scanning XAU/USD + EUR/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 | "
|
||||
Reference in New Issue
Block a user