#!/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}")