From 39cebd3be32ced911d0c1997c5b7a9bccd5db69f Mon Sep 17 00:00:00 2001 From: chemavx Date: Sun, 26 Apr 2026 15:02:39 +0000 Subject: [PATCH] feat(notify): Telegram alerts on trade open and close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New module bot/notify/telegram.py — httpx async, fire-and-forget via asyncio.create_task, swallows all errors so notifications never affect trade execution. Three alert types: 📈/📉 TRADE ABIERTO — direction, size, edge_net (in execute()) ✅/❌ GANADO/PERDIDO — approx PnL (in close_position()) 🔒 LEGACY CLOSE — recovered capital + reason (in close_legacy_position()) close_position() and close_legacy_position() gain an optional question="" param so the message shows the market name instead of market_id. bot/main.py updated to pass question= to close_legacy_position(). Credentials (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID) read from env vars injected via bot-secrets k8s secret. Co-Authored-By: Claude Sonnet 4.6 --- bot/executor/paper.py | 19 ++++++++++++--- bot/main.py | 2 +- bot/notify/__init__.py | 0 bot/notify/telegram.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 bot/notify/__init__.py create mode 100644 bot/notify/telegram.py diff --git a/bot/executor/paper.py b/bot/executor/paper.py index 73145da..4c80a9c 100644 --- a/bot/executor/paper.py +++ b/bot/executor/paper.py @@ -4,6 +4,7 @@ Paper Trading Executor — simulates order execution without real money. Simulates realistic slippage and fees to get accurate paper P&L. All trades are logged to PostgreSQL for metrics analysis. """ +import asyncio import logging import uuid from dataclasses import dataclass, field @@ -12,6 +13,7 @@ from typing import Optional from bot.risk.manager import Order, Portfolio from bot.data.db import Database +from bot.notify import telegram log = logging.getLogger(__name__) @@ -183,9 +185,13 @@ class PaperExecutor: # Persist to DB await self._db.save_trade(trade) + asyncio.create_task( + telegram.trade_opened(trade.question, trade.direction, trade.size_usdc, trade.edge_net) + ) + return trade - async def close_legacy_position(self, market_id: str, reason: str) -> float: + async def close_legacy_position(self, market_id: str, reason: str, question: str = "") -> float: """ Close a paper position flagged by the legacy scan. @@ -200,9 +206,12 @@ class PaperExecutor: "LEGACY_CLOSE market=%s | returned $%.2f to cash | %s", market_id, cost, reason[:80], ) + asyncio.create_task( + telegram.trade_legacy_closed(question or market_id, cost, reason) + ) return cost - async def close_position(self, market_id: str, resolution: float) -> Optional[float]: + async def close_position(self, market_id: str, resolution: float, question: str = "") -> Optional[float]: """Close a paper position after market resolution. resolution: 1.0 if YES won, 0.0 if NO won. @@ -220,6 +229,10 @@ class PaperExecutor: reason=f"market_resolved resolution={resolution:.1f}", resolution=resolution, ) + approx_pnl = position_cost * resolution - position_cost log.info("Closed position in %s, resolution=%.1f", market_id, resolution) + asyncio.create_task( + telegram.trade_closed(question or market_id, approx_pnl) + ) # Approximate PnL: settlement value minus cost. Exact value is in close_pnl. - return position_cost * resolution - position_cost + return approx_pnl diff --git a/bot/main.py b/bot/main.py index de45d61..bd9b05c 100644 --- a/bot/main.py +++ b/bot/main.py @@ -347,7 +347,7 @@ async def run_legacy_scan( log.warning("PAPER MODE: auto-closing %d CLOSE_RECOMMENDED position(s)...", n_close) for p in enriched: if p["recommendation"] == "CLOSE_RECOMMENDED": - recovered = await executor.close_legacy_position(p["market_id"], p["rec_reason"]) + recovered = await executor.close_legacy_position(p["market_id"], p["rec_reason"], question=p["question"]) log.warning( " AUTO_CLOSED market=%s | $%.2f returned to cash | %s", p["market_id"], recovered, p["question"][:60], diff --git a/bot/notify/__init__.py b/bot/notify/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/notify/telegram.py b/bot/notify/telegram.py new file mode 100644 index 0000000..bbbd68e --- /dev/null +++ b/bot/notify/telegram.py @@ -0,0 +1,53 @@ +"""Telegram notifications — fire-and-forget, never blocks trade execution.""" +import logging +import os + +import httpx + +log = logging.getLogger(__name__) + +_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") +_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "") + + +async def _send(text: str) -> None: + """POST a message to Telegram. Swallows all errors silently.""" + if not _TOKEN or not _CHAT_ID: + return + try: + async with httpx.AsyncClient(timeout=10) as client: + resp = await client.post( + f"https://api.telegram.org/bot{_TOKEN}/sendMessage", + json={"chat_id": _CHAT_ID, "text": text}, + ) + if resp.status_code != 200: + log.warning("Telegram %d: %s", resp.status_code, resp.text[:200]) + except Exception as exc: + log.warning("Telegram notification failed: %s", exc) + + +async def trade_opened(question: str, direction: str, size_usdc: float, edge_net: float) -> None: + emoji = "📈" if direction == "BUY_YES" else "📉" + await _send( + f"{emoji} TRADE ABIERTO\n" + f"{question[:80]}\n" + f"Dir: {direction} Size: ${size_usdc:.2f} Edge: {edge_net:+.3f}" + ) + + +async def trade_closed(question: str, pnl: float) -> None: + emoji = "✅" if pnl > 0 else "❌" + result = "GANADO" if pnl > 0 else "PERDIDO" + await _send( + f"{emoji} {result}\n" + f"{question[:80]}\n" + f"PnL: {pnl:+.2f} USDC" + ) + + +async def trade_legacy_closed(question: str, recovered: float, reason: str) -> None: + await _send( + f"🔒 LEGACY CLOSE\n" + f"{question[:80]}\n" + f"Recuperado: ${recovered:.2f} | {reason[:60]}" + )