update 03-22 09:28
This commit is contained in:
152
src/utils/telegram.py
Normal file
152
src/utils/telegram.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user