deploy: 2026-03-20 07:49
This commit is contained in:
0
notification/__init__.py
Normal file
0
notification/__init__.py
Normal file
59
notification/alert_manager.py
Normal file
59
notification/alert_manager.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Alert manager -- unified notification dispatch.
|
||||
|
||||
Coordinates sending notifications through multiple channels
|
||||
(currently Telegram, extensible to Discord, email, etc.).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from notification.telegram_bot import TelegramNotifier
|
||||
|
||||
|
||||
class AlertManager:
|
||||
"""Dispatch alerts to all configured notification channels."""
|
||||
|
||||
def __init__(self, notifiers: List | None = None):
|
||||
if notifiers is None:
|
||||
self._notifiers = [TelegramNotifier()]
|
||||
else:
|
||||
self._notifiers = notifiers
|
||||
|
||||
async def notify_signal(self, signal_data: dict) -> None:
|
||||
"""Broadcast a trade signal to all channels."""
|
||||
for n in self._notifiers:
|
||||
try:
|
||||
await n.send_signal(**signal_data)
|
||||
except Exception as e:
|
||||
logger.error("Alert dispatch failed: {}", e)
|
||||
|
||||
async def notify_fill(self, fill_data: dict) -> None:
|
||||
for n in self._notifiers:
|
||||
try:
|
||||
await n.send_fill(**fill_data)
|
||||
except Exception as e:
|
||||
logger.error("Alert dispatch failed: {}", e)
|
||||
|
||||
async def notify_close(self, close_data: dict) -> None:
|
||||
for n in self._notifiers:
|
||||
try:
|
||||
await n.send_close(**close_data)
|
||||
except Exception as e:
|
||||
logger.error("Alert dispatch failed: {}", e)
|
||||
|
||||
async def notify_error(self, error: str) -> None:
|
||||
for n in self._notifiers:
|
||||
try:
|
||||
await n.send_error(error)
|
||||
except Exception as e:
|
||||
logger.error("Alert dispatch failed: {}", e)
|
||||
|
||||
async def notify_emergency(self, msg: str) -> None:
|
||||
for n in self._notifiers:
|
||||
try:
|
||||
await n.send_emergency(msg)
|
||||
except Exception as e:
|
||||
logger.error("Alert dispatch failed: {}", e)
|
||||
163
notification/telegram_bot.py
Normal file
163
notification/telegram_bot.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user