123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014 |
- #!/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)
|