#!/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.config.config import Config
from src.utils.token_display_formatter import get_formatter # Import the global 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 TokenDisplayFormatter for consistent formatting
formatter = get_formatter() # Get formatter
amount_str = await formatter.format_amount(amount, token)
order_id_str = order_details.get('id', 'N/A')
if price is not None:
price_str = await formatter.format_price_with_symbol(price, token)
value_str = await formatter.format_price_with_symbol(amount * price, token)
message = (
f"ā
Successfully placed LONG limit order for {amount_str} {token} at {price_str}\n\n"
f"š° Value: {value_str}\n"
f"š Order ID: {order_id_str}
\n"
f"ā³ Awaiting fill to open position."
)
else:
# Handle market order where price is not known yet
message = (
f"ā
Successfully submitted LONG market order for {amount_str} {token}.\n\n"
f"ā³ Awaiting fill confirmation for final price and value.\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 = await 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
amount_str = await formatter.format_amount(amount, token)
order_id_str = order_details.get('id', 'N/A')
if price is not None:
price_str = await formatter.format_price_with_symbol(price, token)
value_str = await formatter.format_price_with_symbol(amount * price, token)
message = (
f"ā
Successfully placed SHORT limit order for {amount_str} {token} at {price_str}\n\n"
f"š° Value: {value_str}\n"
f"š Order ID: {order_id_str}
\n"
f"ā³ Awaiting fill to open position."
)
else:
# Handle market order where price is not known yet
message = (
f"ā
Successfully submitted SHORT market order for {amount_str} {token}.\n\n"
f"ā³ Awaiting fill confirmation for final price and value.\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 = await 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 = await formatter.format_price_with_symbol(price, token) if price > 0 else "Market Price"
amount_str = await formatter.format_amount(amount, token)
pnl_str = await 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} {token}\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 = await formatter.format_price_with_symbol(stop_price, token)
amount_str = await formatter.format_amount(amount, 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} {token}\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 = await formatter.format_price_with_symbol(tp_price, token)
amount_str = await formatter.format_amount(amount, 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} {token}\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:"""
formatter = get_formatter() # Get formatter for loop
for order in cancelled_orders:
side = order.get('side', 'Unknown')
amount = order.get('amount', 0)
price = order.get('price', 0)
# Assuming 'token' is the common symbol for all these orders
# If individual orders can have different tokens, order dict should contain 'symbol' or 'token' key
order_token_symbol = order.get('symbol', token) # Fallback to main token if not in order dict
amount_str = await formatter.format_amount(amount, order_token_symbol)
price_str = await formatter.format_price_with_symbol(price, order_token_symbol) if price > 0 else "N/A"
side_emoji = "š¢" if side.lower() == 'buy' else "š“"
success_message += f"""
{side_emoji} {side.upper()} {amount_str} {order_token_symbol} @ {price_str}"""
# 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 "š"
formatter = get_formatter() # Get formatter
target_price_str = await formatter.format_price_with_symbol(target_price, token)
current_price_str = await formatter.format_price_with_symbol(current_price, token)
alarm_message = f"""
š Price Alarm Triggered!
{direction_emoji} Alert Details:
⢠Token: {token}
⢠Target Price: {target_price_str}
⢠Current Price: {current_price_str}
⢠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:
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
"""
elif action_type == "position_decreased":
message = f"""
š Position Decreased (External)
š Trade Details:
⢠Token: {token}
⢠Direction: {side.upper()}
⢠Reduced Size: {amount} {token}
⢠Price: ${price:,.2f}
⢠Value: ${amount * price:,.2f}
ā
Status: Position size decreased externally
ā° Time: {time_str}
š Position remains open. Use /positions to view details
"""
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:
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:
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)
# Get ROE directly from exchange data
info_data = stop_loss_info.get('info', {})
position_info = info_data.get('position', {})
roe_raw = position_info.get('returnOnEquity')
roe = float(roe_raw) * 100 if roe_raw is not None else 0.0
else: # short
pnl = amount * (entry_price - price)
# Get ROE directly from exchange data
info_data = stop_loss_info.get('info', {})
position_info = info_data.get('position', {})
roe_raw = position_info.get('returnOnEquity')
roe = float(roe_raw) * 100 if roe_raw is not None else 0.0
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} ({roe:+.2f}% ROE)
⢠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}")
async def send_take_profit_execution_notification(self, tp_info: Dict, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str):
"""Send notification for external take profit execution."""
try:
token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
formatter = get_formatter()
trigger_price = tp_info.get('trigger_price', price) # Actual TP price
position_side = tp_info.get('position_side', 'unknown')
entry_price = tp_info.get('entry_price', 0)
try:
time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
time_str = time_obj.strftime('%H:%M:%S')
except:
time_str = "Unknown"
pnl_info = ""
if entry_price > 0:
if position_side == 'long':
pnl = amount * (price - entry_price)
pnl_percent = ((price - entry_price) / entry_price) * 100 if entry_price != 0 else 0
else: # short
pnl = amount * (entry_price - price)
pnl_percent = ((entry_price - price) / entry_price) * 100 if entry_price != 0 else 0
# Calculate ROE (Return on Equity) more clearly
cost_basis = amount * entry_price
roe = (pnl / cost_basis) * 100
pnl_emoji = "š¢" if pnl >= 0 else "š“"
pnl_info = f"""
{pnl_emoji} Take Profit P&L:
⢠Entry Price: {await formatter.format_price_with_symbol(entry_price, token)}
⢠Exit Price: {await formatter.format_price_with_symbol(price, token)}
⢠Realized P&L: {await formatter.format_price_with_symbol(pnl)} ({roe:+.2f}% ROE)
⢠Result: {"PROFIT SECURED" if pnl >=0 else "MINIMIZED LOSS"}"""
trade_value = amount * price
position_emoji = "š" if position_side == 'long' else "š"
message = f"""
šÆ TAKE PROFIT EXECUTED
{position_emoji} {position_side.upper()} Position Profit Secured:
⢠Token: {token}
⢠Position Type: {position_side.upper()}
⢠Take Profit Size: {amount} {token}
⢠Target Price: {await formatter.format_price_with_symbol(trigger_price, token)}
⢠Execution Price: {await formatter.format_price_with_symbol(price, token)}
⢠Exit Value: {await formatter.format_price_with_symbol(trade_value, token)}
ā
Take Profit Details:
⢠Status: EXECUTED
⢠Order Side: {side.upper()}
⢠Action Type: {action_type.replace('_', ' ').title()}
{pnl_info}
ā° Execution Time: {time_str}
š¤ Source: External Hyperliquid Order
š Risk Management: Profit successfully secured
š” Your take profit order worked as intended!
"""
if self.bot_application:
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"Take Profit execution notification sent for {token}")
except Exception as e:
logger.error(f"Error sending Take Profit execution notification: {e}", exc_info=True)