notification_manager.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. #!/usr/bin/env python3
  2. """
  3. Notification Manager - Handles all bot notifications and messages.
  4. """
  5. import logging
  6. from typing import Optional, Dict, Any, List
  7. from datetime import datetime
  8. logger = logging.getLogger(__name__)
  9. class NotificationManager:
  10. """Handles all notification logic for the trading bot."""
  11. def __init__(self):
  12. """Initialize the notification manager."""
  13. self.bot_application = None
  14. def set_bot_application(self, application):
  15. """Set the bot application for sending messages."""
  16. self.bot_application = application
  17. async def send_long_success_notification(self, query, token: str, token_amount: float,
  18. actual_price: float, order: Dict[str, Any],
  19. stop_loss_price: Optional[float] = None):
  20. """Send notification for successful long order."""
  21. order_id = order.get('id', 'N/A')
  22. order_type = "Market" if not order.get('price') else "Limit"
  23. success_message = f"""
  24. ✅ <b>Long Position Opened Successfully!</b>
  25. 📊 <b>Order Details:</b>
  26. • Token: {token}
  27. • Direction: LONG (Buy)
  28. • Amount: {token_amount:.6f} {token}
  29. • Entry Price: ${actual_price:,.2f}
  30. • Order Type: {order_type}
  31. • Order ID: <code>{order_id}</code>
  32. 💰 <b>Trade Summary:</b>
  33. • Position Value: ${token_amount * actual_price:,.2f}
  34. • Status: FILLED ✅
  35. • Time: {datetime.now().strftime('%H:%M:%S')}
  36. {f"🛑 <b>Pending Stop Loss:</b> ${stop_loss_price:,.2f}\n• Status: Will activate when order fills\n• Protection: Automatic position closure" if stop_loss_price else "💡 Consider setting a stop loss with /sl {token} [price]"}
  37. 📊 Use /positions to view your open positions.
  38. """
  39. await query.edit_message_text(success_message, parse_mode='HTML')
  40. logger.info(f"Long order executed: {token_amount:.6f} {token} @ ${actual_price:,.2f}")
  41. async def send_short_success_notification(self, query, token: str, token_amount: float,
  42. actual_price: float, order: Dict[str, Any],
  43. stop_loss_price: Optional[float] = None):
  44. """Send notification for successful short order."""
  45. order_id = order.get('id', 'N/A')
  46. order_type = "Market" if not order.get('price') else "Limit"
  47. success_message = f"""
  48. ✅ <b>Short Position Opened Successfully!</b>
  49. 📊 <b>Order Details:</b>
  50. • Token: {token}
  51. • Direction: SHORT (Sell)
  52. • Amount: {token_amount:.6f} {token}
  53. • Entry Price: ${actual_price:,.2f}
  54. • Order Type: {order_type}
  55. • Order ID: <code>{order_id}</code>
  56. 💰 <b>Trade Summary:</b>
  57. • Position Value: ${token_amount * actual_price:,.2f}
  58. • Status: FILLED ✅
  59. • Time: {datetime.now().strftime('%H:%M:%S')}
  60. {f"🛑 <b>Pending Stop Loss:</b> ${stop_loss_price:,.2f}\n• Status: Will activate when order fills\n• Protection: Automatic position closure" if stop_loss_price else "💡 Consider setting a stop loss with /sl {token} [price]"}
  61. 📊 Use /positions to view your open positions.
  62. """
  63. await query.edit_message_text(success_message, parse_mode='HTML')
  64. logger.info(f"Short order executed: {token_amount:.6f} {token} @ ${actual_price:,.2f}")
  65. async def send_exit_success_notification(self, query, token: str, position_type: str,
  66. contracts: float, actual_price: float,
  67. pnl: float, order: Dict[str, Any]):
  68. """Send notification for successful exit order."""
  69. order_id = order.get('id', 'N/A')
  70. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  71. action = "SELL" if position_type == "LONG" else "BUY"
  72. # Check if stop losses were cancelled
  73. cancelled_sls = order.get('cancelled_stop_losses', 0)
  74. success_message = f"""
  75. ✅ <b>{position_type} Position Closed Successfully!</b>
  76. 📊 <b>Exit Details:</b>
  77. • Token: {token}
  78. • Position: {position_type}
  79. • Action: {action} (Close)
  80. • Amount: {contracts:.6f} {token}
  81. • Exit Price: ${actual_price:,.2f}
  82. • Order ID: <code>{order_id}</code>
  83. 💰 <b>Trade Summary:</b>
  84. • Exit Value: ${contracts * actual_price:,.2f}
  85. • {pnl_emoji} Realized P&L: ${pnl:,.2f}
  86. • Status: FILLED ✅
  87. • Time: {datetime.now().strftime('%H:%M:%S')}"""
  88. if cancelled_sls > 0:
  89. success_message += f"""
  90. 🛑 <b>Cleanup:</b>
  91. • Cancelled {cancelled_sls} pending stop loss order(s)
  92. • All protective orders removed"""
  93. success_message += f"""
  94. 📊 <b>Result:</b> Position fully closed
  95. 💡 Use /stats to view updated performance metrics.
  96. """
  97. await query.edit_message_text(success_message, parse_mode='HTML')
  98. logger.info(f"Exit order executed: {contracts:.6f} {token} @ ${actual_price:,.2f} (P&L: ${pnl:,.2f}){f' | Cancelled {cancelled_sls} SLs' if cancelled_sls > 0 else ''}")
  99. async def send_sl_success_notification(self, query, token: str, position_type: str,
  100. contracts: float, stop_price: float,
  101. order: Dict[str, Any]):
  102. """Send notification for successful stop loss order."""
  103. order_id = order.get('id', 'N/A')
  104. action = "SELL" if position_type == "LONG" else "BUY"
  105. success_message = f"""
  106. ✅ <b>Stop Loss Order Set Successfully!</b>
  107. 📊 <b>Stop Loss Details:</b>
  108. • Token: {token}
  109. • Position: {position_type}
  110. • Size: {contracts:.6f} contracts
  111. • Stop Price: ${stop_price:,.2f}
  112. • Action: {action} (Close {position_type})
  113. • Order Type: Limit Order
  114. • Order ID: <code>{order_id}</code>
  115. 🛑 <b>Risk Management:</b>
  116. • Status: ACTIVE ✅
  117. • Trigger: When price reaches ${stop_price:,.2f}
  118. • Protection: Automatic position closure
  119. • Time: {datetime.now().strftime('%H:%M:%S')}
  120. 💡 <b>Note:</b> The stop loss order will execute automatically when the market price reaches your stop price.
  121. 📊 Use /orders to view all active orders.
  122. """
  123. await query.edit_message_text(success_message, parse_mode='HTML')
  124. logger.info(f"Stop loss set: {token} @ ${stop_price:,.2f}")
  125. async def send_tp_success_notification(self, query, token: str, position_type: str,
  126. contracts: float, tp_price: float,
  127. order: Dict[str, Any]):
  128. """Send notification for successful take profit order."""
  129. order_id = order.get('id', 'N/A')
  130. action = "SELL" if position_type == "LONG" else "BUY"
  131. success_message = f"""
  132. ✅ <b>Take Profit Order Set Successfully!</b>
  133. 📊 <b>Take Profit Details:</b>
  134. • Token: {token}
  135. • Position: {position_type}
  136. • Size: {contracts:.6f} contracts
  137. • Take Profit Price: ${tp_price:,.2f}
  138. • Action: {action} (Close {position_type})
  139. • Order Type: Limit Order
  140. • Order ID: <code>{order_id}</code>
  141. 🎯 <b>Profit Management:</b>
  142. • Status: ACTIVE ✅
  143. • Trigger: When price reaches ${tp_price:,.2f}
  144. • Action: Automatic profit taking
  145. • Time: {datetime.now().strftime('%H:%M:%S')}
  146. 💡 <b>Note:</b> The take profit order will execute automatically when the market price reaches your target price.
  147. 📊 Use /orders to view all active orders.
  148. """
  149. await query.edit_message_text(success_message, parse_mode='HTML')
  150. logger.info(f"Take profit set: {token} @ ${tp_price:,.2f}")
  151. async def send_coo_success_notification(self, query, token: str, cancelled_count: int,
  152. failed_count: int, cancelled_linked_sls: int = 0,
  153. cancelled_orders: List[Dict[str, Any]] = None):
  154. """Send notification for successful cancel all orders operation."""
  155. success_message = f"""
  156. ✅ <b>Cancel Orders Results</b>
  157. 📊 <b>Summary:</b>
  158. • Token: {token}
  159. • Cancelled: {cancelled_count} orders
  160. • Failed: {failed_count} orders
  161. • Total Attempted: {cancelled_count + failed_count} orders"""
  162. if cancelled_linked_sls > 0:
  163. success_message += f"""
  164. • 🛑 Linked Stop Losses Cancelled: {cancelled_linked_sls}"""
  165. # Show details of cancelled orders if available
  166. if cancelled_orders and len(cancelled_orders) > 0:
  167. success_message += f"""
  168. 🗑️ <b>Successfully Cancelled:</b>"""
  169. for order in cancelled_orders:
  170. side = order.get('side', 'Unknown')
  171. amount = order.get('amount', 0)
  172. price = order.get('price', 0)
  173. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  174. success_message += f"""
  175. {side_emoji} {side.upper()} {amount} @ ${price:,.2f}"""
  176. # Overall status
  177. if cancelled_count == (cancelled_count + failed_count) and failed_count == 0:
  178. success_message += f"""
  179. 🎉 All {token} orders successfully cancelled!"""
  180. elif cancelled_count > 0:
  181. success_message += f"""
  182. ⚠️ Some orders cancelled. {failed_count} failed."""
  183. else:
  184. success_message += f"""
  185. ❌ Could not cancel any {token} orders."""
  186. success_message += f"""
  187. ⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
  188. 📊 Use /orders to verify no pending orders remain.
  189. """
  190. await query.edit_message_text(success_message, parse_mode='HTML')
  191. 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 ''}")
  192. async def send_alarm_triggered_notification(self, token: str, target_price: float,
  193. current_price: float, direction: str):
  194. """Send notification when a price alarm is triggered."""
  195. if not self.bot_application:
  196. logger.warning("Bot application not set, cannot send alarm notification")
  197. return
  198. direction_emoji = "📈" if direction == 'above' else "📉"
  199. alarm_message = f"""
  200. 🔔 <b>Price Alarm Triggered!</b>
  201. {direction_emoji} <b>Alert Details:</b>
  202. • Token: {token}
  203. • Target Price: ${target_price:,.2f}
  204. • Current Price: ${current_price:,.2f}
  205. • Direction: {direction.upper()}
  206. ⏰ <b>Trigger Time:</b> {datetime.now().strftime('%H:%M:%S')}
  207. 💡 <b>Quick Actions:</b>
  208. • /market {token} - View market data
  209. • /price {token} - Quick price check
  210. • /long {token} [amount] - Open long position
  211. • /short {token} [amount] - Open short position
  212. """
  213. try:
  214. from src.config.config import Config
  215. if Config.TELEGRAM_CHAT_ID:
  216. await self.bot_application.bot.send_message(
  217. chat_id=Config.TELEGRAM_CHAT_ID,
  218. text=alarm_message,
  219. parse_mode='HTML'
  220. )
  221. logger.info(f"Alarm notification sent: {token} {direction} ${target_price}")
  222. except Exception as e:
  223. logger.error(f"Failed to send alarm notification: {e}")
  224. async def send_external_trade_notification(self, symbol: str, side: str, amount: float,
  225. price: float, action_type: str, timestamp: str):
  226. """Send notification for external trades detected."""
  227. if not self.bot_application:
  228. logger.warning("Bot application not set, cannot send external trade notification")
  229. return
  230. # Extract token from symbol
  231. token = symbol.split('/')[0] if '/' in symbol else symbol
  232. # Format timestamp
  233. try:
  234. trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  235. time_str = trade_time.strftime('%H:%M:%S')
  236. except:
  237. time_str = "Unknown"
  238. # Format message based on action type
  239. if action_type == "position_opened":
  240. message = f"""
  241. 🚀 <b>Position Opened (External)</b>
  242. 📊 <b>Trade Details:</b>
  243. • Token: {token}
  244. • Direction: {side.upper()}
  245. • Size: {amount} {token}
  246. • Entry Price: ${price:,.2f}
  247. • Value: ${amount * price:,.2f}
  248. ✅ <b>Status:</b> New position opened externally
  249. ⏰ <b>Time:</b> {time_str}
  250. 📱 Use /positions to view all positions
  251. """
  252. elif action_type == "position_closed":
  253. message = f"""
  254. 🎯 <b>Position Closed (External)</b>
  255. 📊 <b>Trade Details:</b>
  256. • Token: {token}
  257. • Direction: {side.upper()}
  258. • Size: {amount} {token}
  259. • Exit Price: ${price:,.2f}
  260. • Value: ${amount * price:,.2f}
  261. ✅ <b>Status:</b> Position closed externally
  262. ⏰ <b>Time:</b> {time_str}
  263. 📊 Use /stats to view updated performance
  264. """
  265. elif action_type == "position_increased":
  266. message = f"""
  267. 📈 <b>Position Increased (External)</b>
  268. 📊 <b>Trade Details:</b>
  269. • Token: {token}
  270. • Direction: {side.upper()}
  271. • Added Size: {amount} {token}
  272. • Price: ${price:,.2f}
  273. • Value: ${amount * price:,.2f}
  274. ✅ <b>Status:</b> Position size increased externally
  275. ⏰ <b>Time:</b> {time_str}
  276. 📈 Use /positions to view current position
  277. """
  278. else:
  279. # Generic external trade notification
  280. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  281. message = f"""
  282. 🔄 <b>External Trade Detected</b>
  283. 📊 <b>Trade Details:</b>
  284. • Token: {token}
  285. • Side: {side.upper()}
  286. • Amount: {amount} {token}
  287. • Price: ${price:,.2f}
  288. • Value: ${amount * price:,.2f}
  289. {side_emoji} <b>Source:</b> External Platform Trade
  290. ⏰ <b>Time:</b> {time_str}
  291. 📈 <b>Note:</b> This trade was executed outside the Telegram bot
  292. 📊 Stats have been automatically updated
  293. """
  294. try:
  295. from src.config.config import Config
  296. if Config.TELEGRAM_CHAT_ID:
  297. await self.bot_application.bot.send_message(
  298. chat_id=Config.TELEGRAM_CHAT_ID,
  299. text=message,
  300. parse_mode='HTML'
  301. )
  302. logger.info(f"External trade notification sent: {action_type} for {token}")
  303. except Exception as e:
  304. logger.error(f"Failed to send external trade notification: {e}")
  305. async def send_generic_notification(self, message: str):
  306. """Send a generic notification message."""
  307. if not self.bot_application:
  308. logger.warning("Bot application not set, cannot send generic notification")
  309. return
  310. try:
  311. from src.config.config import Config
  312. if Config.TELEGRAM_CHAT_ID:
  313. await self.bot_application.bot.send_message(
  314. chat_id=Config.TELEGRAM_CHAT_ID,
  315. text=message,
  316. parse_mode='HTML'
  317. )
  318. logger.info("Generic notification sent")
  319. except Exception as e:
  320. logger.error(f"Failed to send generic notification: {e}")