Files
crypto_news/notification/telegram_bot.py

164 lines
4.9 KiB
Python
Raw Normal View History

2026-03-20 07:49:42 +09:00
"""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)