Telegram bot: gated push alerts + standalone Ollama Q&A

C1 — runtime/notifier.py: gated, fire-and-forget push alerts that NEVER raise
into the loop. Decoupled by POLLING the event store (no notifier threaded
through breaker/exit/execution). Pushes on major events only: daily-loss
breaker, catastrophe-stop, naked-stop flatten, any |closed trade| > $300, and a
once-per-day EOD summary. Wired as a 45s scheduler step + EOD summary/failure
hooks in step_eod_flatten. Boot ping on start. Silent no-op when unconfigured.

C2 — telegram_bot.py: standalone (separate process) READ-ONLY Q&A listener,
locked to TELEGRAM_CHAT_ID. Long-polls getUpdates; gathers a Postgres data
snapshot (today P&L, open positions, recent trades, all-time) and asks the
local Ollama model. Falls back to the raw snapshot if Ollama is down/disabled;
refuses any action command (buy/sell/flatten) — questions only.

Tests: disabled no-op, major-event dedup, EOD-once, auth gate, action-refusal,
ollama-down + ollama-disabled fallback. 8/8 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-23 16:29:15 -04:00
1 parent e3ac89c2f8
commit 59e296a736
4 files changed
+445

No files matched your search

+24
View File
@@ -814,6 +814,20 @@ def main(argv: list[str] | None = None) -> int:
"path — it will not affect live entries (see entry-path-fast-vs-auto)."
)
# Telegram push alerts — gated, decoupled (polls the event store), and
# fire-and-forget so it can never impede trading.
from runtime.notifier import TelegramNotifier, send_telegram
notifier = TelegramNotifier(rt["store"])
if notifier.enabled:
print("[boot] telegram alerts: ON")
send_telegram("\U0001F916 Momentum loop started — push alerts armed.")
else:
print("[boot] telegram alerts: off (TELEGRAM_BOT_TOKEN/CHAT_ID unset)")
def step_notify():
n = notifier.poll()
return f"telegram: {n} alert(s)" if n else None
def step_manage_exits():
if exit_mgr is None:
return None
@@ -866,7 +880,15 @@ def main(argv: list[str] | None = None) -> int:
gap = days_to_next_session(now_et.date())
sev = f"‼ NAKED-CARRY RISK ({gap}-day gap ahead): " if pre_closure else ""
print(f"[{datetime.now():%H:%M:%S}] {sev}{tag} could not flatten: {res['errors']}", flush=True)
send_telegram(f"⚠️ {tag} FAILED to flatten: {res['errors']} — {sev or 'will retry'}")
return f"{sev}{tag} closed [{closed}] errors={res['errors']} (will retry)"
# success — push the once-per-day EOD P&L summary
try:
from storage.projections import query_session_pnl
notifier.send_eod_summary(now_et.date().isoformat(),
query_session_pnl(rt["store"]))
except Exception: # noqa: BLE001
pass
return f"{tag} closed [{closed}]"
# --- open catch-up flatten: a day-trading book should be FLAT by the open;
@@ -955,6 +977,8 @@ def main(argv: list[str] | None = None) -> int:
enabled=_flag("TRADING_EXECUTION_ENABLED", "1"))
scheduler.add("eod", step_eod_flatten, float(os.environ.get("TRADING_EOD_INTERVAL_SECONDS", "30")),
enabled=_flag("TRADING_EXECUTION_ENABLED", "1"))
scheduler.add("notify", step_notify, float(os.environ.get("TELEGRAM_NOTIFY_INTERVAL_SECONDS", "45")),
enabled=notifier.enabled)
scheduler.add("catchup", step_open_catchup_flatten,
float(os.environ.get("TRADING_OPEN_CATCHUP_INTERVAL_SECONDS", "30")),
enabled=_flag("TRADING_EXECUTION_ENABLED", "1"))