123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- #!/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 <b>LONG</b> position for {amount_str} at ~{price_str}\n\n"
- f"💰 Value: {value_str}\n"
- f"🆔 Order ID: <code>{order_id_str}</code>"
- )
- if trade_lifecycle_id:
- message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
- 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 <b>SHORT</b> position for {amount_str} at ~{price_str}\n\n"
- f"💰 Value: {value_str}\n"
- f"🆔 Order ID: <code>{order_id_str}</code>"
- )
- if trade_lifecycle_id:
- message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
- 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 <b>{position_type}</b> position for {amount_str}\n\n"
- f"🆔 Exit Order ID: <code>{order_id_str}</code>\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: <code>{trade_lifecycle_id[:8]}...</code> (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 <b>STOP LOSS</b> for {position_type} {amount_str}\n\n"
- f"🎯 Trigger Price: {stop_price_str}\n"
- f"🆔 SL Order ID: <code>{order_id_str}</code>"
- )
- if trade_lifecycle_id:
- message += f"\n🆔 Linked to Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
- 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 <b>TAKE PROFIT</b> for {position_type} {amount_str}\n\n"
- f"💰 Target Price: {tp_price_str}\n"
- f"🆔 TP Order ID: <code>{order_id_str}</code>"
- )
- if trade_lifecycle_id:
- message += f"\n🆔 Linked to Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
-
- 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"""
- ✅ <b>Cancel Orders Results</b>
- 📊 <b>Summary:</b>
- • 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"""
- 🗑️ <b>Successfully Cancelled:</b>"""
- 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"""
- ⏰ <b>Time:</b> {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"""
- 🔔 <b>Price Alarm Triggered!</b>
- {direction_emoji} <b>Alert Details:</b>
- • Token: {token}
- • Target Price: ${target_price:,.2f}
- • Current Price: ${current_price:,.2f}
- • Direction: {direction.upper()}
- ⏰ <b>Trigger Time:</b> {datetime.now().strftime('%H:%M:%S')}
- 💡 <b>Quick Actions:</b>
- • /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"""
- 🚀 <b>Position Opened (External)</b>
- 📊 <b>Trade Details:</b>
- • Token: {token}
- • Direction: {side.upper()}
- • Size: {amount} {token}
- • Entry Price: ${price:,.2f}
- • Value: ${amount * price:,.2f}
- ✅ <b>Status:</b> New position opened externally
- ⏰ <b>Time:</b> {time_str}
- 📱 Use /positions to view all positions
- """
- elif action_type == "position_closed":
- message = f"""
- 🎯 <b>Position Closed (External)</b>
- 📊 <b>Trade Details:</b>
- • Token: {token}
- • Direction: {side.upper()}
- • Size: {amount} {token}
- • Exit Price: ${price:,.2f}
- • Value: ${amount * price:,.2f}
- ✅ <b>Status:</b> Position closed externally
- ⏰ <b>Time:</b> {time_str}
- 📊 Use /stats to view updated performance
- """
- elif action_type == "position_increased":
- message = f"""
- 📈 <b>Position Increased (External)</b>
- 📊 <b>Trade Details:</b>
- • Token: {token}
- • Direction: {side.upper()}
- • Added Size: {amount} {token}
- • Price: ${price:,.2f}
- • Value: ${amount * price:,.2f}
- ✅ <b>Status:</b> Position size increased externally
- ⏰ <b>Time:</b> {time_str}
- 📈 Use /positions to view current position
- """
- else:
- # Generic external trade notification
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
- message = f"""
- 🔄 <b>External Trade Detected</b>
- 📊 <b>Trade Details:</b>
- • Token: {token}
- • Side: {side.upper()}
- • Amount: {amount} {token}
- • Price: ${price:,.2f}
- • Value: ${amount * price:,.2f}
- {side_emoji} <b>Source:</b> External Platform Trade
- ⏰ <b>Time:</b> {time_str}
- 📈 <b>Note:</b> 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} <b>Stop Loss P&L:</b>
- • 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"""
- 🛑 <b>STOP LOSS EXECUTED</b>
- {position_emoji} <b>{position_side.upper()} Position Protected:</b>
- • 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}
- 🎯 <b>Stop Loss Details:</b>
- • Status: EXECUTED ✅
- • Order Side: {side.upper()}
- • Action Type: {action_type.replace('_', ' ').title()}
- {effectiveness}
- {pnl_info}
- ⏰ <b>Execution Time:</b> {time_str}
- 🤖 <b>Source:</b> External Hyperliquid Order
- 📊 <b>Risk Management:</b> 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}")
|