"""Telegram notification bot for trade alerts and daily summaries.""" from __future__ import annotations import asyncio from typing import Optional import structlog from src.config import NotificationConfig log = structlog.get_logger() class TelegramNotifier: """Sends trading notifications via Telegram Bot API. Uses aiohttp directly (no python-telegram-bot dependency for async) for lightweight non-blocking notification delivery. """ BASE_URL = "https://api.telegram.org/bot{token}/sendMessage" def __init__(self, config: NotificationConfig) -> None: self.config = config self._enabled = config.telegram_enabled and bool(config.telegram_token) and bool(config.telegram_chat_id) self._session = None if not self._enabled: log.info("telegram_disabled", reason="missing token or chat_id") async def _get_session(self): if self._session is None: import aiohttp self._session = aiohttp.ClientSession() return self._session async def close(self) -> None: if self._session: await self._session.close() self._session = None async def send(self, message: str, parse_mode: str = "HTML") -> bool: """Send a message to the configured Telegram chat.""" if not self._enabled: return False url = self.BASE_URL.format(token=self.config.telegram_token) payload = { "chat_id": self.config.telegram_chat_id, "text": message, "parse_mode": parse_mode, } try: session = await self._get_session() async with session.post(url, json=payload, timeout=10) as resp: if resp.status == 200: return True else: body = await resp.text() log.warning("telegram_send_failed", status=resp.status, body=body[:200]) return False except Exception: log.exception("telegram_send_error") return False # ------------------------------------------------------------------ # Convenience methods # ------------------------------------------------------------------ async def notify_trade( self, asset: str, direction: str, timeframe: str, price: float, size: int, edge: float, ) -> None: """Send a trade notification.""" if not self.config.notify_on_trade: return msg = ( f"🔔 Trade Signal\n" f"Asset: {asset} | {direction}\n" f"Timeframe: {timeframe}\n" f"Price: {price:.2f} | Size: {size}\n" f"Edge: {edge:.2%}" ) await self.send(msg) async def notify_fill( self, asset: str, direction: str, fill_price: float, fill_size: int, trade_id: str, ) -> None: """Send a fill notification.""" if not self.config.notify_on_trade: return msg = ( f"✅ Order Filled\n" f"Asset: {asset} | {direction}\n" f"Fill: {fill_price:.2f} × {fill_size}\n" f"Trade ID: {trade_id}" ) await self.send(msg) async def notify_daily_summary( self, date: str, total_trades: int, wins: int, losses: int, pnl: float, fees: float, volume: float, ) -> None: """Send daily summary.""" if not self.config.notify_on_daily_summary: return win_rate = wins / total_trades * 100 if total_trades > 0 else 0 emoji = "📈" if pnl >= 0 else "📉" msg = ( f"{emoji} Daily Summary — {date}\n" f"Trades: {total_trades} (W:{wins} / L:{losses})\n" f"Win Rate: {win_rate:.1f}%\n" f"PnL: ${pnl:+.2f}\n" f"Fees: ${fees:.2f}\n" f"Volume: ${volume:,.0f}" ) await self.send(msg) async def notify_error(self, error_msg: str) -> None: """Send an error alert.""" if not self.config.notify_on_error: return msg = f"🚨 Error Alert\n{error_msg[:500]}" await self.send(msg) async def notify_halt(self, reason: str) -> None: """Send a trading halt alert.""" msg = f"🛑 TRADING HALTED\nReason: {reason}" await self.send(msg)