info_commands.py 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485
  1. #!/usr/bin/env python3
  2. """
  3. Info Commands - Handles information-related Telegram commands.
  4. """
  5. import logging
  6. from datetime import datetime, timezone, timedelta
  7. from typing import Optional, Dict, Any, List
  8. from telegram import Update
  9. from telegram.ext import ContextTypes
  10. from src.config.config import Config
  11. from src.utils.price_formatter import format_price_with_symbol, get_formatter
  12. logger = logging.getLogger(__name__)
  13. def _normalize_token_case(token: str) -> str:
  14. """
  15. Normalize token case: if any characters are already uppercase, keep as-is.
  16. Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK.
  17. """
  18. # Check if any character is already uppercase
  19. if any(c.isupper() for c in token):
  20. return token # Keep original case for mixed-case tokens
  21. else:
  22. return token.upper() # Convert to uppercase for all-lowercase input
  23. class InfoCommands:
  24. """Handles all information-related Telegram commands."""
  25. def __init__(self, trading_engine, notification_manager=None):
  26. """Initialize with trading engine and notification manager."""
  27. self.trading_engine = trading_engine
  28. self.notification_manager = notification_manager
  29. def _is_authorized(self, chat_id: str) -> bool:
  30. """Check if the chat ID is authorized."""
  31. return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
  32. async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  33. """Handle the /balance command."""
  34. chat_id = update.effective_chat.id
  35. if not self._is_authorized(chat_id):
  36. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  37. return
  38. balance = self.trading_engine.get_balance()
  39. if balance:
  40. balance_text = "💰 <b>Account Balance</b>\n\n"
  41. # Debug: Show raw balance structure (can be removed after debugging)
  42. logger.debug(f"Raw balance data: {balance}")
  43. # CCXT balance structure includes 'free', 'used', and 'total'
  44. total_balance = balance.get('total', {})
  45. free_balance = balance.get('free', {})
  46. used_balance = balance.get('used', {})
  47. # Get total portfolio value
  48. total_portfolio_value = 0
  49. # Show USDC balance prominently
  50. if 'USDC' in total_balance:
  51. usdc_total = float(total_balance['USDC'])
  52. usdc_free = float(free_balance.get('USDC', 0))
  53. usdc_used = float(used_balance.get('USDC', 0))
  54. balance_text += f"💵 <b>USDC:</b>\n"
  55. balance_text += f" 📊 Total: ${usdc_total:,.2f}\n"
  56. balance_text += f" ✅ Available: ${usdc_free:,.2f}\n"
  57. balance_text += f" 🔒 In Use: ${usdc_used:,.2f}\n\n"
  58. total_portfolio_value += usdc_total
  59. # Show other non-zero balances
  60. other_assets = []
  61. for asset, amount in total_balance.items():
  62. if asset != 'USDC' and float(amount) > 0:
  63. other_assets.append((asset, float(amount)))
  64. if other_assets:
  65. balance_text += "📊 <b>Other Assets:</b>\n"
  66. for asset, amount in other_assets:
  67. free_amount = float(free_balance.get(asset, 0))
  68. used_amount = float(used_balance.get(asset, 0))
  69. balance_text += f"💵 <b>{asset}:</b>\n"
  70. balance_text += f" 📊 Total: {amount:.6f}\n"
  71. balance_text += f" ✅ Available: {free_amount:.6f}\n"
  72. balance_text += f" 🔒 In Use: {used_amount:.6f}\n\n"
  73. # Portfolio summary
  74. usdc_balance = float(total_balance.get('USDC', 0))
  75. stats = self.trading_engine.get_stats()
  76. if stats:
  77. basic_stats = stats.get_basic_stats()
  78. initial_balance = basic_stats.get('initial_balance', usdc_balance)
  79. pnl = usdc_balance - initial_balance
  80. pnl_percent = (pnl / initial_balance * 100) if initial_balance > 0 else 0
  81. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  82. balance_text += f"💼 <b>Portfolio Summary:</b>\n"
  83. balance_text += f" 💰 Total Value: ${total_portfolio_value:,.2f}\n"
  84. balance_text += f" 🚀 Available for Trading: ${float(free_balance.get('USDC', 0)):,.2f}\n"
  85. balance_text += f" 🔒 In Active Use: ${float(used_balance.get('USDC', 0)):,.2f}\n\n"
  86. balance_text += f"📊 <b>Performance:</b>\n"
  87. balance_text += f" 💵 Initial: ${initial_balance:,.2f}\n"
  88. balance_text += f" {pnl_emoji} P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
  89. await context.bot.send_message(chat_id=chat_id, text=balance_text, parse_mode='HTML')
  90. else:
  91. await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch balance information")
  92. async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  93. """Handle the /positions command."""
  94. chat_id = update.effective_chat.id
  95. if not self._is_authorized(chat_id):
  96. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  97. return
  98. # 🧹 PHASE 4: Use unified trades table as the single source of truth
  99. stats = self.trading_engine.get_stats()
  100. if not stats:
  101. await context.bot.send_message(chat_id=chat_id, text="❌ Trading statistics not available.")
  102. return
  103. # 🆕 AUTO-SYNC: Check for positions on exchange that don't have trade lifecycle records
  104. # Use cached data from MarketMonitor if available (updated every heartbeat)
  105. if (hasattr(self.trading_engine, 'market_monitor') and
  106. self.trading_engine.market_monitor and
  107. hasattr(self.trading_engine.market_monitor, 'get_cached_positions')):
  108. cache_age = self.trading_engine.market_monitor.get_cache_age_seconds()
  109. if cache_age < 60: # Use cached data if less than 1 minute old
  110. exchange_positions = self.trading_engine.market_monitor.get_cached_positions() or []
  111. logger.debug(f"Using cached positions for auto-sync (age: {cache_age:.1f}s)")
  112. else:
  113. exchange_positions = self.trading_engine.get_positions() or []
  114. logger.debug("Using fresh API call for auto-sync (cache too old)")
  115. else:
  116. exchange_positions = self.trading_engine.get_positions() or []
  117. logger.debug("Using fresh API call for auto-sync (no cache available)")
  118. synced_positions = []
  119. for exchange_pos in exchange_positions:
  120. symbol = exchange_pos.get('symbol')
  121. contracts = float(exchange_pos.get('contracts', 0))
  122. if symbol and abs(contracts) > 0:
  123. # Check if we have a trade lifecycle record for this position
  124. existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
  125. if not existing_trade:
  126. # 🚨 ORPHANED POSITION: Auto-create trade lifecycle record using exchange data
  127. entry_price = float(exchange_pos.get('entryPrice', 0))
  128. position_side = 'long' if contracts > 0 else 'short'
  129. order_side = 'buy' if contracts > 0 else 'sell'
  130. # ✅ Use exchange data - no need to estimate!
  131. if entry_price > 0:
  132. logger.info(f"🔄 Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (exchange data)")
  133. else:
  134. # Fallback only if exchange truly doesn't provide entry price
  135. entry_price = await self._estimate_entry_price_for_orphaned_position(symbol, contracts)
  136. logger.warning(f"🔄 Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (estimated)")
  137. # Create trade lifecycle for external position
  138. lifecycle_id = stats.create_trade_lifecycle(
  139. symbol=symbol,
  140. side=order_side,
  141. entry_order_id=f"external_sync_{int(datetime.now().timestamp())}",
  142. trade_type='external'
  143. )
  144. if lifecycle_id:
  145. # Update to position_opened status
  146. success = stats.update_trade_position_opened(
  147. lifecycle_id=lifecycle_id,
  148. entry_price=entry_price,
  149. entry_amount=abs(contracts),
  150. exchange_fill_id=f"external_fill_{int(datetime.now().timestamp())}"
  151. )
  152. if success:
  153. synced_positions.append(symbol)
  154. logger.info(f"✅ Successfully synced orphaned position for {symbol}")
  155. # 🆕 Send immediate notification for auto-synced position
  156. token = symbol.split('/')[0] if '/' in symbol else symbol
  157. unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
  158. position_value = float(exchange_pos.get('notional', 0))
  159. liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
  160. leverage = float(exchange_pos.get('leverage', 1))
  161. pnl_percentage = float(exchange_pos.get('percentage', 0))
  162. pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
  163. notification_text = (
  164. f"🔄 <b>Position Auto-Synced</b>\n\n"
  165. f"🎯 Token: {token}\n"
  166. f"📈 Direction: {position_side.upper()}\n"
  167. f"📏 Size: {abs(contracts):.6f} {token}\n"
  168. f"💰 Entry: ${entry_price:,.4f}\n"
  169. f"💵 Value: ${position_value:,.2f}\n"
  170. f"{pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
  171. )
  172. if leverage > 1:
  173. notification_text += f"⚡ Leverage: {leverage:.1f}x\n"
  174. if liquidation_price > 0:
  175. notification_text += f"⚠️ Liquidation: ${liquidation_price:,.2f}\n"
  176. notification_text += (
  177. f"\n📍 Reason: Position opened outside bot\n"
  178. f"⏰ Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
  179. f"✅ Position now tracked in bot\n"
  180. f"💡 Use /sl {token} [price] to set stop loss"
  181. )
  182. # Send notification via trading engine's notification manager
  183. if self.notification_manager:
  184. try:
  185. await self.notification_manager.send_generic_notification(notification_text)
  186. logger.info(f"📤 Sent auto-sync notification for {symbol}")
  187. except Exception as e:
  188. logger.error(f"❌ Failed to send auto-sync notification: {e}")
  189. else:
  190. logger.warning(f"⚠️ No notification manager available for auto-sync notification")
  191. else:
  192. logger.error(f"❌ Failed to sync orphaned position for {symbol}")
  193. else:
  194. logger.error(f"❌ Failed to create lifecycle for orphaned position {symbol}")
  195. if synced_positions:
  196. sync_msg = f"🔄 <b>Auto-synced {len(synced_positions)} orphaned position(s):</b> {', '.join([s.split('/')[0] for s in synced_positions])}\n\n"
  197. else:
  198. sync_msg = ""
  199. # Get open positions from unified trades table (now including any newly synced ones)
  200. open_positions = stats.get_open_positions()
  201. positions_text = f"📈 <b>Open Positions</b>\n\n{sync_msg}"
  202. if open_positions:
  203. total_unrealized = 0
  204. total_position_value = 0
  205. # Also get fresh exchange data for display
  206. fresh_exchange_positions = self.trading_engine.get_positions() or []
  207. exchange_data_map = {pos.get('symbol'): pos for pos in fresh_exchange_positions}
  208. for position_trade in open_positions:
  209. symbol = position_trade['symbol']
  210. token = symbol.split('/')[0] if '/' in symbol else symbol
  211. position_side = position_trade['position_side'] # 'long' or 'short'
  212. entry_price = position_trade['entry_price']
  213. current_amount = position_trade['current_position_size']
  214. trade_type = position_trade.get('trade_type', 'manual')
  215. # 🆕 Use fresh exchange data if available (most accurate)
  216. exchange_pos = exchange_data_map.get(symbol)
  217. if exchange_pos:
  218. # Use exchange's official data
  219. unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
  220. mark_price = float(exchange_pos.get('markPrice') or 0)
  221. position_value = float(exchange_pos.get('notional', 0))
  222. liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
  223. margin_used = float(exchange_pos.get('initialMargin', 0))
  224. leverage = float(exchange_pos.get('leverage', 1))
  225. pnl_percentage = float(exchange_pos.get('percentage', 0))
  226. # Get mark price from market data if not in position data
  227. if mark_price <= 0:
  228. try:
  229. market_data = self.trading_engine.get_market_data(symbol)
  230. if market_data and market_data.get('ticker'):
  231. mark_price = float(market_data['ticker'].get('last', entry_price))
  232. except:
  233. mark_price = entry_price # Fallback
  234. else:
  235. # Fallback to our calculation if exchange data unavailable
  236. unrealized_pnl = position_trade.get('unrealized_pnl', 0)
  237. mark_price = entry_price # Fallback
  238. try:
  239. market_data = self.trading_engine.get_market_data(symbol)
  240. if market_data and market_data.get('ticker'):
  241. mark_price = float(market_data['ticker'].get('last', entry_price))
  242. # Calculate unrealized PnL with current price
  243. if position_side == 'long':
  244. unrealized_pnl = current_amount * (mark_price - entry_price)
  245. else: # Short position
  246. unrealized_pnl = current_amount * (entry_price - mark_price)
  247. except:
  248. pass # Use entry price as fallback
  249. position_value = abs(current_amount) * mark_price
  250. liquidation_price = None
  251. margin_used = None
  252. leverage = None
  253. pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
  254. total_position_value += position_value
  255. total_unrealized += unrealized_pnl
  256. # Position emoji and formatting
  257. if position_side == 'long':
  258. pos_emoji = "🟢"
  259. direction = "LONG"
  260. else: # Short position
  261. pos_emoji = "🔴"
  262. direction = "SHORT"
  263. pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
  264. # Format prices with proper precision for this token
  265. formatter = get_formatter()
  266. entry_price_str = formatter.format_price_with_symbol(entry_price, token)
  267. mark_price_str = formatter.format_price_with_symbol(mark_price, token)
  268. # Trade type indicator
  269. type_indicator = ""
  270. if trade_type == 'external':
  271. type_indicator = " 🔄" # External/synced position
  272. elif trade_type == 'bot':
  273. type_indicator = " 🤖" # Bot-created position
  274. positions_text += f"{pos_emoji} <b>{token} ({direction}){type_indicator}</b>\n"
  275. positions_text += f" 📏 Size: {abs(current_amount):.6f} {token}\n"
  276. positions_text += f" 💰 Entry: {entry_price_str}\n"
  277. positions_text += f" 📊 Mark: {mark_price_str}\n"
  278. positions_text += f" 💵 Value: ${position_value:,.2f}\n"
  279. positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
  280. # Show exchange-provided risk data if available
  281. if leverage:
  282. positions_text += f" ⚡ Leverage: {leverage:.1f}x\n"
  283. if margin_used:
  284. positions_text += f" 💳 Margin: ${margin_used:,.2f}\n"
  285. if liquidation_price:
  286. liq_price_str = formatter.format_price_with_symbol(liquidation_price, token)
  287. positions_text += f" ⚠️ Liquidation: {liq_price_str}\n"
  288. # Show stop loss if linked
  289. if position_trade.get('stop_loss_price'):
  290. sl_price = position_trade['stop_loss_price']
  291. sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active"
  292. positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, token)} ({sl_status})\n"
  293. # Show take profit if linked
  294. if position_trade.get('take_profit_price'):
  295. tp_price = position_trade['take_profit_price']
  296. tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active"
  297. positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, token)} ({tp_status})\n"
  298. positions_text += f" 🆔 Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
  299. # Portfolio summary
  300. portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
  301. positions_text += f"💼 <b>Total Portfolio:</b>\n"
  302. positions_text += f" 💵 Total Value: ${total_position_value:,.2f}\n"
  303. positions_text += f" {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n\n"
  304. positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
  305. positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
  306. else:
  307. positions_text += "📭 No open positions\n\n"
  308. positions_text += "💡 Use /long or /short to open a position"
  309. await context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML')
  310. async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  311. """Handle the /orders command."""
  312. chat_id = update.effective_chat.id
  313. if not self._is_authorized(chat_id):
  314. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  315. return
  316. orders = self.trading_engine.get_orders()
  317. if orders is not None:
  318. if len(orders) > 0:
  319. orders_text = "📋 <b>Open Orders</b>\n\n"
  320. # Group orders by symbol
  321. orders_by_symbol = {}
  322. for order in orders:
  323. symbol = order.get('symbol', '').replace('/USDC:USDC', '')
  324. if symbol not in orders_by_symbol:
  325. orders_by_symbol[symbol] = []
  326. orders_by_symbol[symbol].append(order)
  327. for symbol, symbol_orders in orders_by_symbol.items():
  328. orders_text += f"📊 <b>{symbol}</b>\n"
  329. formatter = get_formatter()
  330. for order in symbol_orders:
  331. side = order.get('side', '').upper()
  332. amount = float(order.get('amount', 0))
  333. price = float(order.get('price', 0))
  334. order_type = order.get('type', 'unknown').title()
  335. order_id = order.get('id', 'N/A')
  336. # Order emoji
  337. side_emoji = "🟢" if side == "BUY" else "🔴"
  338. orders_text += f" {side_emoji} {side} {amount:.6f} @ {formatter.format_price_with_symbol(price, symbol)}\n"
  339. orders_text += f" 📋 Type: {order_type} | ID: {order_id}\n"
  340. # Check for pending stop losses linked to this order
  341. stats = self.trading_engine.get_stats()
  342. if stats:
  343. # Try to find this order in our database to get its bot_order_ref_id
  344. order_in_db = stats.get_order_by_exchange_id(order_id)
  345. if order_in_db:
  346. bot_ref_id = order_in_db.get('bot_order_ref_id')
  347. if bot_ref_id:
  348. # Look for pending stop losses with this order as parent
  349. pending_sls = stats.get_orders_by_status(
  350. status='pending_trigger',
  351. order_type_filter='stop_limit_trigger',
  352. parent_bot_order_ref_id=bot_ref_id
  353. )
  354. if pending_sls:
  355. sl_order = pending_sls[0] # Should only be one
  356. sl_price = sl_order.get('price', 0)
  357. sl_side = sl_order.get('side', '').upper()
  358. orders_text += f" 🛑 Pending SL: {sl_side} @ {formatter.format_price_with_symbol(sl_price, symbol)} (activates when filled)\n"
  359. orders_text += "\n"
  360. orders_text += f"💼 <b>Total Orders:</b> {len(orders)}\n"
  361. orders_text += f"💡 Use /coo [token] to cancel orders"
  362. else:
  363. orders_text = "📋 <b>Open Orders</b>\n\n"
  364. orders_text += "📭 No open orders\n\n"
  365. orders_text += "💡 Use /long, /short, /sl, or /tp to create orders"
  366. await context.bot.send_message(chat_id=chat_id, text=orders_text, parse_mode='HTML')
  367. else:
  368. await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch orders")
  369. async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  370. """Handle the /stats command."""
  371. chat_id = update.effective_chat.id
  372. if not self._is_authorized(chat_id):
  373. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  374. return
  375. # Get current balance for stats
  376. balance = self.trading_engine.get_balance()
  377. current_balance = 0
  378. if balance and balance.get('total'):
  379. current_balance = float(balance['total'].get('USDC', 0))
  380. stats = self.trading_engine.get_stats()
  381. if stats:
  382. stats_message = stats.format_stats_message(current_balance)
  383. await context.bot.send_message(chat_id=chat_id, text=stats_message, parse_mode='HTML')
  384. else:
  385. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  386. async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  387. """Handle the /trades command - Show recent trade history."""
  388. if not self._is_authorized(update):
  389. return
  390. try:
  391. stats = self.trading_engine.get_stats()
  392. if not stats:
  393. await update.message.reply_text("❌ Trading statistics not available.", parse_mode='HTML')
  394. return
  395. # Get recent trades (limit to last 20)
  396. recent_trades = stats.get_recent_trades(limit=20)
  397. if not recent_trades:
  398. await update.message.reply_text("📊 <b>No trades found.</b>", parse_mode='HTML')
  399. return
  400. message = "📈 <b>Recent Trades (Last 20)</b>\n\n"
  401. for trade in recent_trades:
  402. symbol = trade['symbol']
  403. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] if ':' in symbol else symbol
  404. side = trade['side'].upper()
  405. amount = trade['amount']
  406. price = trade['price']
  407. timestamp = trade['timestamp']
  408. pnl = trade.get('realized_pnl', 0)
  409. # Format timestamp
  410. try:
  411. from datetime import datetime
  412. dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  413. time_str = dt.strftime('%m/%d %H:%M')
  414. except:
  415. time_str = "Unknown"
  416. # PnL emoji and formatting
  417. if pnl > 0:
  418. pnl_emoji = "🟢"
  419. pnl_str = f"+${pnl:.2f}"
  420. elif pnl < 0:
  421. pnl_emoji = "🔴"
  422. pnl_str = f"${pnl:.2f}"
  423. else:
  424. pnl_emoji = "⚪"
  425. pnl_str = "$0.00"
  426. side_emoji = "🟢" if side == 'BUY' else "🔴"
  427. message += f"{side_emoji} <b>{side}</b> {amount} {token} @ ${price:,.2f}\n"
  428. message += f" {pnl_emoji} P&L: {pnl_str} | {time_str}\n\n"
  429. await update.message.reply_text(message, parse_mode='HTML')
  430. except Exception as e:
  431. logger.error(f"Error in trades command: {e}")
  432. await update.message.reply_text("❌ Error retrieving trade history.", parse_mode='HTML')
  433. async def cycles_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  434. """Handle the /cycles command - Show trade cycles and lifecycle statistics."""
  435. if not self._is_authorized(update):
  436. return
  437. try:
  438. stats = self.trading_engine.get_stats()
  439. if not stats:
  440. await update.message.reply_text("❌ Trading statistics not available.", parse_mode='HTML')
  441. return
  442. # Get trade cycle performance stats
  443. cycle_stats = stats.get_trade_cycle_performance_stats()
  444. if not cycle_stats or cycle_stats.get('total_closed_trades', 0) == 0:
  445. await update.message.reply_text("📊 <b>No completed trade cycles found.</b>", parse_mode='HTML')
  446. return
  447. # Get recent trade cycles
  448. recent_cycles = stats.get_recent_trade_cycles(limit=10)
  449. open_cycles = stats.get_open_trade_cycles()
  450. message = "🔄 <b>Trade Cycle Statistics</b>\n\n"
  451. # Performance summary
  452. total_trades = cycle_stats.get('total_closed_trades', 0)
  453. win_rate = cycle_stats.get('win_rate', 0)
  454. total_pnl = cycle_stats.get('total_pnl', 0)
  455. avg_duration = cycle_stats.get('avg_duration_minutes', 0)
  456. profit_factor = cycle_stats.get('profit_factor', 0)
  457. stop_loss_rate = cycle_stats.get('stop_loss_rate', 0)
  458. pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  459. message += f"📊 <b>Performance Summary:</b>\n"
  460. message += f"• Total Completed: {total_trades} trades\n"
  461. message += f"• Win Rate: {win_rate:.1f}%\n"
  462. message += f"• {pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  463. message += f"• Avg Duration: {avg_duration:.1f} min\n"
  464. message += f"• Profit Factor: {profit_factor:.2f}\n"
  465. message += f"• Stop Loss Rate: {stop_loss_rate:.1f}%\n\n"
  466. # Open cycles
  467. if open_cycles:
  468. message += f"🟢 <b>Open Cycles ({len(open_cycles)}):</b>\n"
  469. for cycle in open_cycles[:5]: # Show max 5
  470. symbol = cycle['symbol']
  471. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
  472. side = cycle['side'].upper()
  473. entry_price = cycle.get('entry_price', 0)
  474. side_emoji = "📈" if side == 'BUY' else "📉"
  475. message += f"{side_emoji} {side} {token} @ ${entry_price:.2f}\n"
  476. message += "\n"
  477. # Recent completed cycles
  478. if recent_cycles:
  479. completed_recent = [c for c in recent_cycles if c['status'] == 'closed'][:5]
  480. if completed_recent:
  481. message += f"📋 <b>Recent Completed ({len(completed_recent)}):</b>\n"
  482. for cycle in completed_recent:
  483. symbol = cycle['symbol']
  484. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
  485. side = cycle['side'].upper()
  486. entry_price = cycle.get('entry_price', 0)
  487. exit_price = cycle.get('exit_price', 0)
  488. pnl = cycle.get('realized_pnl', 0)
  489. exit_type = cycle.get('exit_type', 'unknown')
  490. duration = cycle.get('duration_seconds', 0)
  491. # Format duration
  492. if duration > 3600:
  493. duration_str = f"{duration//3600:.0f}h"
  494. elif duration > 60:
  495. duration_str = f"{duration//60:.0f}m"
  496. else:
  497. duration_str = f"{duration}s"
  498. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  499. side_emoji = "📈" if side == 'BUY' else "📉"
  500. exit_emoji = "🛑" if exit_type == 'stop_loss' else "🎯" if exit_type == 'take_profit' else "👋"
  501. message += f"{side_emoji} {side} {token}: ${entry_price:.2f} → ${exit_price:.2f}\n"
  502. message += f" {pnl_emoji} ${pnl:+.2f} | {exit_emoji} {exit_type} | {duration_str}\n"
  503. message += "\n"
  504. message += "💡 Trade cycles track complete trades from open to close with full P&L analysis."
  505. await update.message.reply_text(message, parse_mode='HTML')
  506. except Exception as e:
  507. error_message = f"❌ Error processing cycles command: {str(e)}"
  508. await context.bot.send_message(chat_id=chat_id, text=error_message)
  509. logger.error(f"Error in cycles command: {e}")
  510. async def active_trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  511. """Handle the /active command to show active trades (using open positions)."""
  512. chat_id = update.effective_chat.id
  513. if not self._is_authorized(chat_id):
  514. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  515. return
  516. try:
  517. stats = self.trading_engine.get_stats()
  518. if not stats:
  519. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  520. return
  521. # Get open positions from unified trades table (current active trades)
  522. open_positions = stats.get_open_positions()
  523. if not open_positions:
  524. await context.bot.send_message(
  525. chat_id=chat_id,
  526. text="📊 <b>Active Positions</b>\n\n📭 No active positions found.\n\n💡 Use /long or /short to open positions.",
  527. parse_mode='HTML'
  528. )
  529. return
  530. message_text = "📊 <b>Active Positions</b>\n\n"
  531. # Show each position
  532. for position in open_positions:
  533. symbol = position['symbol']
  534. token = symbol.split('/')[0] if '/' in symbol else symbol
  535. position_side = position['position_side'] # 'long' or 'short'
  536. entry_price = position['entry_price']
  537. current_amount = position['current_position_size']
  538. trade_type = position.get('trade_type', 'manual')
  539. # Position emoji and formatting
  540. if position_side == 'long':
  541. pos_emoji = "🟢"
  542. direction = "LONG"
  543. else: # Short position
  544. pos_emoji = "🔴"
  545. direction = "SHORT"
  546. # Trade type indicator
  547. type_indicator = ""
  548. if trade_type == 'external':
  549. type_indicator = " 🔄" # External/synced position
  550. elif trade_type == 'bot':
  551. type_indicator = " 🤖" # Bot-created position
  552. message_text += f"{pos_emoji} <b>{token} ({direction}){type_indicator}</b>\n"
  553. message_text += f" 📏 Size: {abs(current_amount):.6f} {token}\n"
  554. message_text += f" 💰 Entry: ${entry_price:.4f}\n"
  555. # Show stop loss if linked
  556. if position.get('stop_loss_price'):
  557. sl_price = position['stop_loss_price']
  558. sl_status = "Pending" if not position.get('stop_loss_order_id') else "Active"
  559. message_text += f" 🛑 Stop Loss: ${sl_price:.4f} ({sl_status})\n"
  560. # Show take profit if linked
  561. if position.get('take_profit_price'):
  562. tp_price = position['take_profit_price']
  563. tp_status = "Pending" if not position.get('take_profit_order_id') else "Active"
  564. message_text += f" 🎯 Take Profit: ${tp_price:.4f} ({tp_status})\n"
  565. message_text += f" 🆔 Lifecycle ID: {position['trade_lifecycle_id'][:8]}\n\n"
  566. # Add summary
  567. total_positions = len(open_positions)
  568. bot_positions = len([p for p in open_positions if p.get('trade_type') == 'bot'])
  569. external_positions = len([p for p in open_positions if p.get('trade_type') == 'external'])
  570. message_text += f"📈 <b>Summary:</b>\n"
  571. message_text += f" Total: {total_positions} | "
  572. message_text += f"Bot: {bot_positions} | "
  573. message_text += f"External: {external_positions}\n\n"
  574. message_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
  575. message_text += f"💡 Use /positions for detailed P&L information"
  576. await context.bot.send_message(chat_id=chat_id, text=message_text.strip(), parse_mode='HTML')
  577. except Exception as e:
  578. error_message = f"❌ Error processing active positions command: {str(e)}"
  579. await context.bot.send_message(chat_id=chat_id, text=error_message)
  580. logger.error(f"Error in active positions command: {e}")
  581. async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  582. """Handle the /market command."""
  583. chat_id = update.effective_chat.id
  584. if not self._is_authorized(chat_id):
  585. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  586. return
  587. # Get token from arguments or use default
  588. if context.args and len(context.args) > 0:
  589. token = _normalize_token_case(context.args[0])
  590. else:
  591. token = Config.DEFAULT_TRADING_TOKEN
  592. symbol = f"{token}/USDC:USDC"
  593. market_data = self.trading_engine.get_market_data(symbol)
  594. if market_data:
  595. ticker = market_data.get('ticker', {})
  596. current_price = float(ticker.get('last', 0.0) or 0.0)
  597. bid_price = float(ticker.get('bid', 0.0) or 0.0)
  598. ask_price = float(ticker.get('ask', 0.0) or 0.0)
  599. raw_base_volume = ticker.get('baseVolume')
  600. volume_24h = float(raw_base_volume if raw_base_volume is not None else 0.0)
  601. raw_change_24h = ticker.get('change')
  602. change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
  603. raw_percentage = ticker.get('percentage')
  604. change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
  605. high_24h = float(ticker.get('high', 0.0) or 0.0)
  606. low_24h = float(ticker.get('low', 0.0) or 0.0)
  607. # Market direction emoji
  608. trend_emoji = "🟢" if change_24h >= 0 else "🔴"
  609. # Format prices with proper precision for this token
  610. formatter = get_formatter()
  611. current_price_str = formatter.format_price_with_symbol(current_price, token)
  612. bid_price_str = formatter.format_price_with_symbol(bid_price, token)
  613. ask_price_str = formatter.format_price_with_symbol(ask_price, token)
  614. spread_str = formatter.format_price_with_symbol(ask_price - bid_price, token)
  615. high_24h_str = formatter.format_price_with_symbol(high_24h, token)
  616. low_24h_str = formatter.format_price_with_symbol(low_24h, token)
  617. change_24h_str = formatter.format_price_with_symbol(change_24h, token)
  618. market_text = f"""
  619. 📊 <b>{token} Market Data</b>
  620. 💰 <b>Price Information:</b>
  621. 💵 Current: {current_price_str}
  622. 🟢 Bid: {bid_price_str}
  623. 🔴 Ask: {ask_price_str}
  624. 📊 Spread: {spread_str}
  625. 📈 <b>24h Statistics:</b>
  626. {trend_emoji} Change: {change_24h_str} ({change_percent:+.2f}%)
  627. 🔝 High: {high_24h_str}
  628. 🔻 Low: {low_24h_str}
  629. 📊 Volume: {volume_24h:,.2f} {token}
  630. ⏰ <b>Last Updated:</b> {datetime.now().strftime('%H:%M:%S')}
  631. """
  632. await context.bot.send_message(chat_id=chat_id, text=market_text.strip(), parse_mode='HTML')
  633. else:
  634. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch market data for {token}")
  635. async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  636. """Handle the /price command."""
  637. chat_id = update.effective_chat.id
  638. if not self._is_authorized(chat_id):
  639. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  640. return
  641. # Get token from arguments or use default
  642. if context.args and len(context.args) > 0:
  643. token = _normalize_token_case(context.args[0])
  644. else:
  645. token = Config.DEFAULT_TRADING_TOKEN
  646. symbol = f"{token}/USDC:USDC"
  647. market_data = self.trading_engine.get_market_data(symbol)
  648. if market_data:
  649. ticker = market_data.get('ticker', {})
  650. current_price = float(ticker.get('last', 0.0) or 0.0)
  651. raw_change_24h = ticker.get('change')
  652. change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
  653. raw_percentage = ticker.get('percentage')
  654. change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
  655. # Price direction emoji
  656. trend_emoji = "🟢" if change_24h >= 0 else "🔴"
  657. # Format prices with proper precision for this token
  658. formatter = get_formatter()
  659. current_price_str = formatter.format_price_with_symbol(current_price, token)
  660. change_24h_str = formatter.format_price_with_symbol(change_24h, token)
  661. price_text = f"""
  662. 💵 <b>{token} Price</b>
  663. 💰 {current_price_str}
  664. {trend_emoji} {change_percent:+.2f}% ({change_24h_str})
  665. ⏰ {datetime.now().strftime('%H:%M:%S')}
  666. """
  667. await context.bot.send_message(chat_id=chat_id, text=price_text.strip(), parse_mode='HTML')
  668. else:
  669. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch price for {token}")
  670. async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  671. """Handle the /performance command to show token performance ranking or detailed stats."""
  672. chat_id = update.effective_chat.id
  673. if not self._is_authorized(chat_id):
  674. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  675. return
  676. try:
  677. # Check if specific token is requested
  678. if context.args and len(context.args) >= 1:
  679. # Detailed performance for specific token
  680. token = _normalize_token_case(context.args[0])
  681. await self._show_token_performance(chat_id, token, context)
  682. else:
  683. # Show token performance ranking
  684. await self._show_performance_ranking(chat_id, context)
  685. except Exception as e:
  686. error_message = f"❌ Error processing performance command: {str(e)}"
  687. await context.bot.send_message(chat_id=chat_id, text=error_message)
  688. logger.error(f"Error in performance command: {e}")
  689. async def _show_performance_ranking(self, chat_id: str, context: ContextTypes.DEFAULT_TYPE):
  690. """Show token performance ranking (compressed view)."""
  691. stats = self.trading_engine.get_stats()
  692. if not stats:
  693. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  694. return
  695. token_performance = stats.get_token_performance()
  696. if not token_performance:
  697. await context.bot.send_message(chat_id=chat_id, text=
  698. "📊 <b>Token Performance</b>\n\n"
  699. "📭 No trading data available yet.\n\n"
  700. "💡 Performance tracking starts after your first completed trades.\n"
  701. "Use /long or /short to start trading!",
  702. parse_mode='HTML'
  703. )
  704. return
  705. # Sort tokens by total P&L (best to worst)
  706. sorted_tokens = sorted(
  707. token_performance.items(),
  708. key=lambda x: x[1]['total_pnl'],
  709. reverse=True
  710. )
  711. performance_text = "🏆 <b>Token Performance Ranking</b>\n\n"
  712. # Add ranking with emojis
  713. for i, (token, stats_data) in enumerate(sorted_tokens, 1):
  714. # Ranking emoji
  715. if i == 1:
  716. rank_emoji = "🥇"
  717. elif i == 2:
  718. rank_emoji = "🥈"
  719. elif i == 3:
  720. rank_emoji = "🥉"
  721. else:
  722. rank_emoji = f"#{i}"
  723. # P&L emoji
  724. pnl_emoji = "🟢" if stats_data['total_pnl'] >= 0 else "🔴"
  725. # Format the line
  726. performance_text += f"{rank_emoji} <b>{token}</b>\n"
  727. performance_text += f" {pnl_emoji} P&L: ${stats_data['total_pnl']:,.2f} ({stats_data['pnl_percentage']:+.1f}%)\n"
  728. performance_text += f" 📊 Trades: {stats_data['completed_trades']}"
  729. # Add win rate if there are completed trades
  730. if stats_data['completed_trades'] > 0:
  731. performance_text += f" | Win: {stats_data['win_rate']:.0f}%"
  732. performance_text += "\n\n"
  733. # Add summary
  734. total_pnl = sum(stats_data['total_pnl'] for stats_data in token_performance.values())
  735. total_trades = sum(stats_data['completed_trades'] for stats_data in token_performance.values())
  736. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  737. performance_text += f"💼 <b>Portfolio Summary:</b>\n"
  738. performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  739. performance_text += f" 📈 Tokens Traded: {len(token_performance)}\n"
  740. performance_text += f" 🔄 Completed Trades: {total_trades}\n\n"
  741. performance_text += f"💡 <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
  742. await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
  743. async def _show_token_performance(self, chat_id: str, token: str, context: ContextTypes.DEFAULT_TYPE):
  744. """Show detailed performance for a specific token."""
  745. stats = self.trading_engine.get_stats()
  746. if not stats:
  747. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  748. return
  749. token_stats = stats.get_token_detailed_stats(token)
  750. # Check if token has any data
  751. if token_stats.get('total_trades', 0) == 0:
  752. await context.bot.send_message(chat_id=chat_id, text=
  753. f"📊 <b>{token} Performance</b>\n\n"
  754. f"📭 No trading history found for {token}.\n\n"
  755. f"💡 Start trading {token} with:\n"
  756. f"• <code>/long {token} 100</code>\n"
  757. f"• <code>/short {token} 100</code>\n\n"
  758. f"🔄 Use <code>/performance</code> to see all token rankings.",
  759. parse_mode='HTML'
  760. )
  761. return
  762. # Check if there's a message (no completed trades)
  763. if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
  764. await context.bot.send_message(chat_id=chat_id, text=
  765. f"📊 <b>{token} Performance</b>\n\n"
  766. f"{token_stats['message']}\n\n"
  767. f"📈 <b>Current Activity:</b>\n"
  768. f"• Total Trades: {token_stats['total_trades']}\n"
  769. f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n"
  770. f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n"
  771. f"• Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
  772. f"💡 Complete some trades to see P&L statistics!\n"
  773. f"🔄 Use <code>/performance</code> to see all token rankings.",
  774. parse_mode='HTML'
  775. )
  776. return
  777. # Detailed stats display
  778. pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "🔴"
  779. performance_text = f"""
  780. 📊 <b>{token} Detailed Performance</b>
  781. 💰 <b>P&L Summary:</b>
  782. • {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
  783. • 💵 Total Volume: ${token_stats['completed_volume']:,.2f}
  784. • 📈 Expectancy: ${token_stats['expectancy']:,.2f}
  785. 📊 <b>Trading Activity:</b>
  786. • Total Trades: {token_stats['total_trades']}
  787. • Completed: {token_stats['completed_trades']}
  788. • Buy Orders: {token_stats['buy_trades']}
  789. • Sell Orders: {token_stats['sell_trades']}
  790. 🏆 <b>Performance Metrics:</b>
  791. • Win Rate: {token_stats['win_rate']:.1f}%
  792. • Profit Factor: {token_stats['profit_factor']:.2f}
  793. • Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
  794. 💡 <b>Best/Worst:</b>
  795. • Largest Win: ${token_stats['largest_win']:,.2f}
  796. • Largest Loss: ${token_stats['largest_loss']:,.2f}
  797. • Avg Win: ${token_stats['avg_win']:,.2f}
  798. • Avg Loss: ${token_stats['avg_loss']:,.2f}
  799. """
  800. # Add recent trades if available
  801. if token_stats.get('recent_trades'):
  802. performance_text += f"\n🔄 <b>Recent Trades:</b>\n"
  803. for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
  804. trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  805. side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
  806. pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
  807. performance_text += f"• {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
  808. performance_text += f"\n🔄 Use <code>/performance</code> to see all token rankings"
  809. await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
  810. async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  811. """Handle the /daily command to show daily performance stats."""
  812. chat_id = update.effective_chat.id
  813. if not self._is_authorized(chat_id):
  814. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  815. return
  816. try:
  817. stats = self.trading_engine.get_stats()
  818. if not stats:
  819. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  820. return
  821. daily_stats = stats.get_daily_stats(10)
  822. if not daily_stats:
  823. await context.bot.send_message(chat_id=chat_id, text=
  824. "📅 <b>Daily Performance</b>\n\n"
  825. "📭 No daily performance data available yet.\n\n"
  826. "💡 Daily stats are calculated from completed trades.\n"
  827. "Start trading to see daily performance!",
  828. parse_mode='HTML'
  829. )
  830. return
  831. daily_text = "📅 <b>Daily Performance (Last 10 Days)</b>\n\n"
  832. total_pnl = 0
  833. total_trades = 0
  834. trading_days = 0
  835. for day_stats in daily_stats:
  836. if day_stats['has_trades']:
  837. # Day with completed trades
  838. pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
  839. daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
  840. daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
  841. daily_text += f" 🔄 Trades: {day_stats['trades']}\n\n"
  842. total_pnl += day_stats['pnl']
  843. total_trades += day_stats['trades']
  844. trading_days += 1
  845. else:
  846. # Day with no trades
  847. daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
  848. daily_text += f" 📭 No trading activity\n\n"
  849. # Add summary
  850. if trading_days > 0:
  851. avg_daily_pnl = total_pnl / trading_days
  852. avg_pnl_emoji = "🟢" if avg_daily_pnl >= 0 else "🔴"
  853. daily_text += f"📈 <b>Period Summary:</b>\n"
  854. daily_text += f" {avg_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  855. daily_text += f" 📊 Trading Days: {trading_days}/10\n"
  856. daily_text += f" 📈 Avg Daily P&L: ${avg_daily_pnl:,.2f}\n"
  857. daily_text += f" 🔄 Total Trades: {total_trades}\n"
  858. await context.bot.send_message(chat_id=chat_id, text=daily_text.strip(), parse_mode='HTML')
  859. except Exception as e:
  860. error_message = f"❌ Error processing daily command: {str(e)}"
  861. await context.bot.send_message(chat_id=chat_id, text=error_message)
  862. logger.error(f"Error in daily command: {e}")
  863. async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  864. """Handle the /weekly command to show weekly performance stats."""
  865. chat_id = update.effective_chat.id
  866. if not self._is_authorized(chat_id):
  867. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  868. return
  869. try:
  870. stats = self.trading_engine.get_stats()
  871. if not stats:
  872. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  873. return
  874. weekly_stats = stats.get_weekly_stats(10)
  875. if not weekly_stats:
  876. await context.bot.send_message(chat_id=chat_id, text=
  877. "📊 <b>Weekly Performance</b>\n\n"
  878. "📭 No weekly performance data available yet.\n\n"
  879. "💡 Weekly stats are calculated from completed trades.\n"
  880. "Start trading to see weekly performance!",
  881. parse_mode='HTML'
  882. )
  883. return
  884. weekly_text = "📊 <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
  885. total_pnl = 0
  886. total_trades = 0
  887. trading_weeks = 0
  888. for week_stats in weekly_stats:
  889. if week_stats['has_trades']:
  890. # Week with completed trades
  891. pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
  892. weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
  893. weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
  894. weekly_text += f" 🔄 Trades: {week_stats['trades']}\n\n"
  895. total_pnl += week_stats['pnl']
  896. total_trades += week_stats['trades']
  897. trading_weeks += 1
  898. else:
  899. # Week with no trades
  900. weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
  901. weekly_text += f" 📭 No completed trades\n\n"
  902. # Add summary
  903. if trading_weeks > 0:
  904. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  905. weekly_text += f"💼 <b>10-Week Summary:</b>\n"
  906. weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  907. weekly_text += f" 🔄 Total Trades: {total_trades}\n"
  908. weekly_text += f" 📈 Trading Weeks: {trading_weeks}/10\n"
  909. weekly_text += f" 📊 Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
  910. else:
  911. weekly_text += f"💼 <b>10-Week Summary:</b>\n"
  912. weekly_text += f" 📭 No completed trades in the last 10 weeks\n"
  913. weekly_text += f" 💡 Start trading to see weekly performance!"
  914. await context.bot.send_message(chat_id=chat_id, text=weekly_text.strip(), parse_mode='HTML')
  915. except Exception as e:
  916. error_message = f"❌ Error processing weekly command: {str(e)}"
  917. await context.bot.send_message(chat_id=chat_id, text=error_message)
  918. logger.error(f"Error in weekly command: {e}")
  919. async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  920. """Handle the /monthly command to show monthly performance stats."""
  921. chat_id = update.effective_chat.id
  922. if not self._is_authorized(chat_id):
  923. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  924. return
  925. try:
  926. stats = self.trading_engine.get_stats()
  927. if not stats:
  928. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  929. return
  930. monthly_stats = stats.get_monthly_stats(10)
  931. if not monthly_stats:
  932. await context.bot.send_message(chat_id=chat_id, text=
  933. "📆 <b>Monthly Performance</b>\n\n"
  934. "📭 No monthly performance data available yet.\n\n"
  935. "💡 Monthly stats are calculated from completed trades.\n"
  936. "Start trading to see monthly performance!",
  937. parse_mode='HTML'
  938. )
  939. return
  940. monthly_text = "📆 <b>Monthly Performance (Last 10 Months)</b>\n\n"
  941. total_pnl = 0
  942. total_trades = 0
  943. trading_months = 0
  944. for month_stats in monthly_stats:
  945. if month_stats['has_trades']:
  946. # Month with completed trades
  947. pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
  948. monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
  949. monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
  950. monthly_text += f" 🔄 Trades: {month_stats['trades']}\n\n"
  951. total_pnl += month_stats['pnl']
  952. total_trades += month_stats['trades']
  953. trading_months += 1
  954. else:
  955. # Month with no trades
  956. monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
  957. monthly_text += f" 📭 No completed trades\n\n"
  958. # Add summary
  959. if trading_months > 0:
  960. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  961. monthly_text += f"💼 <b>10-Month Summary:</b>\n"
  962. monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  963. monthly_text += f" 🔄 Total Trades: {total_trades}\n"
  964. monthly_text += f" 📈 Trading Months: {trading_months}/10\n"
  965. monthly_text += f" 📊 Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
  966. else:
  967. monthly_text += f"💼 <b>10-Month Summary:</b>\n"
  968. monthly_text += f" 📭 No completed trades in the last 10 months\n"
  969. monthly_text += f" 💡 Start trading to see monthly performance!"
  970. await context.bot.send_message(chat_id=chat_id, text=monthly_text.strip(), parse_mode='HTML')
  971. except Exception as e:
  972. error_message = f"❌ Error processing monthly command: {str(e)}"
  973. await context.bot.send_message(chat_id=chat_id, text=error_message)
  974. logger.error(f"Error in monthly command: {e}")
  975. async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  976. """Handle the /risk command to show advanced risk metrics."""
  977. chat_id = update.effective_chat.id
  978. if not self._is_authorized(chat_id):
  979. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  980. return
  981. try:
  982. # Get current balance for context
  983. balance = self.trading_engine.get_balance()
  984. current_balance = 0
  985. if balance and balance.get('total'):
  986. current_balance = float(balance['total'].get('USDC', 0))
  987. # Get risk metrics and basic stats
  988. stats = self.trading_engine.get_stats()
  989. if not stats:
  990. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  991. return
  992. risk_metrics = stats.get_risk_metrics()
  993. basic_stats = stats.get_basic_stats()
  994. # Check if we have enough data for risk calculations
  995. if basic_stats['completed_trades'] < 2:
  996. await context.bot.send_message(chat_id=chat_id, text=
  997. "📊 <b>Risk Analysis</b>\n\n"
  998. "📭 <b>Insufficient Data</b>\n\n"
  999. f"• Current completed trades: {basic_stats['completed_trades']}\n"
  1000. f"• Required for risk analysis: 2+ trades\n"
  1001. f"• Daily balance snapshots: {len(stats.data.get('daily_balances', []))}\n\n"
  1002. "💡 <b>To enable risk analysis:</b>\n"
  1003. "• Complete more trades to generate returns data\n"
  1004. "• Bot automatically records daily balance snapshots\n"
  1005. "• Risk metrics will be available after sufficient trading history\n\n"
  1006. "📈 Use /stats for current performance metrics",
  1007. parse_mode='HTML'
  1008. )
  1009. return
  1010. # Format the risk analysis message
  1011. risk_text = f"""
  1012. 📊 <b>Risk Analysis & Advanced Metrics</b>
  1013. 🎯 <b>Risk-Adjusted Performance:</b>
  1014. • Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
  1015. • Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
  1016. • Annual Volatility: {risk_metrics['volatility']:.2f}%
  1017. 📉 <b>Drawdown Analysis:</b>
  1018. • Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
  1019. • Value at Risk (95%): {risk_metrics['var_95']:.2f}%
  1020. 💰 <b>Portfolio Context:</b>
  1021. • Current Balance: ${current_balance:,.2f}
  1022. • Initial Balance: ${basic_stats['initial_balance']:,.2f}
  1023. • Total P&L: ${basic_stats['total_pnl']:,.2f}
  1024. • Days Active: {basic_stats['days_active']}
  1025. 📊 <b>Risk Interpretation:</b>
  1026. """
  1027. # Add interpretive guidance
  1028. sharpe = risk_metrics['sharpe_ratio']
  1029. if sharpe > 2.0:
  1030. risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
  1031. elif sharpe > 1.0:
  1032. risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
  1033. elif sharpe > 0.5:
  1034. risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
  1035. elif sharpe > 0:
  1036. risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
  1037. else:
  1038. risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
  1039. max_dd = risk_metrics['max_drawdown']
  1040. if max_dd < 5:
  1041. risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
  1042. elif max_dd < 15:
  1043. risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
  1044. elif max_dd < 30:
  1045. risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
  1046. else:
  1047. risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
  1048. volatility = risk_metrics['volatility']
  1049. if volatility < 10:
  1050. risk_text += "• 🟢 <b>Low</b> portfolio volatility (< 10%)\n"
  1051. elif volatility < 25:
  1052. risk_text += "• 🟡 <b>Moderate</b> portfolio volatility (< 25%)\n"
  1053. elif volatility < 50:
  1054. risk_text += "• 🟠 <b>High</b> portfolio volatility (< 50%)\n"
  1055. else:
  1056. risk_text += "• 🔴 <b>Very High</b> portfolio volatility (> 50%)\n"
  1057. risk_text += f"""
  1058. 💡 <b>Risk Definitions:</b>
  1059. • <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
  1060. • <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
  1061. • <b>Max Drawdown:</b> Largest peak-to-trough decline
  1062. • <b>VaR 95%:</b> Maximum expected loss 95% of the time
  1063. • <b>Volatility:</b> Annualized standard deviation of returns
  1064. 📈 <b>Data Based On:</b>
  1065. • Completed Trades: {basic_stats['completed_trades']}
  1066. • Daily Balance Records: {len(stats.data.get('daily_balances', []))}
  1067. • Trading Period: {basic_stats['days_active']} days
  1068. 🔄 Use /stats for trading performance metrics
  1069. """
  1070. await context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML')
  1071. except Exception as e:
  1072. error_message = f"❌ Error processing risk command: {str(e)}"
  1073. await context.bot.send_message(chat_id=chat_id, text=error_message)
  1074. logger.error(f"Error in risk command: {e}")
  1075. async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1076. """Handle the /balance_adjustments command to show deposit/withdrawal history."""
  1077. chat_id = update.effective_chat.id
  1078. if not self._is_authorized(chat_id):
  1079. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  1080. return
  1081. try:
  1082. stats = self.trading_engine.get_stats()
  1083. if not stats:
  1084. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  1085. return
  1086. # Get balance adjustments summary
  1087. adjustments_summary = stats.get_balance_adjustments_summary()
  1088. # Get detailed adjustments
  1089. all_adjustments = stats.data.get('balance_adjustments', [])
  1090. if not all_adjustments:
  1091. await context.bot.send_message(chat_id=chat_id, text=
  1092. "💰 <b>Balance Adjustments</b>\n\n"
  1093. "📭 No deposits or withdrawals detected yet.\n\n"
  1094. "💡 The bot automatically monitors for deposits and withdrawals\n"
  1095. "every hour to maintain accurate P&L calculations.",
  1096. parse_mode='HTML'
  1097. )
  1098. return
  1099. # Format the message
  1100. adjustments_text = f"""
  1101. 💰 <b>Balance Adjustments History</b>
  1102. 📊 <b>Summary:</b>
  1103. • Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
  1104. • Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
  1105. • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
  1106. • Total Transactions: {adjustments_summary['adjustment_count']}
  1107. 📅 <b>Recent Adjustments:</b>
  1108. """
  1109. # Show last 10 adjustments
  1110. recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
  1111. for adj in recent_adjustments:
  1112. try:
  1113. # Format timestamp
  1114. adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
  1115. # Format type and amount
  1116. if adj['type'] == 'deposit':
  1117. emoji = "💰"
  1118. amount_str = f"+${adj['amount']:,.2f}"
  1119. else: # withdrawal
  1120. emoji = "💸"
  1121. amount_str = f"-${abs(adj['amount']):,.2f}"
  1122. adjustments_text += f"• {emoji} {adj_time}: {amount_str}\n"
  1123. except Exception as adj_error:
  1124. logger.warning(f"Error formatting adjustment: {adj_error}")
  1125. continue
  1126. adjustments_text += f"""
  1127. 💡 <b>How it Works:</b>
  1128. • Bot checks for deposits/withdrawals every hour
  1129. • Adjustments maintain accurate P&L calculations
  1130. • Non-trading balance changes don't affect performance metrics
  1131. • Trading statistics remain pure and accurate
  1132. ⏰ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
  1133. """
  1134. await context.bot.send_message(chat_id=chat_id, text=adjustments_text.strip(), parse_mode='HTML')
  1135. except Exception as e:
  1136. error_message = f"❌ Error processing balance adjustments command: {str(e)}"
  1137. await context.bot.send_message(chat_id=chat_id, text=error_message)
  1138. logger.error(f"Error in balance_adjustments command: {e}")
  1139. async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1140. """Handle the /commands and /c command with quick action buttons."""
  1141. chat_id = update.effective_chat.id
  1142. if not self._is_authorized(chat_id):
  1143. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  1144. return
  1145. commands_text = """
  1146. 📱 <b>Quick Commands</b>
  1147. Tap any button below for instant access to bot functions:
  1148. 💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
  1149. """
  1150. from telegram import InlineKeyboardButton, InlineKeyboardMarkup
  1151. keyboard = [
  1152. [
  1153. InlineKeyboardButton("💰 Balance", callback_data="/balance"),
  1154. InlineKeyboardButton("📈 Positions", callback_data="/positions")
  1155. ],
  1156. [
  1157. InlineKeyboardButton("📋 Orders", callback_data="/orders"),
  1158. InlineKeyboardButton("📊 Stats", callback_data="/stats")
  1159. ],
  1160. [
  1161. InlineKeyboardButton("💵 Price", callback_data="/price"),
  1162. InlineKeyboardButton("📊 Market", callback_data="/market")
  1163. ],
  1164. [
  1165. InlineKeyboardButton("🏆 Performance", callback_data="/performance"),
  1166. InlineKeyboardButton("🔔 Alarms", callback_data="/alarm")
  1167. ],
  1168. [
  1169. InlineKeyboardButton("📅 Daily", callback_data="/daily"),
  1170. InlineKeyboardButton("📊 Weekly", callback_data="/weekly")
  1171. ],
  1172. [
  1173. InlineKeyboardButton("📆 Monthly", callback_data="/monthly"),
  1174. InlineKeyboardButton("🔄 Trades", callback_data="/trades")
  1175. ],
  1176. [
  1177. InlineKeyboardButton("🔄 Monitoring", callback_data="/monitoring"),
  1178. InlineKeyboardButton("📝 Logs", callback_data="/logs")
  1179. ],
  1180. [
  1181. InlineKeyboardButton("⚙️ Help", callback_data="/help")
  1182. ]
  1183. ]
  1184. reply_markup = InlineKeyboardMarkup(keyboard)
  1185. await context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup)
  1186. async def _estimate_entry_price_for_orphaned_position(self, symbol: str, contracts: float) -> float:
  1187. """Estimate entry price for an orphaned position by checking recent fills and market data."""
  1188. try:
  1189. # Method 1: Check recent fills from the exchange
  1190. recent_fills = self.trading_engine.get_recent_fills()
  1191. if recent_fills:
  1192. # Look for recent fills for this symbol
  1193. symbol_fills = [fill for fill in recent_fills if fill.get('symbol') == symbol]
  1194. if symbol_fills:
  1195. # Get the most recent fill as entry price estimate
  1196. latest_fill = symbol_fills[0] # Assuming sorted by newest first
  1197. fill_price = float(latest_fill.get('price', 0))
  1198. if fill_price > 0:
  1199. logger.info(f"💡 Found recent fill price for {symbol}: ${fill_price:.4f}")
  1200. return fill_price
  1201. # Method 2: Use current market price as fallback
  1202. market_data = self.trading_engine.get_market_data(symbol)
  1203. if market_data and market_data.get('ticker'):
  1204. current_price = float(market_data['ticker'].get('last', 0))
  1205. if current_price > 0:
  1206. logger.warning(f"⚠️ Using current market price as entry estimate for {symbol}: ${current_price:.4f}")
  1207. return current_price
  1208. # Method 3: Last resort - try bid/ask average
  1209. if market_data and market_data.get('ticker'):
  1210. bid = float(market_data['ticker'].get('bid', 0))
  1211. ask = float(market_data['ticker'].get('ask', 0))
  1212. if bid > 0 and ask > 0:
  1213. avg_price = (bid + ask) / 2
  1214. logger.warning(f"⚠️ Using bid/ask average as entry estimate for {symbol}: ${avg_price:.4f}")
  1215. return avg_price
  1216. # Method 4: Absolute fallback - return a small positive value to avoid 0
  1217. logger.error(f"❌ Could not estimate entry price for {symbol}, using fallback value of $1.00")
  1218. return 1.0
  1219. except Exception as e:
  1220. logger.error(f"❌ Error estimating entry price for {symbol}: {e}")
  1221. return 1.0 # Safe fallback