trading_commands.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. #!/usr/bin/env python3
  2. """
  3. Trading Commands - Handles all trading-related Telegram commands.
  4. """
  5. import logging
  6. from typing import Optional
  7. from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
  8. from telegram.ext import ContextTypes
  9. from src.config.config import Config
  10. logger = logging.getLogger(__name__)
  11. class TradingCommands:
  12. """Handles all trading-related Telegram commands."""
  13. def __init__(self, trading_engine, notification_manager, info_commands_handler=None, management_commands_handler=None):
  14. """Initialize with trading engine, notification manager, and other command handlers."""
  15. self.trading_engine = trading_engine
  16. self.notification_manager = notification_manager
  17. self.info_commands_handler = info_commands_handler
  18. self.management_commands_handler = management_commands_handler
  19. def _is_authorized(self, chat_id: str) -> bool:
  20. """Check if the chat ID is authorized."""
  21. return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
  22. async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  23. """Handle the /long command for opening long positions."""
  24. chat_id = update.effective_chat.id
  25. if not self._is_authorized(chat_id):
  26. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  27. return
  28. try:
  29. if not context.args or len(context.args) < 2:
  30. await context.bot.send_message(chat_id=chat_id, text=(
  31. "❌ Usage: /long [token] [USDC amount] [price (optional)] [sl:price (optional)]\n"
  32. "Examples:\n"
  33. "• /long BTC 100 - Market order\n"
  34. "• /long BTC 100 45000 - Limit order at $45,000\n"
  35. "• /long BTC 100 sl:44000 - Market order with stop loss at $44,000\n"
  36. "• /long BTC 100 45000 sl:44000 - Limit order at $45,000 with stop loss at $44,000"
  37. ))
  38. return
  39. token = context.args[0].upper()
  40. usdc_amount = float(context.args[1])
  41. # Parse arguments for price and stop loss
  42. limit_price = None
  43. stop_loss_price = None
  44. # Parse remaining arguments
  45. for i, arg in enumerate(context.args[2:], 2):
  46. if arg.startswith('sl:'):
  47. # Stop loss parameter
  48. try:
  49. stop_loss_price = float(arg[3:]) # Remove 'sl:' prefix
  50. except ValueError:
  51. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid stop loss price format. Use sl:price (e.g., sl:44000)")
  52. return
  53. elif limit_price is None:
  54. # First non-sl parameter is the limit price
  55. try:
  56. limit_price = float(arg)
  57. except ValueError:
  58. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid limit price format. Please use numbers only.")
  59. return
  60. # Get current market price
  61. market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC")
  62. if not market_data:
  63. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch market data for {token}")
  64. return
  65. current_price = float(market_data['ticker'].get('last', 0))
  66. if current_price <= 0:
  67. await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid current price for {token}")
  68. return
  69. # Determine order type and price
  70. if limit_price:
  71. order_type = "Limit"
  72. price = limit_price
  73. token_amount = usdc_amount / price
  74. else:
  75. order_type = "Market"
  76. price = current_price
  77. token_amount = usdc_amount / current_price
  78. # Validate stop loss for long positions
  79. if stop_loss_price and stop_loss_price >= price:
  80. await context.bot.send_message(chat_id=chat_id, text=(
  81. f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n"
  82. f"📊 Your order:\n"
  83. f"• Entry Price: ${price:,.2f}\n"
  84. f"• Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
  85. f"💡 Try a lower stop loss like: sl:{price * 0.95:.0f}"
  86. ))
  87. return
  88. # Create confirmation message
  89. confirmation_text = f"""
  90. 🟢 <b>Long Order Confirmation</b>
  91. 📊 <b>Order Details:</b>
  92. • Token: {token}
  93. • USDC Amount: ${usdc_amount:,.2f}
  94. • Token Amount: {token_amount:.6f} {token}
  95. • Order Type: {order_type}
  96. • Price: ${price:,.2f}
  97. • Current Price: ${current_price:,.2f}
  98. • Est. Value: ${token_amount * price:,.2f}"""
  99. if stop_loss_price:
  100. confirmation_text += f"""
  101. • 🛑 Stop Loss: ${stop_loss_price:,.2f}"""
  102. confirmation_text += f"""
  103. ⚠️ <b>Are you sure you want to open this LONG position?</b>
  104. This will {"place a limit buy order" if limit_price else "execute a market buy order"} for {token}."""
  105. if stop_loss_price:
  106. confirmation_text += f"\nStop loss will be set automatically when order fills."
  107. # Create callback data for confirmation
  108. callback_data = f"confirm_long_{token}_{usdc_amount}_{price if limit_price else 'market'}"
  109. if stop_loss_price:
  110. callback_data += f"_sl_{stop_loss_price}"
  111. keyboard = [
  112. [
  113. InlineKeyboardButton("✅ Execute Long", callback_data=callback_data),
  114. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  115. ]
  116. ]
  117. reply_markup = InlineKeyboardMarkup(keyboard)
  118. await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  119. except ValueError as e:
  120. await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid input format: {e}")
  121. except Exception as e:
  122. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing long command: {e}")
  123. logger.error(f"Error in long command: {e}")
  124. async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  125. """Handle the /short command for opening short positions."""
  126. chat_id = update.effective_chat.id
  127. if not self._is_authorized(chat_id):
  128. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  129. return
  130. try:
  131. if not context.args or len(context.args) < 2:
  132. await context.bot.send_message(chat_id=chat_id, text=(
  133. "❌ Usage: /short [token] [USDC amount] [price (optional)] [sl:price (optional)]\n"
  134. "Examples:\n"
  135. "• /short BTC 100 - Market order\n"
  136. "• /short BTC 100 45000 - Limit order at $45,000\n"
  137. "• /short BTC 100 sl:46000 - Market order with stop loss at $46,000\n"
  138. "• /short BTC 100 45000 sl:46000 - Limit order at $45,000 with stop loss at $46,000"
  139. ))
  140. return
  141. token = context.args[0].upper()
  142. usdc_amount = float(context.args[1])
  143. # Parse arguments (similar to long_command)
  144. limit_price = None
  145. stop_loss_price = None
  146. for i, arg in enumerate(context.args[2:], 2):
  147. if arg.startswith('sl:'):
  148. try:
  149. stop_loss_price = float(arg[3:])
  150. except ValueError:
  151. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid stop loss price format. Use sl:price (e.g., sl:46000)")
  152. return
  153. elif limit_price is None:
  154. try:
  155. limit_price = float(arg)
  156. except ValueError:
  157. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid limit price format. Please use numbers only.")
  158. return
  159. # Get current market price
  160. market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC")
  161. if not market_data:
  162. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch market data for {token}")
  163. return
  164. current_price = float(market_data['ticker'].get('last', 0))
  165. if current_price <= 0:
  166. await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid current price for {token}")
  167. return
  168. # Determine order type and price
  169. if limit_price:
  170. order_type = "Limit"
  171. price = limit_price
  172. token_amount = usdc_amount / price
  173. else:
  174. order_type = "Market"
  175. price = current_price
  176. token_amount = usdc_amount / current_price
  177. # Validate stop loss for short positions
  178. if stop_loss_price and stop_loss_price <= price:
  179. await context.bot.send_message(chat_id=chat_id, text=(
  180. f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
  181. f"📊 Your order:\n"
  182. f"• Entry Price: ${price:,.2f}\n"
  183. f"• Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
  184. f"💡 Try a higher stop loss like: sl:{price * 1.05:.0f}"
  185. ))
  186. return
  187. # Create confirmation message
  188. confirmation_text = f"""
  189. 🔴 <b>Short Order Confirmation</b>
  190. 📊 <b>Order Details:</b>
  191. • Token: {token}
  192. • USDC Amount: ${usdc_amount:,.2f}
  193. • Token Amount: {token_amount:.6f} {token}
  194. • Order Type: {order_type}
  195. • Price: ${price:,.2f}
  196. • Current Price: ${current_price:,.2f}
  197. • Est. Value: ${token_amount * price:,.2f}"""
  198. if stop_loss_price:
  199. confirmation_text += f"""
  200. • 🛑 Stop Loss: ${stop_loss_price:,.2f}"""
  201. confirmation_text += f"""
  202. ⚠️ <b>Are you sure you want to open this SHORT position?</b>
  203. This will {"place a limit sell order" if limit_price else "execute a market sell order"} for {token}."""
  204. if stop_loss_price:
  205. confirmation_text += f"\nStop loss will be set automatically when order fills."
  206. # Create callback data for confirmation
  207. callback_data = f"confirm_short_{token}_{usdc_amount}_{price if limit_price else 'market'}"
  208. if stop_loss_price:
  209. callback_data += f"_sl_{stop_loss_price}"
  210. keyboard = [
  211. [
  212. InlineKeyboardButton("✅ Execute Short", callback_data=callback_data),
  213. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  214. ]
  215. ]
  216. reply_markup = InlineKeyboardMarkup(keyboard)
  217. await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  218. except ValueError as e:
  219. await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid input format: {e}")
  220. except Exception as e:
  221. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing short command: {e}")
  222. logger.error(f"Error in short command: {e}")
  223. async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  224. """Handle the /exit command for closing positions."""
  225. chat_id = update.effective_chat.id
  226. if not self._is_authorized(chat_id):
  227. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  228. return
  229. try:
  230. if not context.args or len(context.args) < 1:
  231. await context.bot.send_message(chat_id=chat_id, text=(
  232. "❌ Usage: /exit [token]\n"
  233. "Example: /exit BTC\n\n"
  234. "This closes your entire position for the specified token."
  235. ))
  236. return
  237. token = context.args[0].upper()
  238. # Find the position
  239. position = self.trading_engine.find_position(token)
  240. if not position:
  241. await context.bot.send_message(chat_id=chat_id, text=f"📭 No open position found for {token}")
  242. return
  243. # Get position details
  244. position_type, exit_side, contracts = self.trading_engine.get_position_direction(position)
  245. entry_price = float(position.get('entryPx', 0))
  246. unrealized_pnl = float(position.get('unrealizedPnl', 0))
  247. # Get current market price
  248. market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC")
  249. if not market_data:
  250. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch current price for {token}")
  251. return
  252. current_price = float(market_data['ticker'].get('last', 0))
  253. exit_value = contracts * current_price
  254. # Create confirmation message
  255. pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
  256. exit_emoji = "🔴" if position_type == "LONG" else "🟢"
  257. confirmation_text = f"""
  258. {exit_emoji} <b>Exit Position Confirmation</b>
  259. 📊 <b>Position Details:</b>
  260. • Token: {token}
  261. • Position: {position_type}
  262. • Size: {contracts:.6f} contracts
  263. • Entry Price: ${entry_price:,.2f}
  264. • Current Price: ${current_price:,.2f}
  265. • {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f}
  266. 🎯 <b>Exit Order:</b>
  267. • Action: {exit_side.upper()} (Close {position_type})
  268. • Amount: {contracts:.6f} {token}
  269. • Est. Value: ~${exit_value:,.2f}
  270. • Order Type: Market Order
  271. ⚠️ <b>Are you sure you want to close this {position_type} position?</b>
  272. """
  273. keyboard = [
  274. [
  275. InlineKeyboardButton(f"✅ Close {position_type}", callback_data=f"confirm_exit_{token}"),
  276. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  277. ]
  278. ]
  279. reply_markup = InlineKeyboardMarkup(keyboard)
  280. await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  281. except Exception as e:
  282. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing exit command: {e}")
  283. logger.error(f"Error in exit command: {e}")
  284. async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  285. """Handle the /sl (stop loss) command."""
  286. chat_id = update.effective_chat.id
  287. if not self._is_authorized(chat_id):
  288. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  289. return
  290. try:
  291. if not context.args or len(context.args) < 2:
  292. await context.bot.send_message(chat_id=chat_id, text=(
  293. "❌ Usage: /sl [token] [price]\n"
  294. "Example: /sl BTC 44000\n\n"
  295. "This sets a stop loss order for your existing position."
  296. ))
  297. return
  298. token = context.args[0].upper()
  299. stop_price = float(context.args[1])
  300. # Find the position
  301. position = self.trading_engine.find_position(token)
  302. if not position:
  303. await context.bot.send_message(chat_id=chat_id, text=f"📭 No open position found for {token}")
  304. return
  305. # Get position details
  306. position_type, exit_side, contracts = self.trading_engine.get_position_direction(position)
  307. entry_price = float(position.get('entryPx', 0))
  308. # Validate stop loss price based on position direction
  309. if position_type == "LONG" and stop_price >= entry_price:
  310. await context.bot.send_message(chat_id=chat_id, text=(
  311. f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n"
  312. f"📊 Your {token} LONG position:\n"
  313. f"• Entry Price: ${entry_price:,.2f}\n"
  314. f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
  315. f"💡 Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
  316. ))
  317. return
  318. elif position_type == "SHORT" and stop_price <= entry_price:
  319. await context.bot.send_message(chat_id=chat_id, text=(
  320. f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
  321. f"📊 Your {token} SHORT position:\n"
  322. f"• Entry Price: ${entry_price:,.2f}\n"
  323. f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
  324. f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
  325. ))
  326. return
  327. # Get current market price
  328. market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC")
  329. current_price = 0
  330. if market_data:
  331. current_price = float(market_data['ticker'].get('last', 0))
  332. # Calculate estimated P&L at stop loss
  333. if position_type == "LONG":
  334. pnl_at_stop = (stop_price - entry_price) * contracts
  335. else: # SHORT
  336. pnl_at_stop = (entry_price - stop_price) * contracts
  337. pnl_emoji = "🟢" if pnl_at_stop >= 0 else "🔴"
  338. confirmation_text = f"""
  339. 🛑 <b>Stop Loss Order Confirmation</b>
  340. 📊 <b>Position Details:</b>
  341. • Token: {token}
  342. • Position: {position_type}
  343. • Size: {contracts:.6f} contracts
  344. • Entry Price: ${entry_price:,.2f}
  345. • Current Price: ${current_price:,.2f}
  346. 🎯 <b>Stop Loss Order:</b>
  347. • Stop Price: ${stop_price:,.2f}
  348. • Action: {exit_side.upper()} (Close {position_type})
  349. • Amount: {contracts:.6f} {token}
  350. • Order Type: Limit Order
  351. • {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f}
  352. ⚠️ <b>Are you sure you want to set this stop loss?</b>
  353. This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position.
  354. """
  355. keyboard = [
  356. [
  357. InlineKeyboardButton("✅ Set Stop Loss", callback_data=f"confirm_sl_{token}_{stop_price}"),
  358. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  359. ]
  360. ]
  361. reply_markup = InlineKeyboardMarkup(keyboard)
  362. await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  363. except ValueError:
  364. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid price format. Please use numbers only.")
  365. except Exception as e:
  366. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing stop loss command: {e}")
  367. logger.error(f"Error in sl command: {e}")
  368. async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  369. """Handle the /tp (take profit) command."""
  370. chat_id = update.effective_chat.id
  371. if not self._is_authorized(chat_id):
  372. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  373. return
  374. try:
  375. if not context.args or len(context.args) < 2:
  376. await context.bot.send_message(chat_id=chat_id, text=(
  377. "❌ Usage: /tp [token] [price]\n"
  378. "Example: /tp BTC 50000\n\n"
  379. "This sets a take profit order for your existing position."
  380. ))
  381. return
  382. token = context.args[0].upper()
  383. tp_price = float(context.args[1])
  384. # Find the position
  385. position = self.trading_engine.find_position(token)
  386. if not position:
  387. await context.bot.send_message(chat_id=chat_id, text=f"📭 No open position found for {token}")
  388. return
  389. # Get position details
  390. position_type, exit_side, contracts = self.trading_engine.get_position_direction(position)
  391. entry_price = float(position.get('entryPx', 0))
  392. # Validate take profit price based on position direction
  393. if position_type == "LONG" and tp_price <= entry_price:
  394. await context.bot.send_message(chat_id=chat_id, text=(
  395. f"⚠️ Take profit price should be ABOVE entry price for long positions\n\n"
  396. f"📊 Your {token} LONG position:\n"
  397. f"• Entry Price: ${entry_price:,.2f}\n"
  398. f"• Take Profit: ${tp_price:,.2f} ❌\n\n"
  399. f"💡 Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
  400. ))
  401. return
  402. elif position_type == "SHORT" and tp_price >= entry_price:
  403. await context.bot.send_message(chat_id=chat_id, text=(
  404. f"⚠️ Take profit price should be BELOW entry price for short positions\n\n"
  405. f"📊 Your {token} SHORT position:\n"
  406. f"• Entry Price: ${entry_price:,.2f}\n"
  407. f"• Take Profit: ${tp_price:,.2f} ❌\n\n"
  408. f"💡 Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
  409. ))
  410. return
  411. # Get current market price
  412. market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC")
  413. current_price = 0
  414. if market_data:
  415. current_price = float(market_data['ticker'].get('last', 0))
  416. # Calculate estimated P&L at take profit
  417. if position_type == "LONG":
  418. pnl_at_tp = (tp_price - entry_price) * contracts
  419. else: # SHORT
  420. pnl_at_tp = (entry_price - tp_price) * contracts
  421. pnl_emoji = "🟢" if pnl_at_tp >= 0 else "🔴"
  422. confirmation_text = f"""
  423. 🎯 <b>Take Profit Order Confirmation</b>
  424. 📊 <b>Position Details:</b>
  425. • Token: {token}
  426. • Position: {position_type}
  427. • Size: {contracts:.6f} contracts
  428. • Entry Price: ${entry_price:,.2f}
  429. • Current Price: ${current_price:,.2f}
  430. 🎯 <b>Take Profit Order:</b>
  431. • Take Profit Price: ${tp_price:,.2f}
  432. • Action: {exit_side.upper()} (Close {position_type})
  433. • Amount: {contracts:.6f} {token}
  434. • Order Type: Limit Order
  435. • {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f}
  436. ⚠️ <b>Are you sure you want to set this take profit?</b>
  437. This will place a limit {exit_side} order at ${tp_price:,.2f} to secure profits from your {position_type} position.
  438. """
  439. keyboard = [
  440. [
  441. InlineKeyboardButton("✅ Set Take Profit", callback_data=f"confirm_tp_{token}_{tp_price}"),
  442. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  443. ]
  444. ]
  445. reply_markup = InlineKeyboardMarkup(keyboard)
  446. await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  447. except ValueError:
  448. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid price format. Please use numbers only.")
  449. except Exception as e:
  450. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing take profit command: {e}")
  451. logger.error(f"Error in tp command: {e}")
  452. async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  453. """Handle the /coo (cancel all orders) command."""
  454. chat_id = update.effective_chat.id
  455. if not self._is_authorized(chat_id):
  456. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  457. return
  458. try:
  459. if not context.args or len(context.args) < 1:
  460. await context.bot.send_message(chat_id=chat_id, text=(
  461. "❌ Usage: /coo [token]\n"
  462. "Example: /coo BTC\n\n"
  463. "This cancels all open orders for the specified token."
  464. ))
  465. return
  466. token = context.args[0].upper()
  467. confirmation_text = f"""
  468. 🚫 <b>Cancel All Orders Confirmation</b>
  469. 📊 <b>Action:</b> Cancel all open orders for {token}
  470. ⚠️ <b>Are you sure you want to cancel all {token} orders?</b>
  471. This will cancel ALL pending orders for {token}, including:
  472. • Limit orders
  473. • Stop loss orders
  474. • Take profit orders
  475. This action cannot be undone.
  476. """
  477. keyboard = [
  478. [
  479. InlineKeyboardButton(f"✅ Cancel All {token} Orders", callback_data=f"confirm_coo_{token}"),
  480. InlineKeyboardButton("❌ Keep Orders", callback_data="cancel_order")
  481. ]
  482. ]
  483. reply_markup = InlineKeyboardMarkup(keyboard)
  484. await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  485. except Exception as e:
  486. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing cancel orders command: {e}")
  487. logger.error(f"Error in coo command: {e}")
  488. async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  489. """Handle button callbacks for trading commands."""
  490. query = update.callback_query
  491. await query.answer()
  492. if not self._is_authorized(query.message.chat_id):
  493. await query.edit_message_text("❌ Unauthorized access.")
  494. return
  495. callback_data = query.data
  496. logger.info(f"Button callback triggered with data: {callback_data}")
  497. # Define a map for informational and management command callbacks
  498. # These commands expect `update` and `context` as if called by a CommandHandler
  499. command_action_map = {}
  500. if self.info_commands_handler:
  501. command_action_map.update({
  502. "balance": self.info_commands_handler.balance_command,
  503. "positions": self.info_commands_handler.positions_command,
  504. "orders": self.info_commands_handler.orders_command,
  505. "stats": self.info_commands_handler.stats_command,
  506. "price": self.info_commands_handler.price_command,
  507. "market": self.info_commands_handler.market_command,
  508. "performance": self.info_commands_handler.performance_command,
  509. "daily": self.info_commands_handler.daily_command,
  510. "weekly": self.info_commands_handler.weekly_command,
  511. "monthly": self.info_commands_handler.monthly_command,
  512. "trades": self.info_commands_handler.trades_command,
  513. # Note: 'help' is handled separately below as its main handler is in TelegramTradingBot core
  514. })
  515. if self.management_commands_handler:
  516. command_action_map.update({
  517. "alarm": self.management_commands_handler.alarm_command,
  518. "monitoring": self.management_commands_handler.monitoring_command,
  519. "logs": self.management_commands_handler.logs_command,
  520. # Add other management commands here if they have quick action buttons
  521. })
  522. # Prepare key for map lookup, stripping leading '/' if present for general commands
  523. mapped_command_key = callback_data
  524. if callback_data.startswith('/') and not callback_data.startswith('/confirm_'): # Avoid stripping for confirm actions
  525. mapped_command_key = callback_data[1:]
  526. # Check if the callback_data matches a mapped informational/management command
  527. if mapped_command_key in command_action_map:
  528. command_method = command_action_map[mapped_command_key]
  529. try:
  530. logger.info(f"Executing {mapped_command_key} command (from callback: {callback_data}) via button callback.")
  531. # Edit the original message to indicate the action is being processed
  532. # await query.edit_message_text(text=f"🔄 Processing {mapped_command_key.capitalize()}...", parse_mode='HTML') # Optional
  533. await command_method(update, context) # Call the actual command method
  534. # After the command sends its own message(s), we might want to remove or clean up the original message with buttons.
  535. # For now, let the command method handle all responses.
  536. # Optionally, delete the message that had the buttons:
  537. # await query.message.delete()
  538. except Exception as e:
  539. logger.error(f"Error executing command '{mapped_command_key}' from button: {e}", exc_info=True)
  540. try:
  541. await query.message.reply_text(f"❌ Error processing {mapped_command_key.capitalize()}: {e}")
  542. except Exception as reply_e:
  543. logger.error(f"Failed to send error reply for {mapped_command_key} button: {reply_e}")
  544. return # Handled
  545. # Special handling for 'help' callback from InfoCommands quick menu
  546. # This check should use the modified key as well if we want /help to work via this mechanism
  547. # However, the 'help' key in command_action_map is 'help', not '/help'.
  548. # The current 'help' handling is separate and specific. Let's adjust it for consistency if needed or verify.
  549. # The previous change to info_commands.py made help callback_data='/help'.
  550. if callback_data == "/help": # Check for the actual callback_data value
  551. logger.info("Handling '/help' button callback. Guiding user to /help command.")
  552. try:
  553. # Remove the inline keyboard from the original message and provide guidance.
  554. await query.edit_message_text(
  555. text="📖 To view all commands and their descriptions, please type the /help command.",
  556. reply_markup=None, # Remove buttons
  557. parse_mode='HTML'
  558. )
  559. except Exception as e:
  560. logger.error(f"Error editing message for 'help' callback: {e}")
  561. # Fallback if edit fails (e.g., message too old)
  562. await query.message.reply_text("📖 Please type /help for command details.", parse_mode='HTML')
  563. return # Handled
  564. # Existing trading confirmation logic
  565. if callback_data.startswith("confirm_long_"):
  566. await self._execute_long_callback(query, callback_data)
  567. elif callback_data.startswith("confirm_short_"):
  568. await self._execute_short_callback(query, callback_data)
  569. elif callback_data.startswith("confirm_exit_"):
  570. await self._execute_exit_callback(query, callback_data)
  571. elif callback_data.startswith("confirm_sl_"):
  572. await self._execute_sl_callback(query, callback_data)
  573. elif callback_data.startswith("confirm_tp_"):
  574. await self._execute_tp_callback(query, callback_data)
  575. elif callback_data.startswith("confirm_coo_"):
  576. await self._execute_coo_callback(query, callback_data)
  577. elif callback_data == 'cancel_order':
  578. await query.edit_message_text("❌ Order cancelled.")
  579. async def _execute_long_callback(self, query, callback_data):
  580. """Execute long order from callback."""
  581. parts = callback_data.split('_')
  582. token = parts[2]
  583. usdc_amount = float(parts[3])
  584. price = None if parts[4] == 'market' else float(parts[4])
  585. stop_loss_price = None
  586. # Check for stop loss
  587. if len(parts) > 5 and parts[5] == 'sl':
  588. stop_loss_price = float(parts[6])
  589. await query.edit_message_text("⏳ Executing long order...")
  590. result = await self.trading_engine.execute_long_order(token, usdc_amount, price, stop_loss_price)
  591. if result["success"]:
  592. await self.notification_manager.send_long_success_notification(
  593. query, token, result["token_amount"], result["actual_price"], result["order"], stop_loss_price
  594. )
  595. else:
  596. await query.edit_message_text(f"❌ Long order failed: {result['error']}")
  597. async def _execute_short_callback(self, query, callback_data):
  598. """Execute short order from callback."""
  599. parts = callback_data.split('_')
  600. token = parts[2]
  601. usdc_amount = float(parts[3])
  602. price = None if parts[4] == 'market' else float(parts[4])
  603. stop_loss_price = None
  604. # Check for stop loss
  605. if len(parts) > 5 and parts[5] == 'sl':
  606. stop_loss_price = float(parts[6])
  607. await query.edit_message_text("⏳ Executing short order...")
  608. result = await self.trading_engine.execute_short_order(token, usdc_amount, price, stop_loss_price)
  609. if result["success"]:
  610. await self.notification_manager.send_short_success_notification(
  611. query, token, result["token_amount"], result["actual_price"], result["order"], stop_loss_price
  612. )
  613. else:
  614. await query.edit_message_text(f"❌ Short order failed: {result['error']}")
  615. async def _execute_exit_callback(self, query, callback_data):
  616. """Execute exit order from callback."""
  617. parts = callback_data.split('_')
  618. token = parts[2]
  619. await query.edit_message_text("⏳ Closing position...")
  620. result = await self.trading_engine.execute_exit_order(token)
  621. if result["success"]:
  622. await self.notification_manager.send_exit_success_notification(
  623. query, token, result["position_type_closed"], result["contracts_intended_to_close"],
  624. result.get("actual_price", 0), result.get("pnl", 0),
  625. {**result.get("order_placed_details", {}), "cancelled_stop_losses": result.get("cancelled_stop_losses", 0)}
  626. )
  627. else:
  628. await query.edit_message_text(f"❌ Exit order failed: {result['error']}")
  629. async def _execute_sl_callback(self, query, callback_data):
  630. """Execute stop loss order from callback."""
  631. parts = callback_data.split('_')
  632. token = parts[2]
  633. stop_price = float(parts[3])
  634. await query.edit_message_text("⏳ Setting stop loss...")
  635. result = await self.trading_engine.execute_sl_order(token, stop_price)
  636. if result["success"]:
  637. await self.notification_manager.send_sl_success_notification(
  638. query, token, result["position_type_for_sl"], result["contracts_for_sl"],
  639. stop_price, result.get("order_placed_details", {})
  640. )
  641. else:
  642. await query.edit_message_text(f"❌ Stop loss failed: {result['error']}")
  643. async def _execute_tp_callback(self, query, callback_data):
  644. """Execute take profit order from callback."""
  645. parts = callback_data.split('_')
  646. token = parts[2]
  647. tp_price = float(parts[3])
  648. await query.edit_message_text("⏳ Setting take profit...")
  649. result = await self.trading_engine.execute_tp_order(token, tp_price)
  650. if result["success"]:
  651. await self.notification_manager.send_tp_success_notification(
  652. query, token, result["position_type_for_tp"], result["contracts_for_tp"],
  653. tp_price, result.get("order_placed_details", {})
  654. )
  655. else:
  656. await query.edit_message_text(f"❌ Take profit failed: {result['error']}")
  657. async def _execute_coo_callback(self, query, callback_data):
  658. """Execute cancel all orders from callback."""
  659. parts = callback_data.split('_')
  660. token = parts[2]
  661. await query.edit_message_text("⏳ Cancelling orders...")
  662. result = await self.trading_engine.execute_coo_order(token)
  663. if result["success"]:
  664. cancelled_count = result.get("cancelled_count", 0)
  665. failed_count = result.get("failed_count", 0)
  666. cancelled_linked_sls = result.get("cancelled_linked_stop_losses", 0)
  667. cancelled_orders = result.get("cancelled_orders", [])
  668. await self.notification_manager.send_coo_success_notification(
  669. query, token, cancelled_count, failed_count, cancelled_linked_sls, cancelled_orders
  670. )
  671. else:
  672. await query.edit_message_text(f"❌ Cancel orders failed: {result['error']}")