info_commands.py 44 KB

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