Files
polymarket-arb-bot/src/utils/telegram.py
2026-03-22 09:28:14 +09:00

153 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"🔔 <b>Trade Signal</b>\n"
f"Asset: <b>{asset}</b> | {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"✅ <b>Order Filled</b>\n"
f"Asset: {asset} | {direction}\n"
f"Fill: {fill_price:.2f} × {fill_size}\n"
f"Trade ID: <code>{trade_id}</code>"
)
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} <b>Daily Summary — {date}</b>\n"
f"Trades: {total_trades} (W:{wins} / L:{losses})\n"
f"Win Rate: {win_rate:.1f}%\n"
f"PnL: <b>${pnl:+.2f}</b>\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"🚨 <b>Error Alert</b>\n<code>{error_msg[:500]}</code>"
await self.send(msg)
async def notify_halt(self, reason: str) -> None:
"""Send a trading halt alert."""
msg = f"🛑 <b>TRADING HALTED</b>\nReason: {reason}"
await self.send(msg)