|
@@ -5,6 +5,7 @@ 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
|
|
|
|
|
@@ -120,12 +121,22 @@ class InfoCommands:
|
|
|
# Use the new position direction logic
|
|
|
position_type, exit_side, contracts = self.trading_engine.get_position_direction(position)
|
|
|
|
|
|
- entry_price = float(position.get('entryPx', 0))
|
|
|
- mark_price = float(position.get('markPx', entry_price))
|
|
|
+ # 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 = contracts * mark_price
|
|
|
+ position_value = abs(contracts) * mark_price
|
|
|
total_position_value += position_value
|
|
|
total_unrealized += unrealized_pnl
|
|
|
|
|
@@ -141,7 +152,7 @@ class InfoCommands:
|
|
|
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: {contracts:.6f} {symbol}\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"
|
|
@@ -197,7 +208,7 @@ class InfoCommands:
|
|
|
orders_text += f" {side_emoji} {side} {amount:.6f} @ ${price:,.2f}\n"
|
|
|
orders_text += f" 📋 Type: {order_type} | ID: {order_id}\n\n"
|
|
|
|
|
|
- orders_text += f"📊 <b>Total Orders:</b> {len(orders)}\n"
|
|
|
+ orders_text += f"💼 <b>Total Orders:</b> {len(orders)}\n"
|
|
|
orders_text += f"💡 Use /coo [token] to cancel orders"
|
|
|
|
|
|
else:
|
|
@@ -344,4 +355,622 @@ class InfoCommands:
|
|
|
|
|
|
await update.message.reply_text(price_text.strip(), parse_mode='HTML')
|
|
|
else:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch price for {token}")
|
|
|
+ await update.message.reply_text(f"❌ Could not fetch price for {token}")
|
|
|
+
|
|
|
+ async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /performance command to show token performance ranking or detailed stats."""
|
|
|
+ if not self._is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Check if specific token is requested
|
|
|
+ if context.args and len(context.args) >= 1:
|
|
|
+ # Detailed performance for specific token
|
|
|
+ token = context.args[0].upper()
|
|
|
+ await self._show_token_performance(update, token)
|
|
|
+ else:
|
|
|
+ # Show token performance ranking
|
|
|
+ await self._show_performance_ranking(update)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing performance command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in performance command: {e}")
|
|
|
+
|
|
|
+ async def _show_performance_ranking(self, update: Update):
|
|
|
+ """Show token performance ranking (compressed view)."""
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await update.message.reply_text("❌ Could not load trading statistics")
|
|
|
+ return
|
|
|
+
|
|
|
+ token_performance = stats.get_token_performance()
|
|
|
+
|
|
|
+ if not token_performance:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "📊 <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 update.message.reply_text(performance_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ async def _show_token_performance(self, update: Update, token: str):
|
|
|
+ """Show detailed performance for a specific token."""
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await update.message.reply_text("❌ Could not load trading statistics")
|
|
|
+ return
|
|
|
+
|
|
|
+ token_stats = stats.get_token_detailed_stats(token)
|
|
|
+
|
|
|
+ # Check if token has any data
|
|
|
+ if token_stats.get('total_trades', 0) == 0:
|
|
|
+ await update.message.reply_text(
|
|
|
+ f"📊 <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 update.message.reply_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 update.message.reply_text(performance_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /daily command to show daily performance stats."""
|
|
|
+ if not self._is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await update.message.reply_text("❌ Could not load trading statistics")
|
|
|
+ return
|
|
|
+
|
|
|
+ daily_stats = stats.get_daily_stats(10)
|
|
|
+
|
|
|
+ if not daily_stats:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "📅 <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 update.message.reply_text(daily_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing daily command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in daily command: {e}")
|
|
|
+
|
|
|
+ async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /weekly command to show weekly performance stats."""
|
|
|
+ if not self._is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await update.message.reply_text("❌ Could not load trading statistics")
|
|
|
+ return
|
|
|
+
|
|
|
+ weekly_stats = stats.get_weekly_stats(10)
|
|
|
+
|
|
|
+ if not weekly_stats:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "📊 <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 update.message.reply_text(weekly_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing weekly command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in weekly command: {e}")
|
|
|
+
|
|
|
+ async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /monthly command to show monthly performance stats."""
|
|
|
+ if not self._is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await update.message.reply_text("❌ Could not load trading statistics")
|
|
|
+ return
|
|
|
+
|
|
|
+ monthly_stats = stats.get_monthly_stats(10)
|
|
|
+
|
|
|
+ if not monthly_stats:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "📆 <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 update.message.reply_text(monthly_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing monthly command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in monthly command: {e}")
|
|
|
+
|
|
|
+ async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /risk command to show advanced risk metrics."""
|
|
|
+ if not self._is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Get current balance for context
|
|
|
+ balance = self.trading_engine.get_balance()
|
|
|
+ current_balance = 0
|
|
|
+ if balance and balance.get('total'):
|
|
|
+ current_balance = float(balance['total'].get('USDC', 0))
|
|
|
+
|
|
|
+ # Get risk metrics and basic stats
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await update.message.reply_text("❌ Could not load trading statistics")
|
|
|
+ return
|
|
|
+
|
|
|
+ risk_metrics = stats.get_risk_metrics()
|
|
|
+ basic_stats = stats.get_basic_stats()
|
|
|
+
|
|
|
+ # Check if we have enough data for risk calculations
|
|
|
+ if basic_stats['completed_trades'] < 2:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "📊 <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 update.message.reply_text(risk_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing risk command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in risk command: {e}")
|
|
|
+
|
|
|
+ async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /balance_adjustments command to show deposit/withdrawal history."""
|
|
|
+ if not self._is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await update.message.reply_text("❌ Could not load trading statistics")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Get balance adjustments summary
|
|
|
+ adjustments_summary = stats.get_balance_adjustments_summary()
|
|
|
+
|
|
|
+ # Get detailed adjustments
|
|
|
+ all_adjustments = stats.data.get('balance_adjustments', [])
|
|
|
+
|
|
|
+ if not all_adjustments:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "💰 <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 update.message.reply_text(adjustments_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ Error processing balance adjustments command: {str(e)}"
|
|
|
+ await update.message.reply_text(error_message)
|
|
|
+ logger.error(f"Error in balance_adjustments command: {e}")
|
|
|
+
|
|
|
+ async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /commands and /c command with quick action buttons."""
|
|
|
+ if not self._is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ commands_text = """
|
|
|
+📱 <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 update.message.reply_text(commands_text, parse_mode='HTML', reply_markup=reply_markup)
|