#!/usr/bin/env python3
"""
Notification Manager - Handles all bot notifications and messages.
"""
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime
from src.utils.price_formatter import get_formatter
logger = logging.getLogger(__name__)
class NotificationManager:
"""Handles all notification logic for the trading bot."""
def __init__(self):
"""Initialize the notification manager."""
self.bot_application = None
def set_bot_application(self, application):
"""Set the bot application for sending messages."""
self.bot_application = application
async def send_long_success_notification(self, query, token, amount, price, order_details, stop_loss_price=None, trade_lifecycle_id=None):
"""Send notification for successful long order."""
try:
# Use PriceFormatter for consistent formatting
formatter = get_formatter() # Get formatter
price_str = formatter.format_price_with_symbol(price, token)
amount_str = f"{amount:.6f} {token}"
value_str = formatter.format_price_with_symbol(amount * price, token)
order_id_str = order_details.get('id', 'N/A')
message = (
f"ā
Successfully opened LONG position for {amount_str} at ~{price_str}\n\n"
f"š° Value: {value_str}\n"
f"š Order ID: {order_id_str}
"
)
if trade_lifecycle_id:
message += f"\nš Lifecycle ID: {trade_lifecycle_id[:8]}...
"
if stop_loss_price:
sl_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
message += f"\nš Stop Loss pending at {sl_price_str}"
await query.edit_message_text(text=message, parse_mode='HTML')
except Exception as e:
logger.error(f"Error sending long success notification: {e}")
async def send_short_success_notification(self, query, token, amount, price, order_details, stop_loss_price=None, trade_lifecycle_id=None):
"""Send notification for successful short order."""
try:
formatter = get_formatter() # Get formatter
price_str = formatter.format_price_with_symbol(price, token)
amount_str = f"{amount:.6f} {token}"
value_str = formatter.format_price_with_symbol(amount * price, token)
order_id_str = order_details.get('id', 'N/A')
message = (
f"ā
Successfully opened SHORT position for {amount_str} at ~{price_str}\n\n"
f"š° Value: {value_str}\n"
f"š Order ID: {order_id_str}
"
)
if trade_lifecycle_id:
message += f"\nš Lifecycle ID: {trade_lifecycle_id[:8]}...
"
if stop_loss_price:
sl_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
message += f"\nš Stop Loss pending at {sl_price_str}"
await query.edit_message_text(text=message, parse_mode='HTML')
except Exception as e:
logger.error(f"Error sending short success notification: {e}")
async def send_exit_success_notification(self, query, token, position_type, amount, price, pnl, order_details, trade_lifecycle_id=None):
"""Send notification for successful exit order."""
try:
formatter = get_formatter() # Get formatter
# Price is the execution price, PnL is calculated based on it
# For market orders, price might be approximate or from fill later
price_str = formatter.format_price_with_symbol(price, token) if price > 0 else "Market Price"
amount_str = f"{amount:.6f} {token}"
pnl_str = formatter.format_price_with_symbol(pnl)
pnl_emoji = "š¢" if pnl >= 0 else "š“"
order_id_str = order_details.get('id', 'N/A')
cancelled_sl_count = order_details.get('cancelled_stop_losses', 0)
message = (
f"ā
Successfully closed {position_type} position for {amount_str}\n\n"
f"š Exit Order ID: {order_id_str}
\n"
# P&L and price are more reliably determined when MarketMonitor processes the fill.
# This notification confirms the exit order was PLACED.
f"ā³ Awaiting fill confirmation for final price and P&L."
)
if trade_lifecycle_id:
message += f"\nš Lifecycle ID: {trade_lifecycle_id[:8]}...
(Closed)"
if cancelled_sl_count > 0:
message += f"\nā ļø Cancelled {cancelled_sl_count} linked stop loss order(s)."
await query.edit_message_text(text=message, parse_mode='HTML')
except Exception as e:
logger.error(f"Error sending exit success notification: {e}")
async def send_sl_success_notification(self, query, token, position_type, amount, stop_price, order_details, trade_lifecycle_id=None):
"""Send notification for successful stop loss order setup."""
try:
formatter = get_formatter() # Get formatter
stop_price_str = formatter.format_price_with_symbol(stop_price, token)
amount_str = f"{amount:.6f} {token}"
order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
message = (
f"š Successfully set STOP LOSS for {position_type} {amount_str}\n\n"
f"šÆ Trigger Price: {stop_price_str}\n"
f"š SL Order ID: {order_id_str}
"
)
if trade_lifecycle_id:
message += f"\nš Linked to Lifecycle ID: {trade_lifecycle_id[:8]}...
"
await query.edit_message_text(text=message, parse_mode='HTML')
except Exception as e:
logger.error(f"Error sending SL success notification: {e}")
async def send_tp_success_notification(self, query, token, position_type, amount, tp_price, order_details, trade_lifecycle_id=None):
"""Send notification for successful take profit order setup."""
try:
formatter = get_formatter() # Get formatter
tp_price_str = formatter.format_price_with_symbol(tp_price, token)
amount_str = f"{amount:.6f} {token}"
order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
message = (
f"šÆ Successfully set TAKE PROFIT for {position_type} {amount_str}\n\n"
f"š° Target Price: {tp_price_str}\n"
f"š TP Order ID: {order_id_str}
"
)
if trade_lifecycle_id:
message += f"\nš Linked to Lifecycle ID: {trade_lifecycle_id[:8]}...
"
await query.edit_message_text(text=message, parse_mode='HTML')
except Exception as e:
logger.error(f"Error sending TP success notification: {e}")
async def send_coo_success_notification(self, query, token: str, cancelled_count: int,
failed_count: int, cancelled_linked_sls: int = 0,
cancelled_orders: List[Dict[str, Any]] = None):
"""Send notification for successful cancel all orders operation."""
success_message = f"""
ā
Cancel Orders Results
š Summary:
⢠Token: {token}
⢠Cancelled: {cancelled_count} orders
⢠Failed: {failed_count} orders
⢠Total Attempted: {cancelled_count + failed_count} orders"""
if cancelled_linked_sls > 0:
success_message += f"""
⢠š Linked Stop Losses Cancelled: {cancelled_linked_sls}"""
# Show details of cancelled orders if available
if cancelled_orders and len(cancelled_orders) > 0:
success_message += f"""
šļø Successfully Cancelled:"""
for order in cancelled_orders:
side = order.get('side', 'Unknown')
amount = order.get('amount', 0)
price = order.get('price', 0)
side_emoji = "š¢" if side.lower() == 'buy' else "š“"
success_message += f"""
{side_emoji} {side.upper()} {amount} @ ${price:,.2f}"""
# Overall status
if cancelled_count == (cancelled_count + failed_count) and failed_count == 0:
success_message += f"""
š All {token} orders successfully cancelled!"""
elif cancelled_count > 0:
success_message += f"""
ā ļø Some orders cancelled. {failed_count} failed."""
else:
success_message += f"""
ā Could not cancel any {token} orders."""
success_message += f"""
ā° Time: {datetime.now().strftime('%H:%M:%S')}
š Use /orders to verify no pending orders remain.
"""
await query.edit_message_text(success_message, parse_mode='HTML')
logger.info(f"Cancel orders complete: {token} - {cancelled_count} cancelled, {failed_count} failed{f', {cancelled_linked_sls} linked SLs cancelled' if cancelled_linked_sls > 0 else ''}")
async def send_alarm_triggered_notification(self, token: str, target_price: float,
current_price: float, direction: str):
"""Send notification when a price alarm is triggered."""
if not self.bot_application:
logger.warning("Bot application not set, cannot send alarm notification")
return
direction_emoji = "š" if direction == 'above' else "š"
alarm_message = f"""
š Price Alarm Triggered!
{direction_emoji} Alert Details:
⢠Token: {token}
⢠Target Price: ${target_price:,.2f}
⢠Current Price: ${current_price:,.2f}
⢠Direction: {direction.upper()}
ā° Trigger Time: {datetime.now().strftime('%H:%M:%S')}
š” Quick Actions:
⢠/market {token} - View market data
⢠/price {token} - Quick price check
⢠/long {token} [amount] - Open long position
⢠/short {token} [amount] - Open short position
"""
try:
from src.config.config import Config
if Config.TELEGRAM_CHAT_ID:
await self.bot_application.bot.send_message(
chat_id=Config.TELEGRAM_CHAT_ID,
text=alarm_message,
parse_mode='HTML'
)
logger.info(f"Alarm notification sent: {token} {direction} ${target_price}")
except Exception as e:
logger.error(f"Failed to send alarm notification: {e}")
async def send_external_trade_notification(self, symbol: str, side: str, amount: float,
price: float, action_type: str, timestamp: str):
"""Send notification for external trades detected."""
if not self.bot_application:
logger.warning("Bot application not set, cannot send external trade notification")
return
# Extract token from symbol
token = symbol.split('/')[0] if '/' in symbol else symbol
# Format timestamp
try:
trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
time_str = trade_time.strftime('%H:%M:%S')
except:
time_str = "Unknown"
# Format message based on action type
if action_type == "position_opened":
message = f"""
š Position Opened (External)
š Trade Details:
⢠Token: {token}
⢠Direction: {side.upper()}
⢠Size: {amount} {token}
⢠Entry Price: ${price:,.2f}
⢠Value: ${amount * price:,.2f}
ā
Status: New position opened externally
ā° Time: {time_str}
š± Use /positions to view all positions
"""
elif action_type == "position_closed":
message = f"""
šÆ Position Closed (External)
š Trade Details:
⢠Token: {token}
⢠Direction: {side.upper()}
⢠Size: {amount} {token}
⢠Exit Price: ${price:,.2f}
⢠Value: ${amount * price:,.2f}
ā
Status: Position closed externally
ā° Time: {time_str}
š Use /stats to view updated performance
"""
elif action_type == "position_increased":
message = f"""
š Position Increased (External)
š Trade Details:
⢠Token: {token}
⢠Direction: {side.upper()}
⢠Added Size: {amount} {token}
⢠Price: ${price:,.2f}
⢠Value: ${amount * price:,.2f}
ā
Status: Position size increased externally
ā° Time: {time_str}
š Use /positions to view current position
"""
else:
# Generic external trade notification
side_emoji = "š¢" if side.lower() == 'buy' else "š“"
message = f"""
š External Trade Detected
š Trade Details:
⢠Token: {token}
⢠Side: {side.upper()}
⢠Amount: {amount} {token}
⢠Price: ${price:,.2f}
⢠Value: ${amount * price:,.2f}
{side_emoji} Source: External Platform Trade
ā° Time: {time_str}
š Note: This trade was executed outside the Telegram bot
š Stats have been automatically updated
"""
try:
from src.config.config import Config
if Config.TELEGRAM_CHAT_ID:
await self.bot_application.bot.send_message(
chat_id=Config.TELEGRAM_CHAT_ID,
text=message,
parse_mode='HTML'
)
logger.info(f"External trade notification sent: {action_type} for {token}")
except Exception as e:
logger.error(f"Failed to send external trade notification: {e}")
async def send_generic_notification(self, message: str):
"""Send a generic notification message."""
if not self.bot_application:
logger.warning("Bot application not set, cannot send generic notification")
return
try:
from src.config.config import Config
if Config.TELEGRAM_CHAT_ID:
await self.bot_application.bot.send_message(
chat_id=Config.TELEGRAM_CHAT_ID,
text=message,
parse_mode='HTML'
)
logger.info("Generic notification sent")
except Exception as e:
logger.error(f"Failed to send generic notification: {e}")
async def send_stop_loss_execution_notification(self, stop_loss_info: Dict, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str):
"""Send notification for external stop loss execution."""
try:
token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
# Extract stop loss details
trigger_price = stop_loss_info.get('trigger_price', price)
position_side = stop_loss_info.get('position_side', 'unknown')
entry_price = stop_loss_info.get('entry_price', 0)
detected_at = stop_loss_info.get('detected_at')
# Format timestamp
try:
time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
time_str = time_obj.strftime('%H:%M:%S')
except:
time_str = "Unknown"
# Calculate P&L if we have entry price
pnl_info = ""
if entry_price > 0:
if position_side == 'long':
pnl = amount * (price - entry_price)
pnl_percent = ((price - entry_price) / entry_price) * 100
else: # short
pnl = amount * (entry_price - price)
pnl_percent = ((entry_price - price) / entry_price) * 100
pnl_emoji = "š¢" if pnl >= 0 else "š“"
pnl_info = f"""
{pnl_emoji} Stop Loss P&L:
⢠Entry Price: ${entry_price:,.2f}
⢠Exit Price: ${price:,.2f}
⢠Realized P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
⢠Result: {"PROFIT" if pnl >= 0 else "LOSS PREVENTION"}"""
# Determine stop loss effectiveness
effectiveness = ""
if entry_price > 0 and trigger_price > 0:
if position_side == 'long':
# For longs, stop loss should trigger below entry
if trigger_price < entry_price:
loss_percent = ((trigger_price - entry_price) / entry_price) * 100
effectiveness = f"⢠Loss Limited: {loss_percent:.1f}% ā
"
else:
effectiveness = "⢠Unusual: Stop above entry š”"
else: # short
# For shorts, stop loss should trigger above entry
if trigger_price > entry_price:
loss_percent = ((entry_price - trigger_price) / entry_price) * 100
effectiveness = f"⢠Loss Limited: {loss_percent:.1f}% ā
"
else:
effectiveness = "⢠Unusual: Stop below entry š”"
trade_value = amount * price
position_emoji = "š" if position_side == 'long' else "š"
message = f"""
š STOP LOSS EXECUTED
{position_emoji} {position_side.upper()} Position Protected:
⢠Token: {token}
⢠Position Type: {position_side.upper()}
⢠Stop Loss Size: {amount} {token}
⢠Trigger Price: ${trigger_price:,.2f}
⢠Execution Price: ${price:,.2f}
⢠Exit Value: ${trade_value:,.2f}
šÆ Stop Loss Details:
⢠Status: EXECUTED ā
⢠Order Side: {side.upper()}
⢠Action Type: {action_type.replace('_', ' ').title()}
{effectiveness}
{pnl_info}
ā° Execution Time: {time_str}
š¤ Source: External Hyperliquid Order
š Risk Management: Loss prevention successful
š” Your external stop loss order worked as intended!
"""
await self.send_notification(message.strip())
logger.info(f"š Stop loss execution notification sent: {token} {position_side} @ ${price:.2f}")
except Exception as e:
logger.error(f"ā Error sending stop loss execution notification: {e}")