update 03-22 09:28

This commit is contained in:
2026-03-22 09:28:14 +09:00
commit 7f45211276
43 changed files with 9373 additions and 0 deletions

152
src/utils/telegram.py Normal file
View 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)