trading_commands.py 42 KB

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