Backtest tuning, data freshness guard, Telegram improvements
- Drop USD/JPY from scan pairs (zero signals across all timeframes) - Disable divergence indicator (zero impact in 14-config lab test) - Parallelize evaluate.py with ProcessPoolExecutor (16 cores) - Add --pair CLI filter to evaluate.py for single-pair backtests - Add data freshness guard: stale cache auto-invalidated, primary TF stale data suppresses signals to prevent trading on old data - Add /cache command: shows fetch age and last bar per pair/TF - Add /clearcache command: wipes in-memory cache for fresh API fetch - Remove BB from WebSocket breach alerts (keep structural levels only) - Show live prices only for WS-monitored pairs (EUR/USD, XAU/USD) - Replace arrow emojis with text tags in indicator lines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+60
-20
@@ -54,7 +54,7 @@ from telegram.ext import (
|
||||
|
||||
from config import (
|
||||
PlatformConfig, SignalStrength, Timeframe,
|
||||
EURUSD, USDJPY, GBPUSD, XAUUSD, PAIR_INFO,
|
||||
EURUSD, GBPUSD, XAUUSD, PAIR_INFO,
|
||||
)
|
||||
from data_provider import ForexDataProvider
|
||||
from signal_aggregator import SignalAggregator, SignalReport
|
||||
@@ -77,11 +77,10 @@ CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022")
|
||||
|
||||
# ── Pairs & buttons ───────────────────────────────────────────────────
|
||||
|
||||
SCAN_PAIRS = [EURUSD, USDJPY, GBPUSD, XAUUSD]
|
||||
SCAN_PAIRS = [EURUSD, GBPUSD, XAUUSD]
|
||||
|
||||
CORRELATION_GROUPS = {
|
||||
"usd_negative": {"pairs": {EURUSD, GBPUSD}, "label": "USD-weakness"},
|
||||
"risk_sentiment": {"pairs": {XAUUSD, USDJPY}, "label": "Risk-sentiment"},
|
||||
}
|
||||
|
||||
_WS_TO_PAIR = {
|
||||
@@ -93,17 +92,14 @@ _WS_SYMBOLS = list(_WS_TO_PAIR.keys())
|
||||
_LEVEL_KEYS = (
|
||||
"swing_high_1", "swing_high_2", "swing_high_3",
|
||||
"swing_low_1", "swing_low_2", "swing_low_3",
|
||||
"bb_upper", "bb_lower", "ema_trend",
|
||||
"ema_trend",
|
||||
)
|
||||
|
||||
SCAN_BUTTONS = [
|
||||
{"cb": "scan_eurusd_m15", "symbol": EURUSD, "tf": Timeframe.M15, "label": "EUR/USD 15M"},
|
||||
{"cb": "scan_eurusd_h1", "symbol": EURUSD, "tf": Timeframe.H1, "label": "EUR/USD 1H"},
|
||||
{"cb": "scan_eurusd_h4", "symbol": EURUSD, "tf": Timeframe.H4, "label": "EUR/USD 4H"},
|
||||
{"cb": "scan_usdjpy_m15", "symbol": USDJPY, "tf": Timeframe.M15, "label": "USD/JPY 15M"},
|
||||
{"cb": "scan_usdjpy_h1", "symbol": USDJPY, "tf": Timeframe.H1, "label": "USD/JPY 1H"},
|
||||
{"cb": "scan_usdjpy_h4", "symbol": USDJPY, "tf": Timeframe.H4, "label": "USD/JPY 4H"},
|
||||
{"cb": "scan_gbpusd_m15", "symbol": GBPUSD, "tf": Timeframe.M15, "label": "GBP/USD 15M"},
|
||||
{"cb": "scan_gbpusd_m15", "symbol": GBPUSD, "tf": Timeframe.M15, "label": "GBP/USD 15M"},
|
||||
{"cb": "scan_gbpusd_h1", "symbol": GBPUSD, "tf": Timeframe.H1, "label": "GBP/USD 1H"},
|
||||
{"cb": "scan_gbpusd_h4", "symbol": GBPUSD, "tf": Timeframe.H4, "label": "GBP/USD 4H"},
|
||||
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "XAU/USD 15M"},
|
||||
@@ -156,12 +152,12 @@ _QUALITY_EMOJI = {
|
||||
"A+": "🏆", "A": "🥇", "B": "🥈", "C": "🥉", "D": "⚠️", "F": "❌",
|
||||
}
|
||||
|
||||
_ARROW = {
|
||||
SignalStrength.STRONG_BUY: "⬆️",
|
||||
SignalStrength.BUY: "↗️",
|
||||
SignalStrength.NEUTRAL: "➡️",
|
||||
SignalStrength.SELL: "↘️",
|
||||
SignalStrength.STRONG_SELL: "⬇️",
|
||||
_SIGNAL_TAG = {
|
||||
SignalStrength.STRONG_BUY: "Strong Buy",
|
||||
SignalStrength.BUY: "Bullish",
|
||||
SignalStrength.NEUTRAL: "Neutral",
|
||||
SignalStrength.SELL: "Bearish",
|
||||
SignalStrength.STRONG_SELL: "Strong Sell",
|
||||
}
|
||||
|
||||
|
||||
@@ -180,9 +176,9 @@ def _mtf_line(report: SignalReport) -> str:
|
||||
|
||||
|
||||
def _indicator_line(name: str, ind) -> str:
|
||||
arrow = _ARROW.get(ind.signal, "➡️")
|
||||
tag = _SIGNAL_TAG.get(ind.signal, "Neutral")
|
||||
short = ind.description.split(" | ")[0] if " | " in ind.description else ind.description
|
||||
return f"{arrow} *{ind.name}*: {short}"
|
||||
return f"[{tag}] *{ind.name}*: {short}"
|
||||
|
||||
|
||||
def format_signal_message(report: SignalReport) -> str:
|
||||
@@ -601,9 +597,11 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
text = (
|
||||
"📖 *ForexBot2 Commands*\n\n"
|
||||
"/start — Show scan buttons (4 pairs × 4 TFs)\n"
|
||||
"/start — Show scan buttons\n"
|
||||
"/autoscan — Toggle auto-scan on/off\n"
|
||||
"/status — Bot status, WS connection, live prices\n"
|
||||
"/cache — Show when each pair/TF was last fetched\n"
|
||||
"/clearcache — Wipe data cache, force fresh fetch\n"
|
||||
"/help — This message\n\n"
|
||||
"*Auto-scan* fires at candle close: M5/M15/H1/H4.\n"
|
||||
"*WebSocket* monitors live prices and sends Level Break Alerts "
|
||||
@@ -628,10 +626,10 @@ async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None
|
||||
lines.append("❌ WebSocket: *disconnected*")
|
||||
|
||||
if _last_prices:
|
||||
lines.append("\n*Live Prices:*")
|
||||
for pair in SCAN_PAIRS:
|
||||
lines.append("\n*Live Prices (WebSocket):*")
|
||||
for ws_sym, pair in _WS_TO_PAIR.items():
|
||||
price = _last_prices.get(pair)
|
||||
display = PAIR_INFO.get(pair, {}).get("display", pair)
|
||||
display = PAIR_INFO.get(pair, {}).get("display", ws_sym)
|
||||
if price is None:
|
||||
lines.append(f" {display}: waiting...")
|
||||
elif price < 10:
|
||||
@@ -658,6 +656,44 @@ async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None
|
||||
await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
|
||||
async def cmd_cache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
dp = _get_shared_dp()
|
||||
if not dp._cache_timestamps:
|
||||
await update.message.reply_text("📭 Cache is empty — no data fetched yet.")
|
||||
return
|
||||
|
||||
now = time.time()
|
||||
lines = ["📦 *Data Cache Status*\n"]
|
||||
|
||||
for pair in SCAN_PAIRS:
|
||||
display = PAIR_INFO.get(pair, {}).get("display", pair)
|
||||
pair_lines = []
|
||||
for tf in [Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4, Timeframe.D1]:
|
||||
cache_key = f"{pair}_{tf.value}"
|
||||
ts = dp._cache_timestamps.get(cache_key)
|
||||
if ts is None:
|
||||
continue
|
||||
age_min = (now - ts) / 60
|
||||
df = dp._cache.get(cache_key)
|
||||
last_bar = df.index[-1].strftime("%H:%M") if df is not None and not df.empty else "?"
|
||||
fresh = "✅" if age_min < dp.CANDLE_SECONDS.get(tf, 3600) / 60 else "⚠️"
|
||||
pair_lines.append(f" {tf.value:<5} {fresh} fetched `{age_min:.0f}m` ago | last bar `{last_bar}`")
|
||||
if pair_lines:
|
||||
lines.append(f"*{display}*")
|
||||
lines.extend(pair_lines)
|
||||
|
||||
await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
|
||||
async def cmd_clearcache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
dp = _get_shared_dp()
|
||||
count = dp.clear_cache()
|
||||
await update.message.reply_text(
|
||||
f"🗑 Cache cleared ({count} entries wiped).\nNext scan will fetch fresh data from API.",
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
)
|
||||
|
||||
|
||||
async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
chat_id = update.effective_chat.id
|
||||
|
||||
@@ -725,6 +761,8 @@ async def _post_init(app: Application) -> None:
|
||||
("start", "Show scan buttons"),
|
||||
("autoscan", "Toggle auto-scan on/off"),
|
||||
("status", "Bot status, WS connection, live prices"),
|
||||
("cache", "Show data cache age per pair/TF"),
|
||||
("clearcache", "Wipe data cache, force fresh fetch"),
|
||||
("help", "All commands"),
|
||||
])
|
||||
|
||||
@@ -792,6 +830,8 @@ def main() -> None:
|
||||
app.add_handler(CommandHandler("help", cmd_help))
|
||||
app.add_handler(CommandHandler("status", cmd_status))
|
||||
app.add_handler(CommandHandler("autoscan", cmd_autoscan))
|
||||
app.add_handler(CommandHandler("cache", cmd_cache))
|
||||
app.add_handler(CommandHandler("clearcache", cmd_clearcache))
|
||||
app.add_handler(CallbackQueryHandler(cb_scan, pattern="^scan_"))
|
||||
|
||||
logger.info("Bot running — press Ctrl+C to stop")
|
||||
|
||||
Reference in New Issue
Block a user