notification_manager.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  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. from src.config.config import Config
  9. from src.utils.token_display_formatter import get_formatter # Import the global formatter
  10. logger = logging.getLogger(__name__)
  11. class NotificationManager:
  12. """Handles all notification logic for the trading bot."""
  13. def __init__(self):
  14. """Initialize the notification manager."""
  15. self.bot_application = None
  16. def set_bot_application(self, application):
  17. """Set the bot application for sending messages."""
  18. self.bot_application = application
  19. async def send_long_success_notification(self, query, token, amount, price, order_details, stop_loss_price=None, trade_lifecycle_id=None):
  20. """Send notification for successful long order."""
  21. try:
  22. # Use TokenDisplayFormatter for consistent formatting
  23. formatter = get_formatter() # Get formatter
  24. amount_str = await formatter.format_amount(amount, token)
  25. order_id_str = order_details.get('id', 'N/A')
  26. if price is not None:
  27. price_str = await formatter.format_price_with_symbol(price, token)
  28. value_str = await formatter.format_price_with_symbol(amount * price, token)
  29. message = (
  30. f"✅ Successfully placed <b>LONG limit order</b> for {amount_str} {token} at {price_str}\n\n"
  31. f"💰 Value: {value_str}\n"
  32. f"🆔 Order ID: <code>{order_id_str}</code>\n"
  33. f"⏳ Awaiting fill to open position."
  34. )
  35. else:
  36. # Handle market order where price is not known yet
  37. message = (
  38. f"✅ Successfully submitted <b>LONG market order</b> for {amount_str} {token}.\n\n"
  39. f"⏳ Awaiting fill confirmation for final price and value.\n"
  40. f"🆔 Order ID: <code>{order_id_str}</code>"
  41. )
  42. if trade_lifecycle_id:
  43. message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
  44. if stop_loss_price:
  45. sl_price_str = await formatter.format_price_with_symbol(stop_loss_price, token)
  46. message += f"\n🛑 Stop Loss pending at {sl_price_str}"
  47. await query.edit_message_text(text=message, parse_mode='HTML')
  48. except Exception as e:
  49. logger.error(f"Error sending long success notification: {e}")
  50. async def send_short_success_notification(self, query, token, amount, price, order_details, stop_loss_price=None, trade_lifecycle_id=None):
  51. """Send notification for successful short order."""
  52. try:
  53. formatter = get_formatter() # Get formatter
  54. amount_str = await formatter.format_amount(amount, token)
  55. order_id_str = order_details.get('id', 'N/A')
  56. if price is not None:
  57. price_str = await formatter.format_price_with_symbol(price, token)
  58. value_str = await formatter.format_price_with_symbol(amount * price, token)
  59. message = (
  60. f"✅ Successfully placed <b>SHORT limit order</b> for {amount_str} {token} at {price_str}\n\n"
  61. f"💰 Value: {value_str}\n"
  62. f"🆔 Order ID: <code>{order_id_str}</code>\n"
  63. f"⏳ Awaiting fill to open position."
  64. )
  65. else:
  66. # Handle market order where price is not known yet
  67. message = (
  68. f"✅ Successfully submitted <b>SHORT market order</b> for {amount_str} {token}.\n\n"
  69. f"⏳ Awaiting fill confirmation for final price and value.\n"
  70. f"🆔 Order ID: <code>{order_id_str}</code>"
  71. )
  72. if trade_lifecycle_id:
  73. message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
  74. if stop_loss_price:
  75. sl_price_str = await formatter.format_price_with_symbol(stop_loss_price, token)
  76. message += f"\n🛑 Stop Loss pending at {sl_price_str}"
  77. await query.edit_message_text(text=message, parse_mode='HTML')
  78. except Exception as e:
  79. logger.error(f"Error sending short success notification: {e}")
  80. async def send_exit_success_notification(self, query, token, position_type, amount, price, pnl, order_details, trade_lifecycle_id=None):
  81. """Send notification for successful exit order."""
  82. try:
  83. formatter = get_formatter() # Get formatter
  84. # Price is the execution price, PnL is calculated based on it
  85. # For market orders, price might be approximate or from fill later
  86. price_str = await formatter.format_price_with_symbol(price, token) if price > 0 else "Market Price"
  87. amount_str = await formatter.format_amount(amount, token)
  88. pnl_str = await formatter.format_price_with_symbol(pnl)
  89. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  90. order_id_str = order_details.get('id', 'N/A')
  91. cancelled_sl_count = order_details.get('cancelled_stop_losses', 0)
  92. message = (
  93. f"✅ Successfully closed <b>{position_type}</b> position for {amount_str} {token}\n\n"
  94. f"🆔 Exit Order ID: <code>{order_id_str}</code>\n"
  95. # P&L and price are more reliably determined when MarketMonitor processes the fill.
  96. # This notification confirms the exit order was PLACED.
  97. f"⏳ Awaiting fill confirmation for final price and P&L."
  98. )
  99. if trade_lifecycle_id:
  100. message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code> (Closed)"
  101. if cancelled_sl_count > 0:
  102. message += f"\n⚠️ Cancelled {cancelled_sl_count} linked stop loss order(s)."
  103. await query.edit_message_text(text=message, parse_mode='HTML')
  104. except Exception as e:
  105. logger.error(f"Error sending exit success notification: {e}")
  106. async def send_sl_success_notification(self, query, token, position_type, amount, stop_price, order_details, trade_lifecycle_id=None):
  107. """Send notification for successful stop loss order setup."""
  108. try:
  109. formatter = get_formatter() # Get formatter
  110. stop_price_str = await formatter.format_price_with_symbol(stop_price, token)
  111. amount_str = await formatter.format_amount(amount, token)
  112. order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
  113. message = (
  114. f"🛑 Successfully set <b>STOP LOSS</b> for {position_type} {amount_str} {token}\n\n"
  115. f"🎯 Trigger Price: {stop_price_str}\n"
  116. f"🆔 SL Order ID: <code>{order_id_str}</code>"
  117. )
  118. if trade_lifecycle_id:
  119. message += f"\n🆔 Linked to Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
  120. await query.edit_message_text(text=message, parse_mode='HTML')
  121. except Exception as e:
  122. logger.error(f"Error sending SL success notification: {e}")
  123. async def send_tp_success_notification(self, query, token, position_type, amount, tp_price, order_details, trade_lifecycle_id=None):
  124. """Send notification for successful take profit order setup."""
  125. try:
  126. formatter = get_formatter() # Get formatter
  127. tp_price_str = await formatter.format_price_with_symbol(tp_price, token)
  128. amount_str = await formatter.format_amount(amount, token)
  129. order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
  130. message = (
  131. f"🎯 Successfully set <b>TAKE PROFIT</b> for {position_type} {amount_str} {token}\n\n"
  132. f"💰 Target Price: {tp_price_str}\n"
  133. f"🆔 TP Order ID: <code>{order_id_str}</code>"
  134. )
  135. if trade_lifecycle_id:
  136. message += f"\n🆔 Linked to Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
  137. await query.edit_message_text(text=message, parse_mode='HTML')
  138. except Exception as e:
  139. logger.error(f"Error sending TP success notification: {e}")
  140. async def send_coo_success_notification(self, query, token: str, cancelled_count: int,
  141. failed_count: int, cancelled_linked_sls: int = 0,
  142. cancelled_orders: List[Dict[str, Any]] = None):
  143. """Send notification for successful cancel all orders operation."""
  144. success_message = f"""
  145. ✅ <b>Cancel Orders Results</b>
  146. 📊 <b>Summary:</b>
  147. • Token: {token}
  148. • Cancelled: {cancelled_count} orders
  149. • Failed: {failed_count} orders
  150. • Total Attempted: {cancelled_count + failed_count} orders"""
  151. if cancelled_linked_sls > 0:
  152. success_message += f"""
  153. • 🛑 Linked Stop Losses Cancelled: {cancelled_linked_sls}"""
  154. # Show details of cancelled orders if available
  155. if cancelled_orders and len(cancelled_orders) > 0:
  156. success_message += f"""
  157. 🗑️ <b>Successfully Cancelled:</b>"""
  158. formatter = get_formatter() # Get formatter for loop
  159. for order in cancelled_orders:
  160. side = order.get('side', 'Unknown')
  161. amount = order.get('amount', 0)
  162. price = order.get('price', 0)
  163. # Assuming 'token' is the common symbol for all these orders
  164. # If individual orders can have different tokens, order dict should contain 'symbol' or 'token' key
  165. order_token_symbol = order.get('symbol', token) # Fallback to main token if not in order dict
  166. amount_str = await formatter.format_amount(amount, order_token_symbol)
  167. price_str = await formatter.format_price_with_symbol(price, order_token_symbol) if price > 0 else "N/A"
  168. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  169. success_message += f"""
  170. {side_emoji} {side.upper()} {amount_str} {order_token_symbol} @ {price_str}"""
  171. # Overall status
  172. if cancelled_count == (cancelled_count + failed_count) and failed_count == 0:
  173. success_message += f"""
  174. 🎉 All {token} orders successfully cancelled!"""
  175. elif cancelled_count > 0:
  176. success_message += f"""
  177. ⚠️ Some orders cancelled. {failed_count} failed."""
  178. else:
  179. success_message += f"""
  180. ❌ Could not cancel any {token} orders."""
  181. success_message += f"""
  182. ⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
  183. 📊 Use /orders to verify no pending orders remain.
  184. """
  185. await query.edit_message_text(success_message, parse_mode='HTML')
  186. 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 ''}")
  187. async def send_alarm_triggered_notification(self, token: str, target_price: float,
  188. current_price: float, direction: str):
  189. """Send notification when a price alarm is triggered."""
  190. if not self.bot_application:
  191. logger.warning("Bot application not set, cannot send alarm notification")
  192. return
  193. direction_emoji = "📈" if direction == 'above' else "📉"
  194. formatter = get_formatter() # Get formatter
  195. target_price_str = await formatter.format_price_with_symbol(target_price, token)
  196. current_price_str = await formatter.format_price_with_symbol(current_price, token)
  197. alarm_message = f"""
  198. 🔔 <b>Price Alarm Triggered!</b>
  199. {direction_emoji} <b>Alert Details:</b>
  200. • Token: {token}
  201. • Target Price: {target_price_str}
  202. • Current Price: {current_price_str}
  203. • Direction: {direction.upper()}
  204. ⏰ <b>Trigger Time:</b> {datetime.now().strftime('%H:%M:%S')}
  205. 💡 <b>Quick Actions:</b>
  206. • /market {token} - View market data
  207. • /price {token} - Quick price check
  208. • /long {token} [amount] - Open long position
  209. • /short {token} [amount] - Open short position
  210. """
  211. try:
  212. if Config.TELEGRAM_CHAT_ID:
  213. await self.bot_application.bot.send_message(
  214. chat_id=Config.TELEGRAM_CHAT_ID,
  215. text=alarm_message,
  216. parse_mode='HTML'
  217. )
  218. logger.info(f"Alarm notification sent: {token} {direction} ${target_price}")
  219. except Exception as e:
  220. logger.error(f"Failed to send alarm notification: {e}")
  221. async def send_external_trade_notification(self, symbol: str, side: str, amount: float,
  222. price: float, action_type: str, timestamp: str):
  223. """Send notification for external trades detected."""
  224. if not self.bot_application:
  225. logger.warning("Bot application not set, cannot send external trade notification")
  226. return
  227. # Extract token from symbol
  228. token = symbol.split('/')[0] if '/' in symbol else symbol
  229. # Format timestamp
  230. try:
  231. trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  232. time_str = trade_time.strftime('%H:%M:%S')
  233. except:
  234. time_str = "Unknown"
  235. # Format message based on action type
  236. if action_type == "position_opened":
  237. message = f"""
  238. 🚀 <b>Position Opened (External)</b>
  239. 📊 <b>Trade Details:</b>
  240. • Token: {token}
  241. • Direction: {side.upper()}
  242. • Size: {amount} {token}
  243. • Entry Price: ${price:,.2f}
  244. • Value: ${amount * price:,.2f}
  245. ✅ <b>Status:</b> New position opened externally
  246. ⏰ <b>Time:</b> {time_str}
  247. 📱 Use /positions to view all positions
  248. """
  249. elif action_type == "position_closed":
  250. message = f"""
  251. 🎯 <b>Position Closed (External)</b>
  252. 📊 <b>Trade Details:</b>
  253. • Token: {token}
  254. • Direction: {side.upper()}
  255. • Size: {amount} {token}
  256. • Exit Price: ${price:,.2f}
  257. • Value: ${amount * price:,.2f}
  258. ✅ <b>Status:</b> Position closed externally
  259. ⏰ <b>Time:</b> {time_str}
  260. 📊 Use /stats to view updated performance
  261. """
  262. elif action_type == "position_increased":
  263. message = f"""
  264. 📈 <b>Position Increased (External)</b>
  265. 📊 <b>Trade Details:</b>
  266. • Token: {token}
  267. • Direction: {side.upper()}
  268. • Added Size: {amount} {token}
  269. • Price: ${price:,.2f}
  270. • Value: ${amount * price:,.2f}
  271. ✅ <b>Status:</b> Position size increased externally
  272. ⏰ <b>Time:</b> {time_str}
  273. 📈 Use /positions to view current position
  274. """
  275. elif action_type == "position_decreased":
  276. message = f"""
  277. 📉 <b>Position Decreased (External)</b>
  278. 📊 <b>Trade Details:</b>
  279. • Token: {token}
  280. • Direction: {side.upper()}
  281. • Reduced Size: {amount} {token}
  282. • Price: ${price:,.2f}
  283. • Value: ${amount * price:,.2f}
  284. ✅ <b>Status:</b> Position size decreased externally
  285. ⏰ <b>Time:</b> {time_str}
  286. 📊 Position remains open. Use /positions to view details
  287. """
  288. else:
  289. # Generic external trade notification
  290. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  291. message = f"""
  292. 🔄 <b>External Trade Detected</b>
  293. 📊 <b>Trade Details:</b>
  294. • Token: {token}
  295. • Side: {side.upper()}
  296. • Amount: {amount} {token}
  297. • Price: ${price:,.2f}
  298. • Value: ${amount * price:,.2f}
  299. {side_emoji} <b>Source:</b> External Platform Trade
  300. ⏰ <b>Time:</b> {time_str}
  301. 📈 <b>Note:</b> This trade was executed outside the Telegram bot
  302. 📊 Stats have been automatically updated
  303. """
  304. try:
  305. if Config.TELEGRAM_CHAT_ID:
  306. await self.bot_application.bot.send_message(
  307. chat_id=Config.TELEGRAM_CHAT_ID,
  308. text=message,
  309. parse_mode='HTML'
  310. )
  311. logger.info(f"External trade notification sent: {action_type} for {token}")
  312. except Exception as e:
  313. logger.error(f"Failed to send external trade notification: {e}")
  314. async def send_generic_notification(self, message: str):
  315. """Send a generic notification message."""
  316. if not self.bot_application:
  317. logger.warning("Bot application not set, cannot send generic notification")
  318. return
  319. try:
  320. if Config.TELEGRAM_CHAT_ID:
  321. await self.bot_application.bot.send_message(
  322. chat_id=Config.TELEGRAM_CHAT_ID,
  323. text=message,
  324. parse_mode='HTML'
  325. )
  326. logger.info("Generic notification sent")
  327. except Exception as e:
  328. logger.error(f"Failed to send generic notification: {e}")
  329. 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):
  330. """Send notification for external stop loss execution."""
  331. try:
  332. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
  333. # Extract stop loss details
  334. trigger_price = stop_loss_info.get('trigger_price', price)
  335. position_side = stop_loss_info.get('position_side', 'unknown')
  336. entry_price = stop_loss_info.get('entry_price', 0)
  337. detected_at = stop_loss_info.get('detected_at')
  338. # Format timestamp
  339. try:
  340. time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  341. time_str = time_obj.strftime('%H:%M:%S')
  342. except:
  343. time_str = "Unknown"
  344. # Calculate P&L if we have entry price
  345. pnl_info = ""
  346. if entry_price > 0:
  347. if position_side == 'long':
  348. pnl = amount * (price - entry_price)
  349. # Get ROE directly from exchange data
  350. info_data = stop_loss_info.get('info', {})
  351. position_info = info_data.get('position', {})
  352. roe_raw = position_info.get('returnOnEquity')
  353. roe = float(roe_raw) * 100 if roe_raw is not None else 0.0
  354. else: # short
  355. pnl = amount * (entry_price - price)
  356. # Get ROE directly from exchange data
  357. info_data = stop_loss_info.get('info', {})
  358. position_info = info_data.get('position', {})
  359. roe_raw = position_info.get('returnOnEquity')
  360. roe = float(roe_raw) * 100 if roe_raw is not None else 0.0
  361. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  362. pnl_info = f"""
  363. {pnl_emoji} <b>Stop Loss P&L:</b>
  364. • Entry Price: ${entry_price:,.2f}
  365. • Exit Price: ${price:,.2f}
  366. • Realized P&L: ${pnl:,.2f} ({roe:+.2f}% ROE)
  367. • Result: {"PROFIT" if pnl >= 0 else "LOSS PREVENTION"}"""
  368. # Determine stop loss effectiveness
  369. effectiveness = ""
  370. if entry_price > 0 and trigger_price > 0:
  371. if position_side == 'long':
  372. # For longs, stop loss should trigger below entry
  373. if trigger_price < entry_price:
  374. loss_percent = ((trigger_price - entry_price) / entry_price) * 100
  375. effectiveness = f"• Loss Limited: {loss_percent:.1f}% ✅"
  376. else:
  377. effectiveness = "• Unusual: Stop above entry 🟡"
  378. else: # short
  379. # For shorts, stop loss should trigger above entry
  380. if trigger_price > entry_price:
  381. loss_percent = ((entry_price - trigger_price) / entry_price) * 100
  382. effectiveness = f"• Loss Limited: {loss_percent:.1f}% ✅"
  383. else:
  384. effectiveness = "• Unusual: Stop below entry 🟡"
  385. trade_value = amount * price
  386. position_emoji = "📈" if position_side == 'long' else "📉"
  387. message = f"""
  388. 🛑 <b>STOP LOSS EXECUTED</b>
  389. {position_emoji} <b>{position_side.upper()} Position Protected:</b>
  390. • Token: {token}
  391. • Position Type: {position_side.upper()}
  392. • Stop Loss Size: {amount} {token}
  393. • Trigger Price: ${trigger_price:,.2f}
  394. • Execution Price: ${price:,.2f}
  395. • Exit Value: ${trade_value:,.2f}
  396. 🎯 <b>Stop Loss Details:</b>
  397. • Status: EXECUTED ✅
  398. • Order Side: {side.upper()}
  399. • Action Type: {action_type.replace('_', ' ').title()}
  400. {effectiveness}
  401. {pnl_info}
  402. ⏰ <b>Execution Time:</b> {time_str}
  403. 🤖 <b>Source:</b> External Hyperliquid Order
  404. 📊 <b>Risk Management:</b> Loss prevention successful
  405. 💡 Your external stop loss order worked as intended!
  406. """
  407. await self.send_notification(message.strip())
  408. logger.info(f"🛑 Stop loss execution notification sent: {token} {position_side} @ ${price:.2f}")
  409. except Exception as e:
  410. logger.error(f"❌ Error sending stop loss execution notification: {e}")
  411. async def send_take_profit_execution_notification(self, tp_info: Dict, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str):
  412. """Send notification for external take profit execution."""
  413. try:
  414. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
  415. formatter = get_formatter()
  416. trigger_price = tp_info.get('trigger_price', price) # Actual TP price
  417. position_side = tp_info.get('position_side', 'unknown')
  418. entry_price = tp_info.get('entry_price', 0)
  419. try:
  420. time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  421. time_str = time_obj.strftime('%H:%M:%S')
  422. except:
  423. time_str = "Unknown"
  424. pnl_info = ""
  425. if entry_price > 0:
  426. if position_side == 'long':
  427. pnl = amount * (price - entry_price)
  428. pnl_percent = ((price - entry_price) / entry_price) * 100 if entry_price != 0 else 0
  429. else: # short
  430. pnl = amount * (entry_price - price)
  431. pnl_percent = ((entry_price - price) / entry_price) * 100 if entry_price != 0 else 0
  432. # Calculate ROE (Return on Equity) more clearly
  433. cost_basis = amount * entry_price
  434. roe = (pnl / cost_basis) * 100
  435. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  436. pnl_info = f"""
  437. {pnl_emoji} <b>Take Profit P&L:</b>
  438. • Entry Price: {await formatter.format_price_with_symbol(entry_price, token)}
  439. • Exit Price: {await formatter.format_price_with_symbol(price, token)}
  440. • Realized P&L: {await formatter.format_price_with_symbol(pnl)} ({roe:+.2f}% ROE)
  441. • Result: {"PROFIT SECURED" if pnl >=0 else "MINIMIZED LOSS"}"""
  442. trade_value = amount * price
  443. position_emoji = "📈" if position_side == 'long' else "📉"
  444. message = f"""
  445. 🎯 <b>TAKE PROFIT EXECUTED</b>
  446. {position_emoji} <b>{position_side.upper()} Position Profit Secured:</b>
  447. • Token: {token}
  448. • Position Type: {position_side.upper()}
  449. • Take Profit Size: {amount} {token}
  450. • Target Price: {await formatter.format_price_with_symbol(trigger_price, token)}
  451. • Execution Price: {await formatter.format_price_with_symbol(price, token)}
  452. • Exit Value: {await formatter.format_price_with_symbol(trade_value, token)}
  453. ✅ <b>Take Profit Details:</b>
  454. • Status: EXECUTED
  455. • Order Side: {side.upper()}
  456. • Action Type: {action_type.replace('_', ' ').title()}
  457. {pnl_info}
  458. ⏰ <b>Execution Time:</b> {time_str}
  459. 🤖 <b>Source:</b> External Hyperliquid Order
  460. 📊 <b>Risk Management:</b> Profit successfully secured
  461. 💡 Your take profit order worked as intended!
  462. """
  463. if self.bot_application:
  464. if Config.TELEGRAM_CHAT_ID:
  465. await self.bot_application.bot.send_message(
  466. chat_id=Config.TELEGRAM_CHAT_ID,
  467. text=message,
  468. parse_mode='HTML'
  469. )
  470. logger.info(f"Take Profit execution notification sent for {token}")
  471. except Exception as e:
  472. logger.error(f"Error sending Take Profit execution notification: {e}", exc_info=True)