164 lines
4.9 KiB
Python
164 lines
4.9 KiB
Python
"""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"<b>ICT Signal Detected</b>\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"<b>Order Filled</b>\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"<b>Position Closed</b>\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"<b>Daily Report - {date_str}</b>\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"<b>BOT ERROR</b>\n{'=' * 24}\n{error}"
|
|
await self._send(text)
|
|
|
|
async def send_emergency(self, msg: str) -> None:
|
|
"""Send an emergency stop notification."""
|
|
text = f"<b>EMERGENCY STOP</b>\n{'=' * 24}\n{msg}"
|
|
await self._send(text)
|