trading_commands.py 41 KB

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