info_commands.py 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  1. #!/usr/bin/env python3
  2. """
  3. Info Commands - Handles information-related Telegram commands.
  4. """
  5. import logging
  6. from datetime import datetime
  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. logger = logging.getLogger(__name__)
  12. class InfoCommands:
  13. """Handles all information-related Telegram commands."""
  14. def __init__(self, trading_engine):
  15. """Initialize with trading engine."""
  16. self.trading_engine = trading_engine
  17. def _is_authorized(self, chat_id: str) -> bool:
  18. """Check if the chat ID is authorized."""
  19. return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
  20. async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  21. """Handle the /balance command."""
  22. chat_id = update.effective_chat.id
  23. if not self._is_authorized(chat_id):
  24. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  25. return
  26. balance = self.trading_engine.get_balance()
  27. if balance:
  28. balance_text = "💰 <b>Account Balance</b>\n\n"
  29. # Debug: Show raw balance structure (can be removed after debugging)
  30. logger.debug(f"Raw balance data: {balance}")
  31. # CCXT balance structure includes 'free', 'used', and 'total'
  32. total_balance = balance.get('total', {})
  33. free_balance = balance.get('free', {})
  34. used_balance = balance.get('used', {})
  35. # Get total portfolio value
  36. total_portfolio_value = 0
  37. # Show USDC balance prominently
  38. if 'USDC' in total_balance:
  39. usdc_total = float(total_balance['USDC'])
  40. usdc_free = float(free_balance.get('USDC', 0))
  41. usdc_used = float(used_balance.get('USDC', 0))
  42. balance_text += f"💵 <b>USDC:</b>\n"
  43. balance_text += f" 📊 Total: ${usdc_total:,.2f}\n"
  44. balance_text += f" ✅ Available: ${usdc_free:,.2f}\n"
  45. balance_text += f" 🔒 In Use: ${usdc_used:,.2f}\n\n"
  46. total_portfolio_value += usdc_total
  47. # Show other non-zero balances
  48. other_assets = []
  49. for asset, amount in total_balance.items():
  50. if asset != 'USDC' and float(amount) > 0:
  51. other_assets.append((asset, float(amount)))
  52. if other_assets:
  53. balance_text += "📊 <b>Other Assets:</b>\n"
  54. for asset, amount in other_assets:
  55. free_amount = float(free_balance.get(asset, 0))
  56. used_amount = float(used_balance.get(asset, 0))
  57. balance_text += f"💵 <b>{asset}:</b>\n"
  58. balance_text += f" 📊 Total: {amount:.6f}\n"
  59. balance_text += f" ✅ Available: {free_amount:.6f}\n"
  60. balance_text += f" 🔒 In Use: {used_amount:.6f}\n\n"
  61. # Portfolio summary
  62. usdc_balance = float(total_balance.get('USDC', 0))
  63. stats = self.trading_engine.get_stats()
  64. if stats:
  65. basic_stats = stats.get_basic_stats()
  66. initial_balance = basic_stats.get('initial_balance', usdc_balance)
  67. pnl = usdc_balance - initial_balance
  68. pnl_percent = (pnl / initial_balance * 100) if initial_balance > 0 else 0
  69. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  70. balance_text += f"💼 <b>Portfolio Summary:</b>\n"
  71. balance_text += f" 💰 Total Value: ${total_portfolio_value:,.2f}\n"
  72. balance_text += f" 🚀 Available for Trading: ${float(free_balance.get('USDC', 0)):,.2f}\n"
  73. balance_text += f" 🔒 In Active Use: ${float(used_balance.get('USDC', 0)):,.2f}\n\n"
  74. balance_text += f"📊 <b>Performance:</b>\n"
  75. balance_text += f" 💵 Initial: ${initial_balance:,.2f}\n"
  76. balance_text += f" {pnl_emoji} P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
  77. await context.bot.send_message(chat_id=chat_id, text=balance_text, parse_mode='HTML')
  78. else:
  79. await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch balance information")
  80. async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  81. """Handle the /positions command."""
  82. chat_id = update.effective_chat.id
  83. if not self._is_authorized(chat_id):
  84. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  85. return
  86. positions = self.trading_engine.get_positions()
  87. if positions is not None: # Successfully fetched (could be empty list)
  88. positions_text = "📈 <b>Open Positions</b>\n\n"
  89. # Filter for actual open positions
  90. open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
  91. if open_positions:
  92. total_unrealized = 0
  93. total_position_value = 0
  94. for position in open_positions:
  95. symbol = position.get('symbol', '').replace('/USDC:USDC', '')
  96. # Use the new position direction logic
  97. position_type, exit_side, contracts = self.trading_engine.get_position_direction(position)
  98. # Use correct CCXT field names
  99. entry_price = float(position.get('entryPrice', 0))
  100. mark_price = float(position.get('markPrice') or 0)
  101. unrealized_pnl = float(position.get('unrealizedPnl', 0))
  102. # If markPrice is not available, try to get current market price
  103. if mark_price == 0:
  104. try:
  105. market_data = self.trading_engine.get_market_data(position.get('symbol', ''))
  106. if market_data and market_data.get('ticker'):
  107. mark_price = float(market_data['ticker'].get('last', entry_price))
  108. except:
  109. mark_price = entry_price # Fallback to entry price
  110. # Calculate position value
  111. position_value = abs(contracts) * mark_price
  112. total_position_value += position_value
  113. total_unrealized += unrealized_pnl
  114. # Position emoji and formatting
  115. if position_type == "LONG":
  116. pos_emoji = "🟢"
  117. direction = "LONG"
  118. else:
  119. pos_emoji = "🔴"
  120. direction = "SHORT"
  121. pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
  122. pnl_percent = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
  123. positions_text += f"{pos_emoji} <b>{symbol} ({direction})</b>\n"
  124. positions_text += f" 📏 Size: {abs(contracts):.6f} {symbol}\n"
  125. positions_text += f" 💰 Entry: ${entry_price:,.2f}\n"
  126. positions_text += f" 📊 Mark: ${mark_price:,.2f}\n"
  127. positions_text += f" 💵 Value: ${position_value:,.2f}\n"
  128. positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percent:+.2f}%)\n\n"
  129. # Portfolio summary
  130. portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
  131. positions_text += f"💼 <b>Total Portfolio:</b>\n"
  132. positions_text += f" 💵 Total Value: ${total_position_value:,.2f}\n"
  133. positions_text += f" {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n"
  134. else:
  135. positions_text += "📭 No open positions\n\n"
  136. positions_text += "💡 Use /long or /short to open a position"
  137. await context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML')
  138. else:
  139. await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch positions")
  140. async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  141. """Handle the /orders command."""
  142. chat_id = update.effective_chat.id
  143. if not self._is_authorized(chat_id):
  144. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  145. return
  146. orders = self.trading_engine.get_orders()
  147. if orders is not None:
  148. if len(orders) > 0:
  149. orders_text = "📋 <b>Open Orders</b>\n\n"
  150. # Group orders by symbol
  151. orders_by_symbol = {}
  152. for order in orders:
  153. symbol = order.get('symbol', '').replace('/USDC:USDC', '')
  154. if symbol not in orders_by_symbol:
  155. orders_by_symbol[symbol] = []
  156. orders_by_symbol[symbol].append(order)
  157. for symbol, symbol_orders in orders_by_symbol.items():
  158. orders_text += f"📊 <b>{symbol}</b>\n"
  159. for order in symbol_orders:
  160. side = order.get('side', '').upper()
  161. amount = float(order.get('amount', 0))
  162. price = float(order.get('price', 0))
  163. order_type = order.get('type', 'unknown').title()
  164. order_id = order.get('id', 'N/A')
  165. # Order emoji
  166. side_emoji = "🟢" if side == "BUY" else "🔴"
  167. orders_text += f" {side_emoji} {side} {amount:.6f} @ ${price:,.2f}\n"
  168. orders_text += f" 📋 Type: {order_type} | ID: {order_id}\n"
  169. # Check for pending stop losses linked to this order
  170. stats = self.trading_engine.get_stats()
  171. if stats:
  172. # Try to find this order in our database to get its bot_order_ref_id
  173. order_in_db = stats.get_order_by_exchange_id(order_id)
  174. if order_in_db:
  175. bot_ref_id = order_in_db.get('bot_order_ref_id')
  176. if bot_ref_id:
  177. # Look for pending stop losses with this order as parent
  178. pending_sls = stats.get_orders_by_status('pending_trigger', 'stop_limit_trigger')
  179. linked_sls = [sl for sl in pending_sls if sl.get('parent_bot_order_ref_id') == bot_ref_id]
  180. if linked_sls:
  181. sl_order = linked_sls[0] # Should only be one
  182. sl_price = sl_order.get('price', 0)
  183. orders_text += f" 🛑 Pending SL: ${sl_price:,.2f} (activates when filled)\n"
  184. orders_text += "\n"
  185. orders_text += f"💼 <b>Total Orders:</b> {len(orders)}\n"
  186. orders_text += f"💡 Use /coo [token] to cancel orders"
  187. else:
  188. orders_text = "📋 <b>Open Orders</b>\n\n"
  189. orders_text += "📭 No open orders\n\n"
  190. orders_text += "💡 Use /long, /short, /sl, or /tp to create orders"
  191. await context.bot.send_message(chat_id=chat_id, text=orders_text, parse_mode='HTML')
  192. else:
  193. await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch orders")
  194. async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  195. """Handle the /stats command."""
  196. chat_id = update.effective_chat.id
  197. if not self._is_authorized(chat_id):
  198. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  199. return
  200. # Get current balance for stats
  201. balance = self.trading_engine.get_balance()
  202. current_balance = 0
  203. if balance and balance.get('total'):
  204. current_balance = float(balance['total'].get('USDC', 0))
  205. stats = self.trading_engine.get_stats()
  206. if stats:
  207. stats_message = stats.format_stats_message(current_balance)
  208. await context.bot.send_message(chat_id=chat_id, text=stats_message, parse_mode='HTML')
  209. else:
  210. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  211. async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  212. """Handle the /trades command."""
  213. chat_id = update.effective_chat.id
  214. if not self._is_authorized(chat_id):
  215. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  216. return
  217. stats = self.trading_engine.get_stats()
  218. if not stats:
  219. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  220. return
  221. recent_trades = stats.get_recent_trades(10)
  222. if not recent_trades:
  223. await context.bot.send_message(chat_id=chat_id, text="📝 No trades recorded yet.")
  224. return
  225. trades_text = "🔄 <b>Recent Trades</b>\n\n"
  226. for trade in reversed(recent_trades[-5:]): # Show last 5 trades
  227. timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  228. side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
  229. trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
  230. trades_text += f" 💰 ${trade['price']:,.2f} | 💵 ${trade['value']:,.2f}\n"
  231. trades_text += f" 📅 {timestamp}\n\n"
  232. await context.bot.send_message(chat_id=chat_id, text=trades_text, parse_mode='HTML')
  233. async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  234. """Handle the /market command."""
  235. chat_id = update.effective_chat.id
  236. if not self._is_authorized(chat_id):
  237. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  238. return
  239. # Get token from arguments or use default
  240. if context.args and len(context.args) > 0:
  241. token = context.args[0].upper()
  242. else:
  243. token = Config.DEFAULT_TRADING_TOKEN
  244. symbol = f"{token}/USDC:USDC"
  245. market_data = self.trading_engine.get_market_data(symbol)
  246. if market_data:
  247. ticker = market_data.get('ticker', {})
  248. current_price = float(ticker.get('last', 0.0) or 0.0)
  249. bid_price = float(ticker.get('bid', 0.0) or 0.0)
  250. ask_price = float(ticker.get('ask', 0.0) or 0.0)
  251. raw_base_volume = ticker.get('baseVolume')
  252. volume_24h = float(raw_base_volume if raw_base_volume is not None else 0.0)
  253. raw_change_24h = ticker.get('change')
  254. change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
  255. raw_percentage = ticker.get('percentage')
  256. change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
  257. high_24h = float(ticker.get('high', 0.0) or 0.0)
  258. low_24h = float(ticker.get('low', 0.0) or 0.0)
  259. # Market direction emoji
  260. trend_emoji = "🟢" if change_24h >= 0 else "🔴"
  261. market_text = f"""
  262. 📊 <b>{token} Market Data</b>
  263. 💰 <b>Price Information:</b>
  264. 💵 Current: ${current_price:,.2f}
  265. 🟢 Bid: ${bid_price:,.2f}
  266. 🔴 Ask: ${ask_price:,.2f}
  267. 📊 Spread: ${ask_price - bid_price:,.2f}
  268. 📈 <b>24h Statistics:</b>
  269. {trend_emoji} Change: ${change_24h:,.2f} ({change_percent:+.2f}%)
  270. 🔝 High: ${high_24h:,.2f}
  271. 🔻 Low: ${low_24h:,.2f}
  272. 📊 Volume: {volume_24h:,.2f} {token}
  273. ⏰ <b>Last Updated:</b> {datetime.now().strftime('%H:%M:%S')}
  274. """
  275. await context.bot.send_message(chat_id=chat_id, text=market_text.strip(), parse_mode='HTML')
  276. else:
  277. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch market data for {token}")
  278. async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  279. """Handle the /price command."""
  280. chat_id = update.effective_chat.id
  281. if not self._is_authorized(chat_id):
  282. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  283. return
  284. # Get token from arguments or use default
  285. if context.args and len(context.args) > 0:
  286. token = context.args[0].upper()
  287. else:
  288. token = Config.DEFAULT_TRADING_TOKEN
  289. symbol = f"{token}/USDC:USDC"
  290. market_data = self.trading_engine.get_market_data(symbol)
  291. if market_data:
  292. ticker = market_data.get('ticker', {})
  293. current_price = float(ticker.get('last', 0.0) or 0.0)
  294. raw_change_24h = ticker.get('change')
  295. change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
  296. raw_percentage = ticker.get('percentage')
  297. change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
  298. # Price direction emoji
  299. trend_emoji = "🟢" if change_24h >= 0 else "🔴"
  300. price_text = f"""
  301. 💵 <b>{token} Price</b>
  302. 💰 ${current_price:,.2f}
  303. {trend_emoji} {change_percent:+.2f}% (${change_24h:+.2f})
  304. ⏰ {datetime.now().strftime('%H:%M:%S')}
  305. """
  306. await context.bot.send_message(chat_id=chat_id, text=price_text.strip(), parse_mode='HTML')
  307. else:
  308. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch price for {token}")
  309. async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  310. """Handle the /performance command to show token performance ranking or detailed stats."""
  311. chat_id = update.effective_chat.id
  312. if not self._is_authorized(chat_id):
  313. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  314. return
  315. try:
  316. # Check if specific token is requested
  317. if context.args and len(context.args) >= 1:
  318. # Detailed performance for specific token
  319. token = context.args[0].upper()
  320. await self._show_token_performance(chat_id, token, context)
  321. else:
  322. # Show token performance ranking
  323. await self._show_performance_ranking(chat_id, context)
  324. except Exception as e:
  325. error_message = f"❌ Error processing performance command: {str(e)}"
  326. await context.bot.send_message(chat_id=chat_id, text=error_message)
  327. logger.error(f"Error in performance command: {e}")
  328. async def _show_performance_ranking(self, chat_id: str, context: ContextTypes.DEFAULT_TYPE):
  329. """Show token performance ranking (compressed view)."""
  330. stats = self.trading_engine.get_stats()
  331. if not stats:
  332. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  333. return
  334. token_performance = stats.get_token_performance()
  335. if not token_performance:
  336. await context.bot.send_message(chat_id=chat_id, text=
  337. "📊 <b>Token Performance</b>\n\n"
  338. "📭 No trading data available yet.\n\n"
  339. "💡 Performance tracking starts after your first completed trades.\n"
  340. "Use /long or /short to start trading!",
  341. parse_mode='HTML'
  342. )
  343. return
  344. # Sort tokens by total P&L (best to worst)
  345. sorted_tokens = sorted(
  346. token_performance.items(),
  347. key=lambda x: x[1]['total_pnl'],
  348. reverse=True
  349. )
  350. performance_text = "🏆 <b>Token Performance Ranking</b>\n\n"
  351. # Add ranking with emojis
  352. for i, (token, stats_data) in enumerate(sorted_tokens, 1):
  353. # Ranking emoji
  354. if i == 1:
  355. rank_emoji = "🥇"
  356. elif i == 2:
  357. rank_emoji = "🥈"
  358. elif i == 3:
  359. rank_emoji = "🥉"
  360. else:
  361. rank_emoji = f"#{i}"
  362. # P&L emoji
  363. pnl_emoji = "🟢" if stats_data['total_pnl'] >= 0 else "🔴"
  364. # Format the line
  365. performance_text += f"{rank_emoji} <b>{token}</b>\n"
  366. performance_text += f" {pnl_emoji} P&L: ${stats_data['total_pnl']:,.2f} ({stats_data['pnl_percentage']:+.1f}%)\n"
  367. performance_text += f" 📊 Trades: {stats_data['completed_trades']}"
  368. # Add win rate if there are completed trades
  369. if stats_data['completed_trades'] > 0:
  370. performance_text += f" | Win: {stats_data['win_rate']:.0f}%"
  371. performance_text += "\n\n"
  372. # Add summary
  373. total_pnl = sum(stats_data['total_pnl'] for stats_data in token_performance.values())
  374. total_trades = sum(stats_data['completed_trades'] for stats_data in token_performance.values())
  375. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  376. performance_text += f"💼 <b>Portfolio Summary:</b>\n"
  377. performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  378. performance_text += f" 📈 Tokens Traded: {len(token_performance)}\n"
  379. performance_text += f" 🔄 Completed Trades: {total_trades}\n\n"
  380. performance_text += f"💡 <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
  381. await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
  382. async def _show_token_performance(self, chat_id: str, token: str, context: ContextTypes.DEFAULT_TYPE):
  383. """Show detailed performance for a specific token."""
  384. stats = self.trading_engine.get_stats()
  385. if not stats:
  386. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  387. return
  388. token_stats = stats.get_token_detailed_stats(token)
  389. # Check if token has any data
  390. if token_stats.get('total_trades', 0) == 0:
  391. await context.bot.send_message(chat_id=chat_id, text=
  392. f"📊 <b>{token} Performance</b>\n\n"
  393. f"📭 No trading history found for {token}.\n\n"
  394. f"💡 Start trading {token} with:\n"
  395. f"• <code>/long {token} 100</code>\n"
  396. f"• <code>/short {token} 100</code>\n\n"
  397. f"🔄 Use <code>/performance</code> to see all token rankings.",
  398. parse_mode='HTML'
  399. )
  400. return
  401. # Check if there's a message (no completed trades)
  402. if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
  403. await context.bot.send_message(chat_id=chat_id, text=
  404. f"📊 <b>{token} Performance</b>\n\n"
  405. f"{token_stats['message']}\n\n"
  406. f"📈 <b>Current Activity:</b>\n"
  407. f"• Total Trades: {token_stats['total_trades']}\n"
  408. f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n"
  409. f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n"
  410. f"• Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
  411. f"💡 Complete some trades to see P&L statistics!\n"
  412. f"🔄 Use <code>/performance</code> to see all token rankings.",
  413. parse_mode='HTML'
  414. )
  415. return
  416. # Detailed stats display
  417. pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "🔴"
  418. performance_text = f"""
  419. 📊 <b>{token} Detailed Performance</b>
  420. 💰 <b>P&L Summary:</b>
  421. • {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
  422. • 💵 Total Volume: ${token_stats['completed_volume']:,.2f}
  423. • 📈 Expectancy: ${token_stats['expectancy']:,.2f}
  424. 📊 <b>Trading Activity:</b>
  425. • Total Trades: {token_stats['total_trades']}
  426. • Completed: {token_stats['completed_trades']}
  427. • Buy Orders: {token_stats['buy_trades']}
  428. • Sell Orders: {token_stats['sell_trades']}
  429. 🏆 <b>Performance Metrics:</b>
  430. • Win Rate: {token_stats['win_rate']:.1f}%
  431. • Profit Factor: {token_stats['profit_factor']:.2f}
  432. • Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
  433. 💡 <b>Best/Worst:</b>
  434. • Largest Win: ${token_stats['largest_win']:,.2f}
  435. • Largest Loss: ${token_stats['largest_loss']:,.2f}
  436. • Avg Win: ${token_stats['avg_win']:,.2f}
  437. • Avg Loss: ${token_stats['avg_loss']:,.2f}
  438. """
  439. # Add recent trades if available
  440. if token_stats.get('recent_trades'):
  441. performance_text += f"\n🔄 <b>Recent Trades:</b>\n"
  442. for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
  443. trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  444. side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
  445. pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
  446. performance_text += f"• {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
  447. performance_text += f"\n🔄 Use <code>/performance</code> to see all token rankings"
  448. await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
  449. async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  450. """Handle the /daily command to show daily performance stats."""
  451. chat_id = update.effective_chat.id
  452. if not self._is_authorized(chat_id):
  453. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  454. return
  455. try:
  456. stats = self.trading_engine.get_stats()
  457. if not stats:
  458. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  459. return
  460. daily_stats = stats.get_daily_stats(10)
  461. if not daily_stats:
  462. await context.bot.send_message(chat_id=chat_id, text=
  463. "📅 <b>Daily Performance</b>\n\n"
  464. "📭 No daily performance data available yet.\n\n"
  465. "💡 Daily stats are calculated from completed trades.\n"
  466. "Start trading to see daily performance!",
  467. parse_mode='HTML'
  468. )
  469. return
  470. daily_text = "📅 <b>Daily Performance (Last 10 Days)</b>\n\n"
  471. total_pnl = 0
  472. total_trades = 0
  473. trading_days = 0
  474. for day_stats in daily_stats:
  475. if day_stats['has_trades']:
  476. # Day with completed trades
  477. pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
  478. daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
  479. daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
  480. daily_text += f" 🔄 Trades: {day_stats['trades']}\n\n"
  481. total_pnl += day_stats['pnl']
  482. total_trades += day_stats['trades']
  483. trading_days += 1
  484. else:
  485. # Day with no trades
  486. daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
  487. daily_text += f" 📭 No trading activity\n\n"
  488. # Add summary
  489. if trading_days > 0:
  490. avg_daily_pnl = total_pnl / trading_days
  491. avg_pnl_emoji = "🟢" if avg_daily_pnl >= 0 else "🔴"
  492. daily_text += f"📈 <b>Period Summary:</b>\n"
  493. daily_text += f" {avg_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  494. daily_text += f" 📊 Trading Days: {trading_days}/10\n"
  495. daily_text += f" 📈 Avg Daily P&L: ${avg_daily_pnl:,.2f}\n"
  496. daily_text += f" 🔄 Total Trades: {total_trades}\n"
  497. await context.bot.send_message(chat_id=chat_id, text=daily_text.strip(), parse_mode='HTML')
  498. except Exception as e:
  499. error_message = f"❌ Error processing daily command: {str(e)}"
  500. await context.bot.send_message(chat_id=chat_id, text=error_message)
  501. logger.error(f"Error in daily command: {e}")
  502. async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  503. """Handle the /weekly command to show weekly performance stats."""
  504. chat_id = update.effective_chat.id
  505. if not self._is_authorized(chat_id):
  506. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  507. return
  508. try:
  509. stats = self.trading_engine.get_stats()
  510. if not stats:
  511. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  512. return
  513. weekly_stats = stats.get_weekly_stats(10)
  514. if not weekly_stats:
  515. await context.bot.send_message(chat_id=chat_id, text=
  516. "📊 <b>Weekly Performance</b>\n\n"
  517. "📭 No weekly performance data available yet.\n\n"
  518. "💡 Weekly stats are calculated from completed trades.\n"
  519. "Start trading to see weekly performance!",
  520. parse_mode='HTML'
  521. )
  522. return
  523. weekly_text = "📊 <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
  524. total_pnl = 0
  525. total_trades = 0
  526. trading_weeks = 0
  527. for week_stats in weekly_stats:
  528. if week_stats['has_trades']:
  529. # Week with completed trades
  530. pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
  531. weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
  532. weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
  533. weekly_text += f" 🔄 Trades: {week_stats['trades']}\n\n"
  534. total_pnl += week_stats['pnl']
  535. total_trades += week_stats['trades']
  536. trading_weeks += 1
  537. else:
  538. # Week with no trades
  539. weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
  540. weekly_text += f" 📭 No completed trades\n\n"
  541. # Add summary
  542. if trading_weeks > 0:
  543. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  544. weekly_text += f"💼 <b>10-Week Summary:</b>\n"
  545. weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  546. weekly_text += f" 🔄 Total Trades: {total_trades}\n"
  547. weekly_text += f" 📈 Trading Weeks: {trading_weeks}/10\n"
  548. weekly_text += f" 📊 Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
  549. else:
  550. weekly_text += f"💼 <b>10-Week Summary:</b>\n"
  551. weekly_text += f" 📭 No completed trades in the last 10 weeks\n"
  552. weekly_text += f" 💡 Start trading to see weekly performance!"
  553. await context.bot.send_message(chat_id=chat_id, text=weekly_text.strip(), parse_mode='HTML')
  554. except Exception as e:
  555. error_message = f"❌ Error processing weekly command: {str(e)}"
  556. await context.bot.send_message(chat_id=chat_id, text=error_message)
  557. logger.error(f"Error in weekly command: {e}")
  558. async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  559. """Handle the /monthly command to show monthly performance stats."""
  560. chat_id = update.effective_chat.id
  561. if not self._is_authorized(chat_id):
  562. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  563. return
  564. try:
  565. stats = self.trading_engine.get_stats()
  566. if not stats:
  567. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  568. return
  569. monthly_stats = stats.get_monthly_stats(10)
  570. if not monthly_stats:
  571. await context.bot.send_message(chat_id=chat_id, text=
  572. "📆 <b>Monthly Performance</b>\n\n"
  573. "📭 No monthly performance data available yet.\n\n"
  574. "💡 Monthly stats are calculated from completed trades.\n"
  575. "Start trading to see monthly performance!",
  576. parse_mode='HTML'
  577. )
  578. return
  579. monthly_text = "📆 <b>Monthly Performance (Last 10 Months)</b>\n\n"
  580. total_pnl = 0
  581. total_trades = 0
  582. trading_months = 0
  583. for month_stats in monthly_stats:
  584. if month_stats['has_trades']:
  585. # Month with completed trades
  586. pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
  587. monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
  588. monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
  589. monthly_text += f" 🔄 Trades: {month_stats['trades']}\n\n"
  590. total_pnl += month_stats['pnl']
  591. total_trades += month_stats['trades']
  592. trading_months += 1
  593. else:
  594. # Month with no trades
  595. monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
  596. monthly_text += f" 📭 No completed trades\n\n"
  597. # Add summary
  598. if trading_months > 0:
  599. total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  600. monthly_text += f"💼 <b>10-Month Summary:</b>\n"
  601. monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  602. monthly_text += f" 🔄 Total Trades: {total_trades}\n"
  603. monthly_text += f" 📈 Trading Months: {trading_months}/10\n"
  604. monthly_text += f" 📊 Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
  605. else:
  606. monthly_text += f"💼 <b>10-Month Summary:</b>\n"
  607. monthly_text += f" 📭 No completed trades in the last 10 months\n"
  608. monthly_text += f" 💡 Start trading to see monthly performance!"
  609. await context.bot.send_message(chat_id=chat_id, text=monthly_text.strip(), parse_mode='HTML')
  610. except Exception as e:
  611. error_message = f"❌ Error processing monthly command: {str(e)}"
  612. await context.bot.send_message(chat_id=chat_id, text=error_message)
  613. logger.error(f"Error in monthly command: {e}")
  614. async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  615. """Handle the /risk command to show advanced risk metrics."""
  616. chat_id = update.effective_chat.id
  617. if not self._is_authorized(chat_id):
  618. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  619. return
  620. try:
  621. # Get current balance for context
  622. balance = self.trading_engine.get_balance()
  623. current_balance = 0
  624. if balance and balance.get('total'):
  625. current_balance = float(balance['total'].get('USDC', 0))
  626. # Get risk metrics and basic stats
  627. stats = self.trading_engine.get_stats()
  628. if not stats:
  629. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  630. return
  631. risk_metrics = stats.get_risk_metrics()
  632. basic_stats = stats.get_basic_stats()
  633. # Check if we have enough data for risk calculations
  634. if basic_stats['completed_trades'] < 2:
  635. await context.bot.send_message(chat_id=chat_id, text=
  636. "📊 <b>Risk Analysis</b>\n\n"
  637. "📭 <b>Insufficient Data</b>\n\n"
  638. f"• Current completed trades: {basic_stats['completed_trades']}\n"
  639. f"• Required for risk analysis: 2+ trades\n"
  640. f"• Daily balance snapshots: {len(stats.data.get('daily_balances', []))}\n\n"
  641. "💡 <b>To enable risk analysis:</b>\n"
  642. "• Complete more trades to generate returns data\n"
  643. "• Bot automatically records daily balance snapshots\n"
  644. "• Risk metrics will be available after sufficient trading history\n\n"
  645. "📈 Use /stats for current performance metrics",
  646. parse_mode='HTML'
  647. )
  648. return
  649. # Format the risk analysis message
  650. risk_text = f"""
  651. 📊 <b>Risk Analysis & Advanced Metrics</b>
  652. 🎯 <b>Risk-Adjusted Performance:</b>
  653. • Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
  654. • Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
  655. • Annual Volatility: {risk_metrics['volatility']:.2f}%
  656. 📉 <b>Drawdown Analysis:</b>
  657. • Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
  658. • Value at Risk (95%): {risk_metrics['var_95']:.2f}%
  659. 💰 <b>Portfolio Context:</b>
  660. • Current Balance: ${current_balance:,.2f}
  661. • Initial Balance: ${basic_stats['initial_balance']:,.2f}
  662. • Total P&L: ${basic_stats['total_pnl']:,.2f}
  663. • Days Active: {basic_stats['days_active']}
  664. 📊 <b>Risk Interpretation:</b>
  665. """
  666. # Add interpretive guidance
  667. sharpe = risk_metrics['sharpe_ratio']
  668. if sharpe > 2.0:
  669. risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
  670. elif sharpe > 1.0:
  671. risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
  672. elif sharpe > 0.5:
  673. risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
  674. elif sharpe > 0:
  675. risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
  676. else:
  677. risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
  678. max_dd = risk_metrics['max_drawdown']
  679. if max_dd < 5:
  680. risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
  681. elif max_dd < 15:
  682. risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
  683. elif max_dd < 30:
  684. risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
  685. else:
  686. risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
  687. volatility = risk_metrics['volatility']
  688. if volatility < 10:
  689. risk_text += "• 🟢 <b>Low</b> portfolio volatility (< 10%)\n"
  690. elif volatility < 25:
  691. risk_text += "• 🟡 <b>Moderate</b> portfolio volatility (< 25%)\n"
  692. elif volatility < 50:
  693. risk_text += "• 🟠 <b>High</b> portfolio volatility (< 50%)\n"
  694. else:
  695. risk_text += "• 🔴 <b>Very High</b> portfolio volatility (> 50%)\n"
  696. risk_text += f"""
  697. 💡 <b>Risk Definitions:</b>
  698. • <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
  699. • <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
  700. • <b>Max Drawdown:</b> Largest peak-to-trough decline
  701. • <b>VaR 95%:</b> Maximum expected loss 95% of the time
  702. • <b>Volatility:</b> Annualized standard deviation of returns
  703. 📈 <b>Data Based On:</b>
  704. • Completed Trades: {basic_stats['completed_trades']}
  705. • Daily Balance Records: {len(stats.data.get('daily_balances', []))}
  706. • Trading Period: {basic_stats['days_active']} days
  707. 🔄 Use /stats for trading performance metrics
  708. """
  709. await context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML')
  710. except Exception as e:
  711. error_message = f"❌ Error processing risk command: {str(e)}"
  712. await context.bot.send_message(chat_id=chat_id, text=error_message)
  713. logger.error(f"Error in risk command: {e}")
  714. async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  715. """Handle the /balance_adjustments command to show deposit/withdrawal history."""
  716. chat_id = update.effective_chat.id
  717. if not self._is_authorized(chat_id):
  718. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  719. return
  720. try:
  721. stats = self.trading_engine.get_stats()
  722. if not stats:
  723. await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
  724. return
  725. # Get balance adjustments summary
  726. adjustments_summary = stats.get_balance_adjustments_summary()
  727. # Get detailed adjustments
  728. all_adjustments = stats.data.get('balance_adjustments', [])
  729. if not all_adjustments:
  730. await context.bot.send_message(chat_id=chat_id, text=
  731. "💰 <b>Balance Adjustments</b>\n\n"
  732. "📭 No deposits or withdrawals detected yet.\n\n"
  733. "💡 The bot automatically monitors for deposits and withdrawals\n"
  734. "every hour to maintain accurate P&L calculations.",
  735. parse_mode='HTML'
  736. )
  737. return
  738. # Format the message
  739. adjustments_text = f"""
  740. 💰 <b>Balance Adjustments History</b>
  741. 📊 <b>Summary:</b>
  742. • Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
  743. • Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
  744. • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
  745. • Total Transactions: {adjustments_summary['adjustment_count']}
  746. 📅 <b>Recent Adjustments:</b>
  747. """
  748. # Show last 10 adjustments
  749. recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
  750. for adj in recent_adjustments:
  751. try:
  752. # Format timestamp
  753. adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
  754. # Format type and amount
  755. if adj['type'] == 'deposit':
  756. emoji = "💰"
  757. amount_str = f"+${adj['amount']:,.2f}"
  758. else: # withdrawal
  759. emoji = "💸"
  760. amount_str = f"-${abs(adj['amount']):,.2f}"
  761. adjustments_text += f"• {emoji} {adj_time}: {amount_str}\n"
  762. except Exception as adj_error:
  763. logger.warning(f"Error formatting adjustment: {adj_error}")
  764. continue
  765. adjustments_text += f"""
  766. 💡 <b>How it Works:</b>
  767. • Bot checks for deposits/withdrawals every hour
  768. • Adjustments maintain accurate P&L calculations
  769. • Non-trading balance changes don't affect performance metrics
  770. • Trading statistics remain pure and accurate
  771. ⏰ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
  772. """
  773. await context.bot.send_message(chat_id=chat_id, text=adjustments_text.strip(), parse_mode='HTML')
  774. except Exception as e:
  775. error_message = f"❌ Error processing balance adjustments command: {str(e)}"
  776. await context.bot.send_message(chat_id=chat_id, text=error_message)
  777. logger.error(f"Error in balance_adjustments command: {e}")
  778. async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  779. """Handle the /commands and /c command with quick action buttons."""
  780. chat_id = update.effective_chat.id
  781. if not self._is_authorized(chat_id):
  782. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  783. return
  784. commands_text = """
  785. 📱 <b>Quick Commands</b>
  786. Tap any button below for instant access to bot functions:
  787. 💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
  788. """
  789. from telegram import InlineKeyboardButton, InlineKeyboardMarkup
  790. keyboard = [
  791. [
  792. InlineKeyboardButton("💰 Balance", callback_data="/balance"),
  793. InlineKeyboardButton("📈 Positions", callback_data="/positions")
  794. ],
  795. [
  796. InlineKeyboardButton("📋 Orders", callback_data="/orders"),
  797. InlineKeyboardButton("📊 Stats", callback_data="/stats")
  798. ],
  799. [
  800. InlineKeyboardButton("💵 Price", callback_data="/price"),
  801. InlineKeyboardButton("📊 Market", callback_data="/market")
  802. ],
  803. [
  804. InlineKeyboardButton("🏆 Performance", callback_data="/performance"),
  805. InlineKeyboardButton("🔔 Alarms", callback_data="/alarm")
  806. ],
  807. [
  808. InlineKeyboardButton("📅 Daily", callback_data="/daily"),
  809. InlineKeyboardButton("📊 Weekly", callback_data="/weekly")
  810. ],
  811. [
  812. InlineKeyboardButton("📆 Monthly", callback_data="/monthly"),
  813. InlineKeyboardButton("🔄 Trades", callback_data="/trades")
  814. ],
  815. [
  816. InlineKeyboardButton("🔄 Monitoring", callback_data="/monitoring"),
  817. InlineKeyboardButton("📝 Logs", callback_data="/logs")
  818. ],
  819. [
  820. InlineKeyboardButton("⚙️ Help", callback_data="/help")
  821. ]
  822. ]
  823. reply_markup = InlineKeyboardMarkup(keyboard)
  824. await context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup)