#!/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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return balance = self.trading_engine.get_balance() if balance: balance_text = "š° <b>Account Balance</b>\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"šµ <b>USDC:</b>\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 += "š <b>Other Assets:</b>\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"šµ <b>{asset}:</b>\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"š¼ <b>Portfolio Summary:</b>\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"š <b>Performance:</b>\n" balance_text += f" šµ Initial: ${initial_balance:,.2f}\n" balance_text += f" {pnl_emoji} P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n" await context.bot.send_message(chat_id=chat_id, text=balance_text, parse_mode='HTML') else: await context.bot.send_message(chat_id=chat_id, text="ā Could not fetch balance information") async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /positions command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return positions = self.trading_engine.get_positions() if positions is not None: # Successfully fetched (could be empty list) positions_text = "š <b>Open Positions</b>\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} <b>{symbol} ({direction})</b>\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"š¼ <b>Total Portfolio:</b>\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 context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML') else: await context.bot.send_message(chat_id=chat_id, text="ā Could not fetch positions") async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /orders command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return orders = self.trading_engine.get_orders() if orders is not None: if len(orders) > 0: orders_text = "š <b>Open Orders</b>\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"š <b>{symbol}</b>\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" # Check for pending stop losses linked to this order stats = self.trading_engine.get_stats() if stats: # Try to find this order in our database to get its bot_order_ref_id order_in_db = stats.get_order_by_exchange_id(order_id) if order_in_db: bot_ref_id = order_in_db.get('bot_order_ref_id') if bot_ref_id: # Look for pending stop losses with this order as parent pending_sls = stats.get_orders_by_status('pending_trigger', 'stop_limit_trigger') linked_sls = [sl for sl in pending_sls if sl.get('parent_bot_order_ref_id') == bot_ref_id] if linked_sls: sl_order = linked_sls[0] # Should only be one sl_price = sl_order.get('price', 0) orders_text += f" š Pending SL: ${sl_price:,.2f} (activates when filled)\n" orders_text += "\n" orders_text += f"š¼ <b>Total Orders:</b> {len(orders)}\n" orders_text += f"š” Use /coo [token] to cancel orders" else: orders_text = "š <b>Open Orders</b>\n\n" orders_text += "š No open orders\n\n" orders_text += "š” Use /long, /short, /sl, or /tp to create orders" await context.bot.send_message(chat_id=chat_id, text=orders_text, parse_mode='HTML') else: await context.bot.send_message(chat_id=chat_id, text="ā Could not fetch orders") async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /stats command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, 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 context.bot.send_message(chat_id=chat_id, text=stats_message, parse_mode='HTML') else: await context.bot.send_message(chat_id=chat_id, text="ā Could not load trading statistics") async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /trades command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="ā Could not load trading statistics") return recent_trades = stats.get_recent_trades(10) if not recent_trades: await context.bot.send_message(chat_id=chat_id, text="š No trades recorded yet.") return trades_text = "š <b>Recent Trades</b>\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} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n" trades_text += f" š° ${trade['price']:,.2f} | šµ ${trade['value']:,.2f}\n" trades_text += f" š {timestamp}\n\n" await context.bot.send_message(chat_id=chat_id, text=trades_text, parse_mode='HTML') async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /market command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, 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.0) or 0.0) bid_price = float(ticker.get('bid', 0.0) or 0.0) ask_price = float(ticker.get('ask', 0.0) or 0.0) raw_base_volume = ticker.get('baseVolume') volume_24h = float(raw_base_volume if raw_base_volume is not None else 0.0) raw_change_24h = ticker.get('change') change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0) raw_percentage = ticker.get('percentage') change_percent = float(raw_percentage if raw_percentage is not None else 0.0) high_24h = float(ticker.get('high', 0.0) or 0.0) low_24h = float(ticker.get('low', 0.0) or 0.0) # Market direction emoji trend_emoji = "š¢" if change_24h >= 0 else "š“" market_text = f""" š <b>{token} Market Data</b> š° <b>Price Information:</b> šµ Current: ${current_price:,.2f} š¢ Bid: ${bid_price:,.2f} š“ Ask: ${ask_price:,.2f} š Spread: ${ask_price - bid_price:,.2f} š <b>24h Statistics:</b> {trend_emoji} Change: ${change_24h:,.2f} ({change_percent:+.2f}%) š High: ${high_24h:,.2f} š» Low: ${low_24h:,.2f} š Volume: {volume_24h:,.2f} {token} ā° <b>Last Updated:</b> {datetime.now().strftime('%H:%M:%S')} """ await context.bot.send_message(chat_id=chat_id, text=market_text.strip(), parse_mode='HTML') else: await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, 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.0) or 0.0) raw_change_24h = ticker.get('change') change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0) raw_percentage = ticker.get('percentage') change_percent = float(raw_percentage if raw_percentage is not None else 0.0) # Price direction emoji trend_emoji = "š¢" if change_24h >= 0 else "š“" price_text = f""" šµ <b>{token} Price</b> š° ${current_price:,.2f} {trend_emoji} {change_percent:+.2f}% (${change_24h:+.2f}) ā° {datetime.now().strftime('%H:%M:%S')} """ await context.bot.send_message(chat_id=chat_id, text=price_text.strip(), parse_mode='HTML') else: await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, 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(chat_id, token, context) else: # Show token performance ranking await self._show_performance_ranking(chat_id, context) except Exception as e: error_message = f"ā Error processing performance command: {str(e)}" await context.bot.send_message(chat_id=chat_id, text=error_message) logger.error(f"Error in performance command: {e}") async def _show_performance_ranking(self, chat_id: str, context: ContextTypes.DEFAULT_TYPE): """Show token performance ranking (compressed view).""" stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="ā Could not load trading statistics") return token_performance = stats.get_token_performance() if not token_performance: await context.bot.send_message(chat_id=chat_id, text= "š <b>Token Performance</b>\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 = "š <b>Token Performance Ranking</b>\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} <b>{token}</b>\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"š¼ <b>Portfolio Summary:</b>\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"š” <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats" await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML') async def _show_token_performance(self, chat_id: str, token: str, context: ContextTypes.DEFAULT_TYPE): """Show detailed performance for a specific token.""" stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, 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 context.bot.send_message(chat_id=chat_id, text= f"š <b>{token} Performance</b>\n\n" f"š No trading history found for {token}.\n\n" f"š” Start trading {token} with:\n" f"⢠<code>/long {token} 100</code>\n" f"⢠<code>/short {token} 100</code>\n\n" f"š Use <code>/performance</code> 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 context.bot.send_message(chat_id=chat_id, text= f"š <b>{token} Performance</b>\n\n" f"{token_stats['message']}\n\n" f"š <b>Current Activity:</b>\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 <code>/performance</code> to see all token rankings.", parse_mode='HTML' ) return # Detailed stats display pnl_emoji = "š¢" if token_stats['total_pnl'] >= 0 else "š“" performance_text = f""" š <b>{token} Detailed Performance</b> š° <b>P&L Summary:</b> ⢠{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} š <b>Trading Activity:</b> ⢠Total Trades: {token_stats['total_trades']} ⢠Completed: {token_stats['completed_trades']} ⢠Buy Orders: {token_stats['buy_trades']} ⢠Sell Orders: {token_stats['sell_trades']} š <b>Performance Metrics:</b> ⢠Win Rate: {token_stats['win_rate']:.1f}% ⢠Profit Factor: {token_stats['profit_factor']:.2f} ⢠Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']} š” <b>Best/Worst:</b> ⢠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š <b>Recent Trades:</b>\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 <code>/performance</code> to see all token rankings" await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="ā Could not load trading statistics") return daily_stats = stats.get_daily_stats(10) if not daily_stats: await context.bot.send_message(chat_id=chat_id, text= "š <b>Daily Performance</b>\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 = "š <b>Daily Performance (Last 10 Days)</b>\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"š <b>{day_stats['date_formatted']}</b>\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"š <b>{day_stats['date_formatted']}</b>\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"š <b>Period Summary:</b>\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 context.bot.send_message(chat_id=chat_id, text=daily_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"ā Error processing daily command: {str(e)}" await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="ā Could not load trading statistics") return weekly_stats = stats.get_weekly_stats(10) if not weekly_stats: await context.bot.send_message(chat_id=chat_id, text= "š <b>Weekly Performance</b>\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 = "š <b>Weekly Performance (Last 10 Weeks)</b>\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"š <b>{week_stats['week_formatted']}</b>\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"š <b>{week_stats['week_formatted']}</b>\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"š¼ <b>10-Week Summary:</b>\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"š¼ <b>10-Week Summary:</b>\n" weekly_text += f" š No completed trades in the last 10 weeks\n" weekly_text += f" š” Start trading to see weekly performance!" await context.bot.send_message(chat_id=chat_id, text=weekly_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"ā Error processing weekly command: {str(e)}" await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="ā Could not load trading statistics") return monthly_stats = stats.get_monthly_stats(10) if not monthly_stats: await context.bot.send_message(chat_id=chat_id, text= "š <b>Monthly Performance</b>\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 = "š <b>Monthly Performance (Last 10 Months)</b>\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"š <b>{month_stats['month_formatted']}</b>\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"š <b>{month_stats['month_formatted']}</b>\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"š¼ <b>10-Month Summary:</b>\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"š¼ <b>10-Month Summary:</b>\n" monthly_text += f" š No completed trades in the last 10 months\n" monthly_text += f" š” Start trading to see monthly performance!" await context.bot.send_message(chat_id=chat_id, text=monthly_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"ā Error processing monthly command: {str(e)}" await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, 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 context.bot.send_message(chat_id=chat_id, 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 context.bot.send_message(chat_id=chat_id, text= "š <b>Risk Analysis</b>\n\n" "š <b>Insufficient Data</b>\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" "š” <b>To enable risk analysis:</b>\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""" š <b>Risk Analysis & Advanced Metrics</b> šÆ <b>Risk-Adjusted Performance:</b> ⢠Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f} ⢠Sortino Ratio: {risk_metrics['sortino_ratio']:.3f} ⢠Annual Volatility: {risk_metrics['volatility']:.2f}% š <b>Drawdown Analysis:</b> ⢠Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}% ⢠Value at Risk (95%): {risk_metrics['var_95']:.2f}% š° <b>Portfolio Context:</b> ⢠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']} š <b>Risk Interpretation:</b> """ # Add interpretive guidance sharpe = risk_metrics['sharpe_ratio'] if sharpe > 2.0: risk_text += "⢠š¢ <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n" elif sharpe > 1.0: risk_text += "⢠š” <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n" elif sharpe > 0.5: risk_text += "⢠š <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n" elif sharpe > 0: risk_text += "⢠š“ <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n" else: risk_text += "⢠⫠<b>Negative</b> risk-adjusted returns (Sharpe < 0)\n" max_dd = risk_metrics['max_drawdown'] if max_dd < 5: risk_text += "⢠š¢ <b>Low</b> maximum drawdown (< 5%)\n" elif max_dd < 15: risk_text += "⢠š” <b>Moderate</b> maximum drawdown (< 15%)\n" elif max_dd < 30: risk_text += "⢠š <b>High</b> maximum drawdown (< 30%)\n" else: risk_text += "⢠š“ <b>Very High</b> maximum drawdown (> 30%)\n" volatility = risk_metrics['volatility'] if volatility < 10: risk_text += "⢠š¢ <b>Low</b> portfolio volatility (< 10%)\n" elif volatility < 25: risk_text += "⢠š” <b>Moderate</b> portfolio volatility (< 25%)\n" elif volatility < 50: risk_text += "⢠š <b>High</b> portfolio volatility (< 50%)\n" else: risk_text += "⢠š“ <b>Very High</b> portfolio volatility (> 50%)\n" risk_text += f""" š” <b>Risk Definitions:</b> ⢠<b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility) ⢠<b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility) ⢠<b>Max Drawdown:</b> Largest peak-to-trough decline ⢠<b>VaR 95%:</b> Maximum expected loss 95% of the time ⢠<b>Volatility:</b> Annualized standard deviation of returns š <b>Data Based On:</b> ⢠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 context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"ā Error processing risk command: {str(e)}" await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return try: stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, 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 context.bot.send_message(chat_id=chat_id, text= "š° <b>Balance Adjustments</b>\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""" š° <b>Balance Adjustments History</b> š <b>Summary:</b> ⢠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']} š <b>Recent Adjustments:</b> """ # 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""" š” <b>How it Works:</b> ⢠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 ā° <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'} """ await context.bot.send_message(chat_id=chat_id, text=adjustments_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"ā Error processing balance adjustments command: {str(e)}" await context.bot.send_message(chat_id=chat_id, 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.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="ā Unauthorized access.") return commands_text = """ š± <b>Quick Commands</b> Tap any button below for instant access to bot functions: š” <b>Pro Tip:</b> 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 context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup)