Spaces:
Running
Running
Commit ·
fd39eb4
1
Parent(s): 8de16b3
Alert gate: confidence >=7.0 AND PA grade A/A+ (was conf>=8.8, no grade check)
Browse files- main.py +24 -7
- telegram_alert.py +19 -4
main.py
CHANGED
|
@@ -60,6 +60,27 @@ def is_synthetic(symbol: str) -> bool:
|
|
| 60 |
return any(p.search(symbol) for p in _SYNTHETIC_PATTERNS)
|
| 61 |
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# ── Website -> Telegram alert config sync ──────────────────────────────────
|
| 64 |
# Root cause fixed here: the dashboard's account/risk/top/leverage settings
|
| 65 |
# used to live only in the browser tab and were never seen by the background
|
|
@@ -120,9 +141,7 @@ async def _alert_loop():
|
|
| 120 |
try:
|
| 121 |
card = score_symbol(src, c["symbol"], alert_account, alert_risk_pct,
|
| 122 |
secondary=secondary, user_leverage=alert_leverage)
|
| 123 |
-
|
| 124 |
-
vetoed = card.get("vetoed", False)
|
| 125 |
-
if conf >= 8.8 and not vetoed:
|
| 126 |
tv_sym = src.tv_symbol(card["symbol"])
|
| 127 |
sent = await send_alert(card, tv_sym)
|
| 128 |
# Only log alerts that actually reached Telegram (sent=True) —
|
|
@@ -331,11 +350,9 @@ def scan(top: int = 8, min_volume: float = None, account: float = 100000,
|
|
| 331 |
log_signal(card)
|
| 332 |
except Exception:
|
| 333 |
pass
|
| 334 |
-
# Fire Telegram alert
|
| 335 |
try:
|
| 336 |
-
|
| 337 |
-
vetoed = card.get("vetoed", False)
|
| 338 |
-
if conf >= 8.8 and not vetoed and _event_loop is not None:
|
| 339 |
from telegram_alert import send_alert
|
| 340 |
tv_sym = src.tv_symbol(card["symbol"])
|
| 341 |
asyncio.run_coroutine_threadsafe(
|
|
|
|
| 60 |
return any(p.search(symbol) for p in _SYNTHETIC_PATTERNS)
|
| 61 |
|
| 62 |
|
| 63 |
+
# ── Alert trigger rule (confidence + PA confluence grade) ──────────────────
|
| 64 |
+
# Changed 13 Jul 2026 per explicit instruction: previously any card with
|
| 65 |
+
# confidence >= 8.8 fired an alert, no grade check. Now the confidence floor
|
| 66 |
+
# is lowered to 7.0, but a chart-pattern (confluence) grade of A or A+ is
|
| 67 |
+
# REQUIRED regardless of how high confidence is — grade B/C/D never fires,
|
| 68 |
+
# even at confidence 9+. Single shared gate used by both the background loop
|
| 69 |
+
# and manual /api/scan so the two paths can't drift apart again (that exact
|
| 70 |
+
# drift was the earlier website/Telegram discrepancy bug).
|
| 71 |
+
ALERT_CONF_MIN = 7.0
|
| 72 |
+
ALERT_GRADES_OK = {"A+", "A"}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _passes_alert_gate(card: dict) -> bool:
|
| 76 |
+
if card.get("vetoed", False):
|
| 77 |
+
return False
|
| 78 |
+
if card.get("confidence", 0) < ALERT_CONF_MIN:
|
| 79 |
+
return False
|
| 80 |
+
grade = (card.get("price_action") or {}).get("confluence_grade")
|
| 81 |
+
return grade in ALERT_GRADES_OK
|
| 82 |
+
|
| 83 |
+
|
| 84 |
# ── Website -> Telegram alert config sync ──────────────────────────────────
|
| 85 |
# Root cause fixed here: the dashboard's account/risk/top/leverage settings
|
| 86 |
# used to live only in the browser tab and were never seen by the background
|
|
|
|
| 141 |
try:
|
| 142 |
card = score_symbol(src, c["symbol"], alert_account, alert_risk_pct,
|
| 143 |
secondary=secondary, user_leverage=alert_leverage)
|
| 144 |
+
if _passes_alert_gate(card):
|
|
|
|
|
|
|
| 145 |
tv_sym = src.tv_symbol(card["symbol"])
|
| 146 |
sent = await send_alert(card, tv_sym)
|
| 147 |
# Only log alerts that actually reached Telegram (sent=True) —
|
|
|
|
| 350 |
log_signal(card)
|
| 351 |
except Exception:
|
| 352 |
pass
|
| 353 |
+
# Fire Telegram alert per _passes_alert_gate (confidence >= 7.0 AND PA grade A/A+)
|
| 354 |
try:
|
| 355 |
+
if _passes_alert_gate(card) and _event_loop is not None:
|
|
|
|
|
|
|
| 356 |
from telegram_alert import send_alert
|
| 357 |
tv_sym = src.tv_symbol(card["symbol"])
|
| 358 |
asyncio.run_coroutine_threadsafe(
|
telegram_alert.py
CHANGED
|
@@ -106,15 +106,28 @@ def _rr(card: dict) -> str:
|
|
| 106 |
def _tier_line(conf: float) -> str:
|
| 107 |
"""Confidence tier prefix (Strategy Fix Plan F1a).
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
| 112 |
"""
|
| 113 |
if conf >= 9.0:
|
| 114 |
return "💎 <b>TRADE-GRADE</b>"
|
| 115 |
if conf >= 8.8:
|
| 116 |
return "🔭 <b>WATCH ONLY</b> — below auto-trade bar"
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
|
| 120 |
def _momentum_line(card: dict) -> str:
|
|
@@ -184,12 +197,14 @@ def build_message(card: dict, tv_sym: str) -> str:
|
|
| 184 |
)
|
| 185 |
flow_line = _order_flow_line(card)
|
| 186 |
tier_line = _tier_line(float(conf))
|
|
|
|
| 187 |
momentum_line = _momentum_line(card)
|
| 188 |
tp_dyn = lvl.get("tp_dyn")
|
| 189 |
|
| 190 |
lines = [
|
| 191 |
*([tier_line] if tier_line else []),
|
| 192 |
f"{dir_emoji} <b>{sym} {direction} · {conf:.1f}/10</b>",
|
|
|
|
| 193 |
"",
|
| 194 |
f"📍 <b>Entry</b> <code>{_fmt_price(card.get('close'))}</code>",
|
| 195 |
f"🛑 <b>SL</b> <code>{_fmt_price(lvl.get('sl'))}</code>",
|
|
|
|
| 106 |
def _tier_line(conf: float) -> str:
|
| 107 |
"""Confidence tier prefix (Strategy Fix Plan F1a).
|
| 108 |
|
| 109 |
+
Cosmetic label only — the actual send/no-send decision lives in
|
| 110 |
+
main.py's _passes_alert_gate() (confidence >= 7.0 AND PA grade A/A+,
|
| 111 |
+
changed 13 Jul 2026). This just tells Utkarsh at a glance where in the
|
| 112 |
+
confidence range a delivered alert sits.
|
| 113 |
"""
|
| 114 |
if conf >= 9.0:
|
| 115 |
return "💎 <b>TRADE-GRADE</b>"
|
| 116 |
if conf >= 8.8:
|
| 117 |
return "🔭 <b>WATCH ONLY</b> — below auto-trade bar"
|
| 118 |
+
if conf >= 8.0:
|
| 119 |
+
return "🟡 <b>STRONG</b>"
|
| 120 |
+
return "🟠 <b>WATCH</b>"
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _grade_line(card: dict) -> str:
|
| 124 |
+
"""PA confluence grade line — required by the alert gate itself now, so
|
| 125 |
+
surface it on every message (grade is always A or A+ for delivered
|
| 126 |
+
alerts, since B/C/D never pass _passes_alert_gate)."""
|
| 127 |
+
grade = (card.get("price_action") or {}).get("confluence_grade")
|
| 128 |
+
if not grade:
|
| 129 |
+
return ""
|
| 130 |
+
return f"📐 <b>Grade {grade}</b>"
|
| 131 |
|
| 132 |
|
| 133 |
def _momentum_line(card: dict) -> str:
|
|
|
|
| 197 |
)
|
| 198 |
flow_line = _order_flow_line(card)
|
| 199 |
tier_line = _tier_line(float(conf))
|
| 200 |
+
grade_line = _grade_line(card)
|
| 201 |
momentum_line = _momentum_line(card)
|
| 202 |
tp_dyn = lvl.get("tp_dyn")
|
| 203 |
|
| 204 |
lines = [
|
| 205 |
*([tier_line] if tier_line else []),
|
| 206 |
f"{dir_emoji} <b>{sym} {direction} · {conf:.1f}/10</b>",
|
| 207 |
+
*([grade_line] if grade_line else []),
|
| 208 |
"",
|
| 209 |
f"📍 <b>Entry</b> <code>{_fmt_price(card.get('close'))}</code>",
|
| 210 |
f"🛑 <b>SL</b> <code>{_fmt_price(lvl.get('sl'))}</code>",
|