#!/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 from src.utils.price_formatter import format_price_with_symbol, get_formatter logger = logging.getLogger(__name__) class InfoCommands: """Handles all information-related Telegram commands.""" def __init__(self, trading_engine, notification_manager=None): """Initialize with trading engine and notification manager.""" self.trading_engine = trading_engine self.notification_manager = notification_manager 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 = "๐Ÿ’ฐ 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 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 # ๐Ÿงน PHASE 4: Use unified trades table as the single source of truth stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="โŒ Trading statistics not available.") return # ๐Ÿ†• AUTO-SYNC: Check for positions on exchange that don't have trade lifecycle records # Use cached data from MarketMonitor if available (updated every heartbeat) if (hasattr(self.trading_engine, 'market_monitor') and self.trading_engine.market_monitor and hasattr(self.trading_engine.market_monitor, 'get_cached_positions')): cache_age = self.trading_engine.market_monitor.get_cache_age_seconds() if cache_age < 60: # Use cached data if less than 1 minute old exchange_positions = self.trading_engine.market_monitor.get_cached_positions() or [] logger.debug(f"Using cached positions for auto-sync (age: {cache_age:.1f}s)") else: exchange_positions = self.trading_engine.get_positions() or [] logger.debug("Using fresh API call for auto-sync (cache too old)") else: exchange_positions = self.trading_engine.get_positions() or [] logger.debug("Using fresh API call for auto-sync (no cache available)") synced_positions = [] for exchange_pos in exchange_positions: symbol = exchange_pos.get('symbol') contracts = float(exchange_pos.get('contracts', 0)) if symbol and abs(contracts) > 0: # Check if we have a trade lifecycle record for this position existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened') if not existing_trade: # ๐Ÿšจ ORPHANED POSITION: Auto-create trade lifecycle record using exchange data entry_price = float(exchange_pos.get('entryPrice', 0)) position_side = 'long' if contracts > 0 else 'short' order_side = 'buy' if contracts > 0 else 'sell' # โœ… Use exchange data - no need to estimate! if entry_price > 0: logger.info(f"๐Ÿ”„ Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (exchange data)") else: # Fallback only if exchange truly doesn't provide entry price entry_price = await self._estimate_entry_price_for_orphaned_position(symbol, contracts) logger.warning(f"๐Ÿ”„ Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (estimated)") # Create trade lifecycle for external position lifecycle_id = stats.create_trade_lifecycle( symbol=symbol, side=order_side, entry_order_id=f"external_sync_{int(datetime.now().timestamp())}", trade_type='external' ) if lifecycle_id: # Update to position_opened status success = stats.update_trade_position_opened( lifecycle_id=lifecycle_id, entry_price=entry_price, entry_amount=abs(contracts), exchange_fill_id=f"external_fill_{int(datetime.now().timestamp())}" ) if success: synced_positions.append(symbol) logger.info(f"โœ… Successfully synced orphaned position for {symbol}") # ๐Ÿ†• Send immediate notification for auto-synced position token = symbol.split('/')[0] if '/' in symbol else symbol unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0)) position_value = float(exchange_pos.get('notional', 0)) liquidation_price = float(exchange_pos.get('liquidationPrice', 0)) leverage = float(exchange_pos.get('leverage', 1)) pnl_percentage = float(exchange_pos.get('percentage', 0)) pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด" notification_text = ( f"๐Ÿ”„ Position Auto-Synced\n\n" f"๐ŸŽฏ Token: {token}\n" f"๐Ÿ“ˆ Direction: {position_side.upper()}\n" f"๐Ÿ“ Size: {abs(contracts):.6f} {token}\n" f"๐Ÿ’ฐ Entry: ${entry_price:,.4f}\n" f"๐Ÿ’ต Value: ${position_value:,.2f}\n" f"{pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n" ) if leverage > 1: notification_text += f"โšก Leverage: {leverage:.1f}x\n" if liquidation_price > 0: notification_text += f"โš ๏ธ Liquidation: ${liquidation_price:,.2f}\n" notification_text += ( f"\n๐Ÿ“ Reason: Position opened outside bot\n" f"โฐ Time: {datetime.now().strftime('%H:%M:%S')}\n\n" f"โœ… Position now tracked in bot\n" f"๐Ÿ’ก Use /sl {token} [price] to set stop loss" ) # Send notification via trading engine's notification manager if self.notification_manager: try: await self.notification_manager.send_generic_notification(notification_text) logger.info(f"๐Ÿ“ค Sent auto-sync notification for {symbol}") except Exception as e: logger.error(f"โŒ Failed to send auto-sync notification: {e}") else: logger.warning(f"โš ๏ธ No notification manager available for auto-sync notification") else: logger.error(f"โŒ Failed to sync orphaned position for {symbol}") else: logger.error(f"โŒ Failed to create lifecycle for orphaned position {symbol}") if synced_positions: sync_msg = f"๐Ÿ”„ Auto-synced {len(synced_positions)} orphaned position(s): {', '.join([s.split('/')[0] for s in synced_positions])}\n\n" else: sync_msg = "" # Get open positions from unified trades table (now including any newly synced ones) open_positions = stats.get_open_positions() positions_text = f"๐Ÿ“ˆ Open Positions\n\n{sync_msg}" if open_positions: total_unrealized = 0 total_position_value = 0 # Also get fresh exchange data for display fresh_exchange_positions = self.trading_engine.get_positions() or [] exchange_data_map = {pos.get('symbol'): pos for pos in fresh_exchange_positions} for position_trade in open_positions: symbol = position_trade['symbol'] token = symbol.split('/')[0] if '/' in symbol else symbol position_side = position_trade['position_side'] # 'long' or 'short' entry_price = position_trade['entry_price'] current_amount = position_trade['current_position_size'] trade_type = position_trade.get('trade_type', 'manual') # ๐Ÿ†• Use fresh exchange data if available (most accurate) exchange_pos = exchange_data_map.get(symbol) if exchange_pos: # Use exchange's official data unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0)) mark_price = float(exchange_pos.get('markPrice') or 0) position_value = float(exchange_pos.get('notional', 0)) liquidation_price = float(exchange_pos.get('liquidationPrice', 0)) margin_used = float(exchange_pos.get('initialMargin', 0)) leverage = float(exchange_pos.get('leverage', 1)) pnl_percentage = float(exchange_pos.get('percentage', 0)) # Get mark price from market data if not in position data if mark_price <= 0: try: market_data = self.trading_engine.get_market_data(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 else: # Fallback to our calculation if exchange data unavailable unrealized_pnl = position_trade.get('unrealized_pnl', 0) mark_price = entry_price # Fallback try: market_data = self.trading_engine.get_market_data(symbol) if market_data and market_data.get('ticker'): mark_price = float(market_data['ticker'].get('last', entry_price)) # Calculate unrealized PnL with current price if position_side == 'long': unrealized_pnl = current_amount * (mark_price - entry_price) else: # Short position unrealized_pnl = current_amount * (entry_price - mark_price) except: pass # Use entry price as fallback position_value = abs(current_amount) * mark_price liquidation_price = None margin_used = None leverage = None pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0 total_position_value += position_value total_unrealized += unrealized_pnl # Position emoji and formatting if position_side == 'long': pos_emoji = "๐ŸŸข" direction = "LONG" else: # Short position pos_emoji = "๐Ÿ”ด" direction = "SHORT" pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด" # Format prices with proper precision for this token formatter = get_formatter() entry_price_str = formatter.format_price_with_symbol(entry_price, token) mark_price_str = formatter.format_price_with_symbol(mark_price, token) # Trade type indicator type_indicator = "" if trade_type == 'external': type_indicator = " ๐Ÿ”„" # External/synced position elif trade_type == 'bot': type_indicator = " ๐Ÿค–" # Bot-created position positions_text += f"{pos_emoji} {token} ({direction}){type_indicator}\n" positions_text += f" ๐Ÿ“ Size: {abs(current_amount):.6f} {token}\n" positions_text += f" ๐Ÿ’ฐ Entry: {entry_price_str}\n" positions_text += f" ๐Ÿ“Š Mark: {mark_price_str}\n" positions_text += f" ๐Ÿ’ต Value: ${position_value:,.2f}\n" positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n" # Show exchange-provided risk data if available if leverage: positions_text += f" โšก Leverage: {leverage:.1f}x\n" if margin_used: positions_text += f" ๐Ÿ’ณ Margin: ${margin_used:,.2f}\n" if liquidation_price: liq_price_str = formatter.format_price_with_symbol(liquidation_price, token) positions_text += f" โš ๏ธ Liquidation: {liq_price_str}\n" # Show stop loss if linked if position_trade.get('stop_loss_price'): sl_price = position_trade['stop_loss_price'] sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active" positions_text += f" ๐Ÿ›‘ Stop Loss: {formatter.format_price_with_symbol(sl_price, token)} ({sl_status})\n" # Show take profit if linked if position_trade.get('take_profit_price'): tp_price = position_trade['take_profit_price'] tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active" positions_text += f" ๐ŸŽฏ Take Profit: {formatter.format_price_with_symbol(tp_price, token)} ({tp_status})\n" positions_text += f" ๐Ÿ†” Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\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\n" positions_text += f"๐Ÿค– Legend: ๐Ÿค– Bot-created โ€ข ๐Ÿ”„ External/synced\n" positions_text += f"๐Ÿ’ก Use /sl [token] [price] or /tp [token] [price] to set risk management" 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') 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 = "๐Ÿ“‹ 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" formatter = get_formatter() 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} @ {formatter.format_price_with_symbol(price, symbol)}\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( status='pending_trigger', order_type_filter='stop_limit_trigger', parent_bot_order_ref_id=bot_ref_id ) if pending_sls: sl_order = pending_sls[0] # Should only be one sl_price = sl_order.get('price', 0) sl_side = sl_order.get('side', '').upper() orders_text += f" ๐Ÿ›‘ Pending SL: {sl_side} @ {formatter.format_price_with_symbol(sl_price, symbol)} (activates when filled)\n" orders_text += "\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 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 - Show recent trade history.""" if not self._is_authorized(update): return try: stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("โŒ Trading statistics not available.", parse_mode='HTML') return # Get recent trades (limit to last 20) recent_trades = stats.get_recent_trades(limit=20) if not recent_trades: await update.message.reply_text("๐Ÿ“Š No trades found.", parse_mode='HTML') return message = "๐Ÿ“ˆ Recent Trades (Last 20)\n\n" for trade in recent_trades: symbol = trade['symbol'] token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] if ':' in symbol else symbol side = trade['side'].upper() amount = trade['amount'] price = trade['price'] timestamp = trade['timestamp'] pnl = trade.get('realized_pnl', 0) # Format timestamp try: from datetime import datetime dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) time_str = dt.strftime('%m/%d %H:%M') except: time_str = "Unknown" # PnL emoji and formatting if pnl > 0: pnl_emoji = "๐ŸŸข" pnl_str = f"+${pnl:.2f}" elif pnl < 0: pnl_emoji = "๐Ÿ”ด" pnl_str = f"${pnl:.2f}" else: pnl_emoji = "โšช" pnl_str = "$0.00" side_emoji = "๐ŸŸข" if side == 'BUY' else "๐Ÿ”ด" message += f"{side_emoji} {side} {amount} {token} @ ${price:,.2f}\n" message += f" {pnl_emoji} P&L: {pnl_str} | {time_str}\n\n" await update.message.reply_text(message, parse_mode='HTML') except Exception as e: logger.error(f"Error in trades command: {e}") await update.message.reply_text("โŒ Error retrieving trade history.", parse_mode='HTML') async def cycles_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /cycles command - Show trade cycles and lifecycle statistics.""" if not self._is_authorized(update): return try: stats = self.trading_engine.get_stats() if not stats: await update.message.reply_text("โŒ Trading statistics not available.", parse_mode='HTML') return # Get trade cycle performance stats cycle_stats = stats.get_trade_cycle_performance_stats() if not cycle_stats or cycle_stats.get('total_closed_trades', 0) == 0: await update.message.reply_text("๐Ÿ“Š No completed trade cycles found.", parse_mode='HTML') return # Get recent trade cycles recent_cycles = stats.get_recent_trade_cycles(limit=10) open_cycles = stats.get_open_trade_cycles() message = "๐Ÿ”„ Trade Cycle Statistics\n\n" # Performance summary total_trades = cycle_stats.get('total_closed_trades', 0) win_rate = cycle_stats.get('win_rate', 0) total_pnl = cycle_stats.get('total_pnl', 0) avg_duration = cycle_stats.get('avg_duration_minutes', 0) profit_factor = cycle_stats.get('profit_factor', 0) stop_loss_rate = cycle_stats.get('stop_loss_rate', 0) pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด" message += f"๐Ÿ“Š Performance Summary:\n" message += f"โ€ข Total Completed: {total_trades} trades\n" message += f"โ€ข Win Rate: {win_rate:.1f}%\n" message += f"โ€ข {pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" message += f"โ€ข Avg Duration: {avg_duration:.1f} min\n" message += f"โ€ข Profit Factor: {profit_factor:.2f}\n" message += f"โ€ข Stop Loss Rate: {stop_loss_rate:.1f}%\n\n" # Open cycles if open_cycles: message += f"๐ŸŸข Open Cycles ({len(open_cycles)}):\n" for cycle in open_cycles[:5]: # Show max 5 symbol = cycle['symbol'] token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] side = cycle['side'].upper() entry_price = cycle.get('entry_price', 0) side_emoji = "๐Ÿ“ˆ" if side == 'BUY' else "๐Ÿ“‰" message += f"{side_emoji} {side} {token} @ ${entry_price:.2f}\n" message += "\n" # Recent completed cycles if recent_cycles: completed_recent = [c for c in recent_cycles if c['status'] == 'closed'][:5] if completed_recent: message += f"๐Ÿ“‹ Recent Completed ({len(completed_recent)}):\n" for cycle in completed_recent: symbol = cycle['symbol'] token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] side = cycle['side'].upper() entry_price = cycle.get('entry_price', 0) exit_price = cycle.get('exit_price', 0) pnl = cycle.get('realized_pnl', 0) exit_type = cycle.get('exit_type', 'unknown') duration = cycle.get('duration_seconds', 0) # Format duration if duration > 3600: duration_str = f"{duration//3600:.0f}h" elif duration > 60: duration_str = f"{duration//60:.0f}m" else: duration_str = f"{duration}s" pnl_emoji = "๐ŸŸข" if pnl >= 0 else "๐Ÿ”ด" side_emoji = "๐Ÿ“ˆ" if side == 'BUY' else "๐Ÿ“‰" exit_emoji = "๐Ÿ›‘" if exit_type == 'stop_loss' else "๐ŸŽฏ" if exit_type == 'take_profit' else "๐Ÿ‘‹" message += f"{side_emoji} {side} {token}: ${entry_price:.2f} โ†’ ${exit_price:.2f}\n" message += f" {pnl_emoji} ${pnl:+.2f} | {exit_emoji} {exit_type} | {duration_str}\n" message += "\n" message += "๐Ÿ’ก Trade cycles track complete trades from open to close with full P&L analysis." await update.message.reply_text(message, parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing cycles command: {str(e)}" await context.bot.send_message(chat_id=chat_id, text=error_message) logger.error(f"Error in cycles command: {e}") async def active_trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /active command to show active trades (Phase 1 testing).""" 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 all active trades all_active_trades = stats.get_all_active_trades() if not all_active_trades: await context.bot.send_message( chat_id=chat_id, text="๐Ÿ“Š Active Trades (Phase 1)\n\n๐Ÿ“ญ No active trades found.", parse_mode='HTML' ) return # Group by status active_trades_by_status = {} for trade in all_active_trades: status = trade['status'] if status not in active_trades_by_status: active_trades_by_status[status] = [] active_trades_by_status[status].append(trade) message_text = "๐Ÿ“Š Active Trades (Phase 1)\n\n" # Show each status group for status, trades in active_trades_by_status.items(): status_emoji = { 'pending': 'โณ', 'active': '๐ŸŸข', 'closed': 'โœ…', 'cancelled': 'โŒ' }.get(status, '๐Ÿ“Š') message_text += f"{status_emoji} {status.upper()} ({len(trades)} trades):\n" for trade in trades[:5]: # Limit to 5 per status to avoid long messages symbol = trade['symbol'] token = symbol.split('/')[0] if '/' in symbol else symbol side = trade['side'].upper() entry_price = trade.get('entry_price') entry_amount = trade.get('entry_amount') realized_pnl = trade.get('realized_pnl', 0) message_text += f" โ€ข {side} {token}" if entry_price and entry_amount: message_text += f" | {entry_amount:.6f} @ ${entry_price:.2f}" if status == 'closed' and realized_pnl != 0: pnl_emoji = "๐ŸŸข" if realized_pnl >= 0 else "๐Ÿ”ด" message_text += f" | {pnl_emoji} ${realized_pnl:.2f}" if trade.get('stop_loss_price'): message_text += f" | SL: ${trade['stop_loss_price']:.2f}" message_text += "\n" if len(trades) > 5: message_text += f" ... and {len(trades) - 5} more\n" message_text += "\n" # Add summary total_trades = len(all_active_trades) pending_count = len(active_trades_by_status.get('pending', [])) active_count = len(active_trades_by_status.get('active', [])) closed_count = len(active_trades_by_status.get('closed', [])) cancelled_count = len(active_trades_by_status.get('cancelled', [])) message_text += f"๐Ÿ“ˆ Summary:\n" message_text += f" Total: {total_trades} | " message_text += f"Pending: {pending_count} | " message_text += f"Active: {active_count} | " message_text += f"Closed: {closed_count} | " message_text += f"Cancelled: {cancelled_count}\n\n" message_text += f"๐Ÿ’ก This is Phase 1 testing - active trades run parallel to trade cycles" await context.bot.send_message(chat_id=chat_id, text=message_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing active trades command: {str(e)}" await context.bot.send_message(chat_id=chat_id, text=error_message) logger.error(f"Error in active trades command: {e}") 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 "๐Ÿ”ด" # Format prices with proper precision for this token formatter = get_formatter() current_price_str = formatter.format_price_with_symbol(current_price, token) bid_price_str = formatter.format_price_with_symbol(bid_price, token) ask_price_str = formatter.format_price_with_symbol(ask_price, token) spread_str = formatter.format_price_with_symbol(ask_price - bid_price, token) high_24h_str = formatter.format_price_with_symbol(high_24h, token) low_24h_str = formatter.format_price_with_symbol(low_24h, token) change_24h_str = formatter.format_price_with_symbol(change_24h, token) market_text = f""" ๐Ÿ“Š {token} Market Data ๐Ÿ’ฐ Price Information: ๐Ÿ’ต Current: {current_price_str} ๐ŸŸข Bid: {bid_price_str} ๐Ÿ”ด Ask: {ask_price_str} ๐Ÿ“Š Spread: {spread_str} ๐Ÿ“ˆ 24h Statistics: {trend_emoji} Change: {change_24h_str} ({change_percent:+.2f}%) ๐Ÿ” High: {high_24h_str} ๐Ÿ”ป Low: {low_24h_str} ๐Ÿ“Š Volume: {volume_24h:,.2f} {token} โฐ Last Updated: {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 "๐Ÿ”ด" # Format prices with proper precision for this token formatter = get_formatter() current_price_str = formatter.format_price_with_symbol(current_price, token) change_24h_str = formatter.format_price_with_symbol(change_24h, token) price_text = f""" ๐Ÿ’ต {token} Price ๐Ÿ’ฐ {current_price_str} {trend_emoji} {change_percent:+.2f}% ({change_24h_str}) โฐ {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= "๐Ÿ“Š 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 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"๐Ÿ“Š {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 context.bot.send_message(chat_id=chat_id, 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 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= "๐Ÿ“… 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 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= "๐Ÿ“Š 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 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= "๐Ÿ“† 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 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= "๐Ÿ“Š 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 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= "๐Ÿ’ฐ 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 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 = """ ๐Ÿ“ฑ 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 context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup) async def _estimate_entry_price_for_orphaned_position(self, symbol: str, contracts: float) -> float: """Estimate entry price for an orphaned position by checking recent fills and market data.""" try: # Method 1: Check recent fills from the exchange recent_fills = self.trading_engine.get_recent_fills() if recent_fills: # Look for recent fills for this symbol symbol_fills = [fill for fill in recent_fills if fill.get('symbol') == symbol] if symbol_fills: # Get the most recent fill as entry price estimate latest_fill = symbol_fills[0] # Assuming sorted by newest first fill_price = float(latest_fill.get('price', 0)) if fill_price > 0: logger.info(f"๐Ÿ’ก Found recent fill price for {symbol}: ${fill_price:.4f}") return fill_price # Method 2: Use current market price as fallback market_data = self.trading_engine.get_market_data(symbol) if market_data and market_data.get('ticker'): current_price = float(market_data['ticker'].get('last', 0)) if current_price > 0: logger.warning(f"โš ๏ธ Using current market price as entry estimate for {symbol}: ${current_price:.4f}") return current_price # Method 3: Last resort - try bid/ask average if market_data and market_data.get('ticker'): bid = float(market_data['ticker'].get('bid', 0)) ask = float(market_data['ticker'].get('ask', 0)) if bid > 0 and ask > 0: avg_price = (bid + ask) / 2 logger.warning(f"โš ๏ธ Using bid/ask average as entry estimate for {symbol}: ${avg_price:.4f}") return avg_price # Method 4: Absolute fallback - return a small positive value to avoid 0 logger.error(f"โŒ Could not estimate entry price for {symbol}, using fallback value of $1.00") return 1.0 except Exception as e: logger.error(f"โŒ Error estimating entry price for {symbol}: {e}") return 1.0 # Safe fallback