Tune 5M signals, fix XAU regression, add signal dedup cooldown

EUR/USD 5M: raise min_entry_score 28→35, block hours 4/19/21
  → 83.4% WR, 145 trades, +9.68R, PF 1.42 (120-day)

XAU/USD 5M: fix B-grade exclusion regression in setup_grades,
  add breakeven trail (trigger 0.15R, buffer 0.05R)
  → 84.9% WR, 4810 trades, +164.54R, PF 1.23 (730-day)

EUR/USD M15: tighten defaults (SL 1.5x ATR, TP 1.0x, min_rr 0.5,
  block ranging+transitioning, BE trigger 0.2R)

Backtest: multiprocess fork parallelism, vectorized pre-filter,
  CUDA preload fix, per-pair BE trigger routing

Telegram bot: add signal deduplication — suppress repeated
  same-direction messages within 5-bar cooldown per pair+TF.
  Resend on direction flip, quality upgrade, or cooldown expiry.
  Manual scans always ungated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-19 20:19:11 -04:00
parent 9e5410d617
commit b7ec5341cf
7 changed files with 280 additions and 151 deletions
+51 -7
View File
@@ -86,6 +86,8 @@ _LEVEL_KEYS = (
_signal_stats: Dict[str, Dict[str, SignalReport]] = {}
_stats_updated: Optional[float] = None
_last_sent: Dict[str, Dict] = {}
SCAN_BUTTONS = [
{"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "XAU 5m"},
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "XAU 15m"},
@@ -468,6 +470,37 @@ def _seconds_until_boundary(interval_minutes: int, buffer: int = 10) -> float:
return remaining
_QUALITY_RANK = {"A+": 4, "A": 3, "B": 2, "C": 1}
def _should_send(report: SignalReport, tf: Timeframe) -> bool:
if not report.trade_setup:
return False
key = f"{report.symbol}:{tf.value}"
prev = _last_sent.get(key)
if prev is None:
return True
direction = report.trade_setup.direction
quality = report.signal_quality
if direction != prev["direction"]:
return True
if _QUALITY_RANK.get(quality, 0) > _QUALITY_RANK.get(prev["quality"], 0):
return True
cooldown = 5 * _TF_SECONDS.get(tf, 300)
if time.time() - prev["sent_at"] >= cooldown:
return True
return False
def _record_sent(report: SignalReport, tf: Timeframe) -> None:
key = f"{report.symbol}:{tf.value}"
_last_sent[key] = {
"direction": report.trade_setup.direction,
"quality": report.signal_quality,
"sent_at": time.time(),
}
async def _scan_timeframe(
bot,
tf: Timeframe,
@@ -492,13 +525,19 @@ async def _scan_timeframe(
await _refresh_dashboard(bot)
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
for chat_id in list(_autoscan_chats):
for report in actionable:
detail = format_signal_message(report)
try:
await bot.send_message(chat_id, detail, parse_mode=ParseMode.MARKDOWN)
except Exception as exc:
logger.warning(f"Failed to send signal for chat {chat_id}: {exc}")
sendable = [r for r in actionable if _should_send(r, tf)]
for report in sendable:
_record_sent(report, tf)
if sendable:
for chat_id in list(_autoscan_chats):
for report in sendable:
detail = format_signal_message(report)
try:
await bot.send_message(chat_id, detail, parse_mode=ParseMode.MARKDOWN)
except Exception as exc:
logger.warning(f"Failed to send signal for chat {chat_id}: {exc}")
elif actionable:
logger.info(f"Auto-scan {tf.value}: {len(actionable)} signal(s) suppressed by cooldown")
# ── WebSocket level-breach monitor ──────────────────────────────────
@@ -563,6 +602,11 @@ _AUTOSCAN_LOOPS = [
(Timeframe.H4, 240),
]
_TF_SECONDS = {
Timeframe.M5: 300, Timeframe.M15: 900,
Timeframe.H1: 3600, Timeframe.H4: 14400,
}
def _start_scheduler(bot) -> None:
global _dashboard_task