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:
+51
-7
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user