#!/usr/bin/env python3 """ Info Commands - Handles information-related Telegram commands. """ import logging from datetime import datetime from typing import Optional, Dict, Any, List from telegram import Update from telegram.ext import ContextTypes from src.config.config import Config logger = logging.getLogger(__name__) class InfoCommands: """Handles all information-related Telegram commands.""" def __init__(self, trading_engine): """Initialize with trading engine.""" self.trading_engine = trading_engine def _is_authorized(self, chat_id: str) -> bool: """Check if the chat ID is authorized.""" return str(chat_id) == str(Config.TELEGRAM_CHAT_ID) async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /balance command.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return balance = self.trading_engine.get_balance() if balance: balance_text = "šŸ’° Account Balance\n\n" # Debug: Show raw balance structure (can be removed after debugging) logger.debug(f"Raw balance data: {balance}") # CCXT balance structure includes 'free', 'used', and 'total' total_balance = balance.get('total', {}) free_balance = balance.get('free', {}) used_balance = balance.get('used', {}) # Get total portfolio value total_portfolio_value = 0 # Show USDC balance prominently if 'USDC' in total_balance: usdc_total = float(total_balance['USDC']) usdc_free = float(free_balance.get('USDC', 0)) usdc_used = float(used_balance.get('USDC', 0)) balance_text += f"šŸ’µ USDC:\n" balance_text += f" šŸ“Š Total: ${usdc_total:,.2f}\n" balance_text += f" āœ… Available: ${usdc_free:,.2f}\n" balance_text += f" šŸ”’ In Use: ${usdc_used:,.2f}\n\n" total_portfolio_value += usdc_total # Show other non-zero balances other_assets = [] for asset, amount in total_balance.items(): if asset != 'USDC' and float(amount) > 0: other_assets.append((asset, float(amount))) if other_assets: balance_text += "šŸ“Š Other Assets:\n" for asset, amount in other_assets: free_amount = float(free_balance.get(asset, 0)) used_amount = float(used_balance.get(asset, 0)) balance_text += f"šŸ’µ {asset}:\n" balance_text += f" šŸ“Š Total: {amount:.6f}\n" balance_text += f" āœ… Available: {free_amount:.6f}\n" balance_text += f" šŸ”’ In Use: {used_amount:.6f}\n\n" # Portfolio summary usdc_balance = float(total_balance.get('USDC', 0)) stats = self.trading_engine.get_stats() if stats: basic_stats = stats.get_basic_stats() initial_balance = basic_stats.get('initial_balance', usdc_balance) pnl = usdc_balance - initial_balance pnl_percent = (pnl / initial_balance * 100) if initial_balance > 0 else 0 pnl_emoji = "🟢" if pnl >= 0 else "šŸ”“" balance_text += f"šŸ’¼ Portfolio Summary:\n" balance_text += f" šŸ’° Total Value: ${total_portfolio_value:,.2f}\n" balance_text += f" šŸš€ Available for Trading: ${float(free_balance.get('USDC', 0)):,.2f}\n" balance_text += f" šŸ”’ In Active Use: ${float(used_balance.get('USDC', 0)):,.2f}\n\n" balance_text += f"šŸ“Š Performance:\n" balance_text += f" šŸ’µ Initial: ${initial_balance:,.2f}\n" balance_text += f" {pnl_emoji} P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n" await update.message.reply_text(balance_text, parse_mode='HTML') else: await update.message.reply_text("āŒ Could not fetch balance information") async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /positions command.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return positions = self.trading_engine.get_positions() if positions is not None: # Successfully fetched (could be empty list) positions_text = "šŸ“ˆ Open Positions\n\n" # Filter for actual open positions open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0] if open_positions: total_unrealized = 0 total_position_value = 0 for position in open_positions: symbol = position.get('symbol', '').replace('/USDC:USDC', '') # Use the new position direction logic position_type, exit_side, contracts = self.trading_engine.get_position_direction(position) # Use correct CCXT field names entry_price = float(position.get('entryPrice', 0)) mark_price = float(position.get('markPrice') or 0) unrealized_pnl = float(position.get('unrealizedPnl', 0)) # If markPrice is not available, try to get current market price if mark_price == 0: try: market_data = self.trading_engine.get_market_data(position.get('symbol', '')) if market_data and market_data.get('ticker'): mark_price = float(market_data['ticker'].get('last', entry_price)) except: mark_price = entry_price # Fallback to entry price # Calculate position value position_value = abs(contracts) * mark_price total_position_value += position_value total_unrealized += unrealized_pnl # Position emoji and formatting if position_type == "LONG": pos_emoji = "🟢" direction = "LONG" else: pos_emoji = "šŸ”“" direction = "SHORT" pnl_emoji = "🟢" if unrealized_pnl >= 0 else "šŸ”“" pnl_percent = (unrealized_pnl / position_value * 100) if position_value > 0 else 0 positions_text += f"{pos_emoji} {symbol} ({direction})\n" positions_text += f" šŸ“ Size: {abs(contracts):.6f} {symbol}\n" positions_text += f" šŸ’° Entry: ${entry_price:,.2f}\n" positions_text += f" šŸ“Š Mark: ${mark_price:,.2f}\n" positions_text += f" šŸ’µ Value: ${position_value:,.2f}\n" positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percent:+.2f}%)\n\n" # Portfolio summary portfolio_emoji = "🟢" if total_unrealized >= 0 else "šŸ”“" positions_text += f"šŸ’¼ Total Portfolio:\n" positions_text += f" šŸ’µ Total Value: ${total_position_value:,.2f}\n" positions_text += f" {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n" else: positions_text += "šŸ“­ No open positions\n\n" positions_text += "šŸ’” Use /long or /short to open a position" await update.message.reply_text(positions_text, parse_mode='HTML') else: await update.message.reply_text("āŒ Could not fetch positions") async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /orders command.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return orders = self.trading_engine.get_orders() if orders is not None: if len(orders) > 0: orders_text = "šŸ“‹ Open Orders\n\n" # Group orders by symbol orders_by_symbol = {} for order in orders: symbol = order.get('symbol', '').replace('/USDC:USDC', '') if symbol not in orders_by_symbol: orders_by_symbol[symbol] = [] orders_by_symbol[symbol].append(order) for symbol, symbol_orders in orders_by_symbol.items(): orders_text += f"šŸ“Š {symbol}\n" for order in symbol_orders: side = order.get('side', '').upper() amount = float(order.get('amount', 0)) price = float(order.get('price', 0)) order_type = order.get('type', 'unknown').title() order_id = order.get('id', 'N/A') # Order emoji side_emoji = "🟢" if side == "BUY" else "šŸ”“" orders_text += f" {side_emoji} {side} {amount:.6f} @ ${price:,.2f}\n" orders_text += f" šŸ“‹ Type: {order_type} | ID: {order_id}\n\n" orders_text += f"šŸ’¼ Total Orders: {len(orders)}\n" orders_text += f"šŸ’” Use /coo [token] to cancel orders" else: orders_text = "šŸ“‹ Open Orders\n\n" orders_text += "šŸ“­ No open orders\n\n" orders_text += "šŸ’” Use /long, /short, /sl, or /tp to create orders" await update.message.reply_text(orders_text, parse_mode='HTML') else: await update.message.reply_text("āŒ Could not fetch orders") async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /stats command.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return # Get current balance for stats balance = self.trading_engine.get_balance() current_balance = 0 if balance and balance.get('total'): current_balance = float(balance['total'].get('USDC', 0)) stats = self.trading_engine.get_stats() if stats: stats_message = stats.format_stats_message(current_balance) await update.message.reply_text(stats_message, parse_mode='HTML') else: await update.message.reply_text("āŒ Could not load trading statistics") async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /trades command.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return recent_trades = stats.get_recent_trades(10) if not recent_trades: await update.message.reply_text("šŸ“ No trades recorded yet.") return trades_text = "šŸ”„ Recent Trades\n\n" for trade in reversed(recent_trades[-5:]): # Show last 5 trades timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M') side_emoji = "🟢" if trade['side'] == 'buy' else "šŸ”“" trades_text += f"{side_emoji} {trade['side'].upper()} {trade['amount']} {trade['symbol']}\n" trades_text += f" šŸ’° ${trade['price']:,.2f} | šŸ’µ ${trade['value']:,.2f}\n" trades_text += f" šŸ“… {timestamp}\n\n" await update.message.reply_text(trades_text, parse_mode='HTML') async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /market command.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return # Get token from arguments or use default if context.args and len(context.args) > 0: token = context.args[0].upper() else: token = Config.DEFAULT_TRADING_TOKEN symbol = f"{token}/USDC:USDC" market_data = self.trading_engine.get_market_data(symbol) if market_data: ticker = market_data.get('ticker', {}) current_price = float(ticker.get('last', 0)) bid_price = float(ticker.get('bid', 0)) ask_price = float(ticker.get('ask', 0)) volume_24h = float(ticker.get('baseVolume', 0)) change_24h = float(ticker.get('change', 0)) change_percent = float(ticker.get('percentage', 0)) high_24h = float(ticker.get('high', 0)) low_24h = float(ticker.get('low', 0)) # Market direction emoji trend_emoji = "🟢" if change_24h >= 0 else "šŸ”“" market_text = f""" šŸ“Š {token} Market Data šŸ’° Price Information: šŸ’µ Current: ${current_price:,.2f} 🟢 Bid: ${bid_price:,.2f} šŸ”“ Ask: ${ask_price:,.2f} šŸ“Š Spread: ${ask_price - bid_price:,.2f} šŸ“ˆ 24h Statistics: {trend_emoji} Change: ${change_24h:,.2f} ({change_percent:+.2f}%) šŸ” High: ${high_24h:,.2f} šŸ”» Low: ${low_24h:,.2f} šŸ“Š Volume: {volume_24h:,.2f} {token} ā° Last Updated: {datetime.now().strftime('%H:%M:%S')} """ await update.message.reply_text(market_text.strip(), parse_mode='HTML') else: await update.message.reply_text(f"āŒ Could not fetch market data for {token}") async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /price command.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return # Get token from arguments or use default if context.args and len(context.args) > 0: token = context.args[0].upper() else: token = Config.DEFAULT_TRADING_TOKEN symbol = f"{token}/USDC:USDC" market_data = self.trading_engine.get_market_data(symbol) if market_data: ticker = market_data.get('ticker', {}) current_price = float(ticker.get('last', 0)) change_24h = float(ticker.get('change', 0)) change_percent = float(ticker.get('percentage', 0)) # Price direction emoji trend_emoji = "🟢" if change_24h >= 0 else "šŸ”“" price_text = f""" šŸ’µ {token} Price šŸ’° ${current_price:,.2f} {trend_emoji} {change_percent:+.2f}% (${change_24h:+.2f}) ā° {datetime.now().strftime('%H:%M:%S')} """ await update.message.reply_text(price_text.strip(), parse_mode='HTML') else: await update.message.reply_text(f"āŒ Could not fetch price for {token}") async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /performance command to show token performance ranking or detailed stats.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return try: # Check if specific token is requested if context.args and len(context.args) >= 1: # Detailed performance for specific token token = context.args[0].upper() await self._show_token_performance(update, token) else: # Show token performance ranking await self._show_performance_ranking(update) except Exception as e: error_message = f"āŒ Error processing performance command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in performance command: {e}") async def _show_performance_ranking(self, update: Update): """Show token performance ranking (compressed view).""" stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return token_performance = stats.get_token_performance() if not token_performance: await update.message.reply_text( "šŸ“Š Token Performance\n\n" "šŸ“­ No trading data available yet.\n\n" "šŸ’” Performance tracking starts after your first completed trades.\n" "Use /long or /short to start trading!", parse_mode='HTML' ) return # Sort tokens by total P&L (best to worst) sorted_tokens = sorted( token_performance.items(), key=lambda x: x[1]['total_pnl'], reverse=True ) performance_text = "šŸ† Token Performance Ranking\n\n" # Add ranking with emojis for i, (token, stats_data) in enumerate(sorted_tokens, 1): # Ranking emoji if i == 1: rank_emoji = "šŸ„‡" elif i == 2: rank_emoji = "🄈" elif i == 3: rank_emoji = "šŸ„‰" else: rank_emoji = f"#{i}" # P&L emoji pnl_emoji = "🟢" if stats_data['total_pnl'] >= 0 else "šŸ”“" # Format the line performance_text += f"{rank_emoji} {token}\n" performance_text += f" {pnl_emoji} P&L: ${stats_data['total_pnl']:,.2f} ({stats_data['pnl_percentage']:+.1f}%)\n" performance_text += f" šŸ“Š Trades: {stats_data['completed_trades']}" # Add win rate if there are completed trades if stats_data['completed_trades'] > 0: performance_text += f" | Win: {stats_data['win_rate']:.0f}%" performance_text += "\n\n" # Add summary total_pnl = sum(stats_data['total_pnl'] for stats_data in token_performance.values()) total_trades = sum(stats_data['completed_trades'] for stats_data in token_performance.values()) total_pnl_emoji = "🟢" if total_pnl >= 0 else "šŸ”“" performance_text += f"šŸ’¼ Portfolio Summary:\n" performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" performance_text += f" šŸ“ˆ Tokens Traded: {len(token_performance)}\n" performance_text += f" šŸ”„ Completed Trades: {total_trades}\n\n" performance_text += f"šŸ’” Usage: /performance BTC for detailed {Config.DEFAULT_TRADING_TOKEN} stats" await update.message.reply_text(performance_text.strip(), parse_mode='HTML') async def _show_token_performance(self, update: Update, token: str): """Show detailed performance for a specific token.""" stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return token_stats = stats.get_token_detailed_stats(token) # Check if token has any data if token_stats.get('total_trades', 0) == 0: await update.message.reply_text( f"šŸ“Š {token} Performance\n\n" f"šŸ“­ No trading history found for {token}.\n\n" f"šŸ’” Start trading {token} with:\n" f"• /long {token} 100\n" f"• /short {token} 100\n\n" f"šŸ”„ Use /performance to see all token rankings.", parse_mode='HTML' ) return # Check if there's a message (no completed trades) if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0: await update.message.reply_text( f"šŸ“Š {token} Performance\n\n" f"{token_stats['message']}\n\n" f"šŸ“ˆ Current Activity:\n" f"• Total Trades: {token_stats['total_trades']}\n" f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n" f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n" f"• Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n" f"šŸ’” Complete some trades to see P&L statistics!\n" f"šŸ”„ Use /performance to see all token rankings.", parse_mode='HTML' ) return # Detailed stats display pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "šŸ”“" performance_text = f""" šŸ“Š {token} Detailed Performance šŸ’° P&L Summary: • {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%) • šŸ’µ Total Volume: ${token_stats['completed_volume']:,.2f} • šŸ“ˆ Expectancy: ${token_stats['expectancy']:,.2f} šŸ“Š Trading Activity: • Total Trades: {token_stats['total_trades']} • Completed: {token_stats['completed_trades']} • Buy Orders: {token_stats['buy_trades']} • Sell Orders: {token_stats['sell_trades']} šŸ† Performance Metrics: • Win Rate: {token_stats['win_rate']:.1f}% • Profit Factor: {token_stats['profit_factor']:.2f} • Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']} šŸ’” Best/Worst: • Largest Win: ${token_stats['largest_win']:,.2f} • Largest Loss: ${token_stats['largest_loss']:,.2f} • Avg Win: ${token_stats['avg_win']:,.2f} • Avg Loss: ${token_stats['avg_loss']:,.2f} """ # Add recent trades if available if token_stats.get('recent_trades'): performance_text += f"\nšŸ”„ Recent Trades:\n" for trade in token_stats['recent_trades'][-3:]: # Last 3 trades trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M') side_emoji = "🟢" if trade['side'] == 'buy' else "šŸ”“" pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else "" performance_text += f"• {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n" performance_text += f"\nšŸ”„ Use /performance to see all token rankings" await update.message.reply_text(performance_text.strip(), parse_mode='HTML') async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /daily command to show daily performance stats.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return daily_stats = stats.get_daily_stats(10) if not daily_stats: await update.message.reply_text( "šŸ“… Daily Performance\n\n" "šŸ“­ No daily performance data available yet.\n\n" "šŸ’” Daily stats are calculated from completed trades.\n" "Start trading to see daily performance!", parse_mode='HTML' ) return daily_text = "šŸ“… Daily Performance (Last 10 Days)\n\n" total_pnl = 0 total_trades = 0 trading_days = 0 for day_stats in daily_stats: if day_stats['has_trades']: # Day with completed trades pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "šŸ”“" daily_text += f"šŸ“Š {day_stats['date_formatted']}\n" daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n" daily_text += f" šŸ”„ Trades: {day_stats['trades']}\n\n" total_pnl += day_stats['pnl'] total_trades += day_stats['trades'] trading_days += 1 else: # Day with no trades daily_text += f"šŸ“Š {day_stats['date_formatted']}\n" daily_text += f" šŸ“­ No trading activity\n\n" # Add summary if trading_days > 0: avg_daily_pnl = total_pnl / trading_days avg_pnl_emoji = "🟢" if avg_daily_pnl >= 0 else "šŸ”“" daily_text += f"šŸ“ˆ Period Summary:\n" daily_text += f" {avg_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" daily_text += f" šŸ“Š Trading Days: {trading_days}/10\n" daily_text += f" šŸ“ˆ Avg Daily P&L: ${avg_daily_pnl:,.2f}\n" daily_text += f" šŸ”„ Total Trades: {total_trades}\n" await update.message.reply_text(daily_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"āŒ Error processing daily command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in daily command: {e}") async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /weekly command to show weekly performance stats.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return weekly_stats = stats.get_weekly_stats(10) if not weekly_stats: await update.message.reply_text( "šŸ“Š Weekly Performance\n\n" "šŸ“­ No weekly performance data available yet.\n\n" "šŸ’” Weekly stats are calculated from completed trades.\n" "Start trading to see weekly performance!", parse_mode='HTML' ) return weekly_text = "šŸ“Š Weekly Performance (Last 10 Weeks)\n\n" total_pnl = 0 total_trades = 0 trading_weeks = 0 for week_stats in weekly_stats: if week_stats['has_trades']: # Week with completed trades pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "šŸ”“" weekly_text += f"šŸ“ˆ {week_stats['week_formatted']}\n" weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n" weekly_text += f" šŸ”„ Trades: {week_stats['trades']}\n\n" total_pnl += week_stats['pnl'] total_trades += week_stats['trades'] trading_weeks += 1 else: # Week with no trades weekly_text += f"šŸ“ˆ {week_stats['week_formatted']}\n" weekly_text += f" šŸ“­ No completed trades\n\n" # Add summary if trading_weeks > 0: total_pnl_emoji = "🟢" if total_pnl >= 0 else "šŸ”“" weekly_text += f"šŸ’¼ 10-Week Summary:\n" weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" weekly_text += f" šŸ”„ Total Trades: {total_trades}\n" weekly_text += f" šŸ“ˆ Trading Weeks: {trading_weeks}/10\n" weekly_text += f" šŸ“Š Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}" else: weekly_text += f"šŸ’¼ 10-Week Summary:\n" weekly_text += f" šŸ“­ No completed trades in the last 10 weeks\n" weekly_text += f" šŸ’” Start trading to see weekly performance!" await update.message.reply_text(weekly_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"āŒ Error processing weekly command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in weekly command: {e}") async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /monthly command to show monthly performance stats.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return monthly_stats = stats.get_monthly_stats(10) if not monthly_stats: await update.message.reply_text( "šŸ“† Monthly Performance\n\n" "šŸ“­ No monthly performance data available yet.\n\n" "šŸ’” Monthly stats are calculated from completed trades.\n" "Start trading to see monthly performance!", parse_mode='HTML' ) return monthly_text = "šŸ“† Monthly Performance (Last 10 Months)\n\n" total_pnl = 0 total_trades = 0 trading_months = 0 for month_stats in monthly_stats: if month_stats['has_trades']: # Month with completed trades pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "šŸ”“" monthly_text += f"šŸ“… {month_stats['month_formatted']}\n" monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n" monthly_text += f" šŸ”„ Trades: {month_stats['trades']}\n\n" total_pnl += month_stats['pnl'] total_trades += month_stats['trades'] trading_months += 1 else: # Month with no trades monthly_text += f"šŸ“… {month_stats['month_formatted']}\n" monthly_text += f" šŸ“­ No completed trades\n\n" # Add summary if trading_months > 0: total_pnl_emoji = "🟢" if total_pnl >= 0 else "šŸ”“" monthly_text += f"šŸ’¼ 10-Month Summary:\n" monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" monthly_text += f" šŸ”„ Total Trades: {total_trades}\n" monthly_text += f" šŸ“ˆ Trading Months: {trading_months}/10\n" monthly_text += f" šŸ“Š Avg per Trading Month: ${total_pnl/trading_months:,.2f}" else: monthly_text += f"šŸ’¼ 10-Month Summary:\n" monthly_text += f" šŸ“­ No completed trades in the last 10 months\n" monthly_text += f" šŸ’” Start trading to see monthly performance!" await update.message.reply_text(monthly_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"āŒ Error processing monthly command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in monthly command: {e}") async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /risk command to show advanced risk metrics.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return try: # Get current balance for context balance = self.trading_engine.get_balance() current_balance = 0 if balance and balance.get('total'): current_balance = float(balance['total'].get('USDC', 0)) # Get risk metrics and basic stats stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return risk_metrics = stats.get_risk_metrics() basic_stats = stats.get_basic_stats() # Check if we have enough data for risk calculations if basic_stats['completed_trades'] < 2: await update.message.reply_text( "šŸ“Š Risk Analysis\n\n" "šŸ“­ Insufficient Data\n\n" f"• Current completed trades: {basic_stats['completed_trades']}\n" f"• Required for risk analysis: 2+ trades\n" f"• Daily balance snapshots: {len(stats.data.get('daily_balances', []))}\n\n" "šŸ’” To enable risk analysis:\n" "• Complete more trades to generate returns data\n" "• Bot automatically records daily balance snapshots\n" "• Risk metrics will be available after sufficient trading history\n\n" "šŸ“ˆ Use /stats for current performance metrics", parse_mode='HTML' ) return # Format the risk analysis message risk_text = f""" šŸ“Š Risk Analysis & Advanced Metrics šŸŽÆ Risk-Adjusted Performance: • Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f} • Sortino Ratio: {risk_metrics['sortino_ratio']:.3f} • Annual Volatility: {risk_metrics['volatility']:.2f}% šŸ“‰ Drawdown Analysis: • Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}% • Value at Risk (95%): {risk_metrics['var_95']:.2f}% šŸ’° Portfolio Context: • Current Balance: ${current_balance:,.2f} • Initial Balance: ${basic_stats['initial_balance']:,.2f} • Total P&L: ${basic_stats['total_pnl']:,.2f} • Days Active: {basic_stats['days_active']} šŸ“Š Risk Interpretation: """ # Add interpretive guidance sharpe = risk_metrics['sharpe_ratio'] if sharpe > 2.0: risk_text += "• 🟢 Excellent risk-adjusted returns (Sharpe > 2.0)\n" elif sharpe > 1.0: risk_text += "• 🟔 Good risk-adjusted returns (Sharpe > 1.0)\n" elif sharpe > 0.5: risk_text += "• 🟠 Moderate risk-adjusted returns (Sharpe > 0.5)\n" elif sharpe > 0: risk_text += "• šŸ”“ Poor risk-adjusted returns (Sharpe > 0)\n" else: risk_text += "• ⚫ Negative risk-adjusted returns (Sharpe < 0)\n" max_dd = risk_metrics['max_drawdown'] if max_dd < 5: risk_text += "• 🟢 Low maximum drawdown (< 5%)\n" elif max_dd < 15: risk_text += "• 🟔 Moderate maximum drawdown (< 15%)\n" elif max_dd < 30: risk_text += "• 🟠 High maximum drawdown (< 30%)\n" else: risk_text += "• šŸ”“ Very High maximum drawdown (> 30%)\n" volatility = risk_metrics['volatility'] if volatility < 10: risk_text += "• 🟢 Low portfolio volatility (< 10%)\n" elif volatility < 25: risk_text += "• 🟔 Moderate portfolio volatility (< 25%)\n" elif volatility < 50: risk_text += "• 🟠 High portfolio volatility (< 50%)\n" else: risk_text += "• šŸ”“ Very High portfolio volatility (> 50%)\n" risk_text += f""" šŸ’” Risk Definitions: • Sharpe Ratio: Risk-adjusted return (excess return / volatility) • Sortino Ratio: Return / downside volatility (focuses on bad volatility) • Max Drawdown: Largest peak-to-trough decline • VaR 95%: Maximum expected loss 95% of the time • Volatility: Annualized standard deviation of returns šŸ“ˆ Data Based On: • Completed Trades: {basic_stats['completed_trades']} • Daily Balance Records: {len(stats.data.get('daily_balances', []))} • Trading Period: {basic_stats['days_active']} days šŸ”„ Use /stats for trading performance metrics """ await update.message.reply_text(risk_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"āŒ Error processing risk command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in risk command: {e}") async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /balance_adjustments command to show deposit/withdrawal history.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("āŒ Could not load trading statistics") return # Get balance adjustments summary adjustments_summary = stats.get_balance_adjustments_summary() # Get detailed adjustments all_adjustments = stats.data.get('balance_adjustments', []) if not all_adjustments: await update.message.reply_text( "šŸ’° Balance Adjustments\n\n" "šŸ“­ No deposits or withdrawals detected yet.\n\n" "šŸ’” The bot automatically monitors for deposits and withdrawals\n" "every hour to maintain accurate P&L calculations.", parse_mode='HTML' ) return # Format the message adjustments_text = f""" šŸ’° Balance Adjustments History šŸ“Š Summary: • Total Deposits: ${adjustments_summary['total_deposits']:,.2f} • Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f} • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f} • Total Transactions: {adjustments_summary['adjustment_count']} šŸ“… Recent Adjustments: """ # Show last 10 adjustments recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10] for adj in recent_adjustments: try: # Format timestamp adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M') # Format type and amount if adj['type'] == 'deposit': emoji = "šŸ’°" amount_str = f"+${adj['amount']:,.2f}" else: # withdrawal emoji = "šŸ’ø" amount_str = f"-${abs(adj['amount']):,.2f}" adjustments_text += f"• {emoji} {adj_time}: {amount_str}\n" except Exception as adj_error: logger.warning(f"Error formatting adjustment: {adj_error}") continue adjustments_text += f""" šŸ’” How it Works: • Bot checks for deposits/withdrawals every hour • Adjustments maintain accurate P&L calculations • Non-trading balance changes don't affect performance metrics • Trading statistics remain pure and accurate ā° Last Check: {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'} """ await update.message.reply_text(adjustments_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"āŒ Error processing balance adjustments command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in balance_adjustments command: {e}") async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /commands and /c command with quick action buttons.""" if not self._is_authorized(update.effective_chat.id): await update.message.reply_text("āŒ Unauthorized access.") return commands_text = """ šŸ“± Quick Commands Tap any button below for instant access to bot functions: šŸ’” Pro Tip: These buttons work the same as typing the commands manually, but faster! """ from telegram import InlineKeyboardButton, InlineKeyboardMarkup keyboard = [ [ InlineKeyboardButton("šŸ’° Balance", callback_data="balance"), InlineKeyboardButton("šŸ“ˆ Positions", callback_data="positions") ], [ InlineKeyboardButton("šŸ“‹ Orders", callback_data="orders"), InlineKeyboardButton("šŸ“Š Stats", callback_data="stats") ], [ InlineKeyboardButton("šŸ’µ Price", callback_data="price"), InlineKeyboardButton("šŸ“Š Market", callback_data="market") ], [ InlineKeyboardButton("šŸ† Performance", callback_data="performance"), InlineKeyboardButton("šŸ”” Alarms", callback_data="alarm") ], [ InlineKeyboardButton("šŸ“… Daily", callback_data="daily"), InlineKeyboardButton("šŸ“Š Weekly", callback_data="weekly") ], [ InlineKeyboardButton("šŸ“† Monthly", callback_data="monthly"), InlineKeyboardButton("šŸ”„ Trades", callback_data="trades") ], [ InlineKeyboardButton("šŸ”„ Monitoring", callback_data="monitoring"), InlineKeyboardButton("šŸ“ Logs", callback_data="logs") ], [ InlineKeyboardButton("āš™ļø Help", callback_data="help") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(commands_text, parse_mode='HTML', reply_markup=reply_markup)