"""Telegram notification service. Sends formatted trade alerts, daily reports, and error notifications to a configured Telegram chat. """ from __future__ import annotations from typing import Optional from loguru import logger from config import settings try: from telegram import Bot from telegram.constants import ParseMode except ImportError: Bot = None # type: ignore ParseMode = None # type: ignore logger.warning("python-telegram-bot not installed") class TelegramNotifier: """Send trading notifications via Telegram.""" def __init__( self, token: str | None = None, chat_id: str | None = None, ): self._token = token or settings.TELEGRAM_BOT_TOKEN self._chat_id = chat_id or settings.TELEGRAM_CHAT_ID self._bot: Optional[object] = None self._enabled = bool(self._token and self._chat_id and Bot is not None) async def _get_bot(self): if self._bot is None and Bot is not None: self._bot = Bot(token=self._token) return self._bot async def _send(self, text: str) -> None: """Send a message to the configured chat.""" if not self._enabled: logger.debug("Telegram disabled, skipping message") return try: bot = await self._get_bot() await bot.send_message( chat_id=self._chat_id, text=text, parse_mode=ParseMode.HTML if ParseMode else None, ) except Exception as e: logger.error("Telegram send failed: {}", e) # ------------------------------------------------------------------ # Signal alerts # ------------------------------------------------------------------ async def send_signal( self, symbol: str, direction: str, entry_price: float, stop_loss: float, take_profit: float, confluence: int, reasons: list[str] | None = None, ) -> None: """Send a new trade signal notification.""" sl_pct = abs(entry_price - stop_loss) / entry_price * 100 tp_pct = abs(take_profit - entry_price) / entry_price * 100 reasons_str = " + ".join(reasons) if reasons else "N/A" text = ( f"ICT Signal Detected\n" f"{'=' * 24}\n" f"Symbol : {symbol}\n" f"Direction: {direction}\n" f"Entry : ${entry_price:,.2f}\n" f"SL : ${stop_loss:,.2f} (-{sl_pct:.2f}%)\n" f"TP : ${take_profit:,.2f} (+{tp_pct:.2f}%)\n" f"Confluence: {confluence}/6\n" f"Reasons : {reasons_str}" ) await self._send(text) async def send_fill( self, symbol: str, side: str, amount: float, price: float, order_type: str = "market", ) -> None: """Notify when an order is filled.""" text = ( f"Order Filled\n" f"{'=' * 24}\n" f"Symbol: {symbol}\n" f"Side : {side.upper()}\n" f"Type : {order_type}\n" f"Amount: {amount:.6f}\n" f"Price : ${price:,.2f}" ) await self._send(text) async def send_close( self, symbol: str, direction: str, entry_price: float, exit_price: float, pnl: float, reason: str, ) -> None: """Notify when a position is closed.""" emoji = "+" if pnl >= 0 else "" text = ( f"Position Closed\n" f"{'=' * 24}\n" f"Symbol : {symbol}\n" f"Direction: {direction}\n" f"Entry : ${entry_price:,.2f}\n" f"Exit : ${exit_price:,.2f}\n" f"PnL : {emoji}${pnl:,.2f}\n" f"Reason : {reason}" ) await self._send(text) async def send_daily_report( self, date_str: str, total_trades: int, winning: int, losing: int, total_pnl: float, win_rate: float, balance: float, ) -> None: """Send end-of-day performance summary.""" text = ( f"Daily Report - {date_str}\n" f"{'=' * 24}\n" f"Trades : {total_trades}\n" f"Wins : {winning}\n" f"Losses : {losing}\n" f"Win Rate : {win_rate:.1%}\n" f"PnL : ${total_pnl:,.2f}\n" f"Balance : ${balance:,.2f}" ) await self._send(text) async def send_error(self, error: str) -> None: """Send an error notification.""" text = f"BOT ERROR\n{'=' * 24}\n{error}" await self._send(text) async def send_emergency(self, msg: str) -> None: """Send an emergency stop notification.""" text = f"EMERGENCY STOP\n{'=' * 24}\n{msg}" await self._send(text)