|
@@ -1,8 +1,8 @@
|
|
import logging
|
|
import logging
|
|
|
|
+import html
|
|
from telegram import Update
|
|
from telegram import Update
|
|
from telegram.ext import ContextTypes
|
|
from telegram.ext import ContextTypes
|
|
from .base import InfoCommandsBase
|
|
from .base import InfoCommandsBase
|
|
-from src.utils.token_display_formatter import get_formatter
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@@ -10,121 +10,127 @@ class RiskCommands(InfoCommandsBase):
|
|
"""Handles all risk management-related commands."""
|
|
"""Handles all risk management-related commands."""
|
|
|
|
|
|
async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
- """Handle the /risk command to show risk management information."""
|
|
|
|
|
|
+ """Handle the /risk command to show advanced risk metrics."""
|
|
|
|
+ chat_id = update.effective_chat.id
|
|
if not self._is_authorized(update):
|
|
if not self._is_authorized(update):
|
|
return
|
|
return
|
|
-
|
|
|
|
|
|
+
|
|
try:
|
|
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()
|
|
stats = self.trading_engine.get_stats()
|
|
if not stats:
|
|
if not stats:
|
|
- await self._reply(update, "❌ Could not load trading statistics")
|
|
|
|
|
|
+ await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
|
|
return
|
|
return
|
|
-
|
|
|
|
- # Get current positions
|
|
|
|
- positions = stats.get_open_positions()
|
|
|
|
- if not positions:
|
|
|
|
- await self._reply(update, "📭 No open positions to analyze risk")
|
|
|
|
|
|
+
|
|
|
|
+ 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: {html.escape(str(basic_stats['completed_trades']))}\n"
|
|
|
|
+ f"• Required for risk analysis: 2+ trades\n"
|
|
|
|
+ f"• Daily balance snapshots: {html.escape(str(stats.get_daily_balance_record_count()))}\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
|
|
return
|
|
-
|
|
|
|
- # Get current orders for stop loss analysis
|
|
|
|
- orders = self.trading_engine.get_orders() or []
|
|
|
|
|
|
|
|
- # Get formatter for consistent display
|
|
|
|
- formatter = get_formatter()
|
|
|
|
-
|
|
|
|
- # Build risk analysis message
|
|
|
|
- risk_text_parts = ["🎯 <b>Risk Management Overview</b>"]
|
|
|
|
-
|
|
|
|
- # Analyze each position
|
|
|
|
- for position in positions:
|
|
|
|
- try:
|
|
|
|
- symbol = position.get('symbol', '')
|
|
|
|
- if not symbol:
|
|
|
|
- continue
|
|
|
|
|
|
+ # Get risk metric values with safe defaults
|
|
|
|
+ sharpe_ratio = risk_metrics.get('sharpe_ratio')
|
|
|
|
+ max_drawdown_pct = risk_metrics.get('max_drawdown_live_percentage', 0.0)
|
|
|
|
+
|
|
|
|
+ # Format values safely
|
|
|
|
+ sharpe_str = f"{sharpe_ratio:.3f}" if sharpe_ratio is not None else "N/A"
|
|
|
|
+
|
|
|
|
+ # Format the risk analysis message
|
|
|
|
+ risk_text = f"""
|
|
|
|
+📊 <b>Risk Analysis & Advanced Metrics</b>
|
|
|
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
|
|
|
|
- side = position.get('position_side', '')
|
|
|
|
- size = float(position.get('current_position_size', 0))
|
|
|
|
- entry_price = float(position.get('entry_price', 0))
|
|
|
|
- current_price = float(position.get('current_price', entry_price))
|
|
|
|
-
|
|
|
|
- if size == 0 or entry_price <= 0 or current_price <= 0:
|
|
|
|
- continue
|
|
|
|
|
|
+🎯 <b>Risk-Adjusted Performance:</b>
|
|
|
|
+• Sharpe Ratio: {sharpe_str}
|
|
|
|
+• Profit Factor: {risk_metrics.get('profit_factor', 0):.2f}
|
|
|
|
+• Win Rate: {risk_metrics.get('win_rate', 0):.1f}%
|
|
|
|
|
|
- # Calculate unrealized P&L
|
|
|
|
- if side == 'long':
|
|
|
|
- unrealized_pnl = size * (current_price - entry_price)
|
|
|
|
- else: # short
|
|
|
|
- unrealized_pnl = size * (entry_price - current_price)
|
|
|
|
-
|
|
|
|
- # Find stop loss orders for this position
|
|
|
|
- stop_loss_orders = [
|
|
|
|
- order for order in orders
|
|
|
|
- if order.get('symbol') == symbol and
|
|
|
|
- order.get('type') == 'stop_loss'
|
|
|
|
- ]
|
|
|
|
-
|
|
|
|
- # Get the most relevant stop loss price
|
|
|
|
- stop_loss_price = None
|
|
|
|
- if stop_loss_orders:
|
|
|
|
- # Sort by trigger price to find the most conservative stop loss
|
|
|
|
- stop_loss_orders.sort(key=lambda x: float(x.get('triggerPrice', 0)))
|
|
|
|
- if side == 'long':
|
|
|
|
- stop_loss_price = float(stop_loss_orders[0].get('triggerPrice', 0))
|
|
|
|
- else: # short
|
|
|
|
- stop_loss_price = float(stop_loss_orders[-1].get('triggerPrice', 0))
|
|
|
|
-
|
|
|
|
- # Calculate risk metrics
|
|
|
|
- risk_text_parts.append(f"\n📊 <b>{token}</b>")
|
|
|
|
- risk_text_parts.append(f"• Position: {side.upper()} {formatter.format_amount(size, token)} @ {formatter.format_price_with_symbol(entry_price, token)}")
|
|
|
|
- risk_text_parts.append(f"• Current: {formatter.format_price_with_symbol(current_price, token)}")
|
|
|
|
-
|
|
|
|
- # Show P&L
|
|
|
|
- pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
|
- risk_text_parts.append(f"• P&L: {pnl_emoji} {formatter.format_price_with_symbol(unrealized_pnl)}")
|
|
|
|
-
|
|
|
|
- # Show stop loss info
|
|
|
|
- if stop_loss_price:
|
|
|
|
- risk_amount = abs(size * (stop_loss_price - entry_price))
|
|
|
|
- risk_text_parts.append(f"• Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)}")
|
|
|
|
- risk_text_parts.append(f"• Risk Amount: {formatter.format_price_with_symbol(risk_amount)}")
|
|
|
|
- else:
|
|
|
|
- risk_text_parts.append("• Stop Loss: ❌ Not set")
|
|
|
|
-
|
|
|
|
- # Add separator between positions
|
|
|
|
- risk_text_parts.append("")
|
|
|
|
|
|
+📉 <b>Drawdown Analysis:</b>
|
|
|
|
+• Maximum Drawdown: {max_drawdown_pct:.2f}%
|
|
|
|
+• Max Consecutive Losses: {risk_metrics.get('max_consecutive_losses', 0)}
|
|
|
|
|
|
- except (ValueError, TypeError) as e:
|
|
|
|
- logger.warning(f"Error processing position {position.get('symbol', 'unknown')}: {e}")
|
|
|
|
- continue
|
|
|
|
|
|
+💰 <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: {html.escape(str(basic_stats['days_active']))}
|
|
|
|
|
|
- # Add portfolio-level risk metrics
|
|
|
|
- total_position_value = 0
|
|
|
|
- for pos in positions:
|
|
|
|
- try:
|
|
|
|
- size = float(pos.get('current_position_size', 0))
|
|
|
|
- price = float(pos.get('current_price', 0))
|
|
|
|
- if size != 0 and price > 0:
|
|
|
|
- total_position_value += abs(size * price)
|
|
|
|
- except (ValueError, TypeError):
|
|
|
|
- continue
|
|
|
|
|
|
+📊 <b>Risk Interpretation:</b>
|
|
|
|
+"""
|
|
|
|
|
|
- # Get account balance
|
|
|
|
- balance = self.trading_engine.get_balance()
|
|
|
|
- if balance:
|
|
|
|
- try:
|
|
|
|
- total_balance = float(balance.get('total', 0))
|
|
|
|
- if total_balance > 0:
|
|
|
|
- portfolio_risk = (total_position_value / total_balance) * 100
|
|
|
|
- risk_text_parts.append(f"📈 <b>Portfolio Risk</b>")
|
|
|
|
- risk_text_parts.append(f"• Total Position Value: {formatter.format_price_with_symbol(total_position_value)}")
|
|
|
|
- risk_text_parts.append(f"• Portfolio Risk: {portfolio_risk:.1f}% of balance")
|
|
|
|
- except (ValueError, TypeError) as e:
|
|
|
|
- logger.warning(f"Error calculating portfolio risk: {e}")
|
|
|
|
|
|
+ # Add interpretive guidance
|
|
|
|
+ if sharpe_ratio is not None:
|
|
|
|
+ if sharpe_ratio > 2.0:
|
|
|
|
+ risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
|
|
|
|
+ elif sharpe_ratio > 1.0:
|
|
|
|
+ risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
|
|
|
|
+ elif sharpe_ratio > 0.5:
|
|
|
|
+ risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
|
|
|
|
+ elif sharpe_ratio > 0:
|
|
|
|
+ risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
|
|
|
|
+ else:
|
|
|
|
+ risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
|
|
|
|
+ else:
|
|
|
|
+ risk_text += "• ⚪ <b>Insufficient data</b> for Sharpe ratio calculation\n"
|
|
|
|
+
|
|
|
|
+ if max_drawdown_pct < 5:
|
|
|
|
+ risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
|
|
|
|
+ elif max_drawdown_pct < 15:
|
|
|
|
+ risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
|
|
|
|
+ elif max_drawdown_pct < 30:
|
|
|
|
+ risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
|
|
|
|
+ else:
|
|
|
|
+ risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
|
|
|
|
+
|
|
|
|
+ # Add profit factor interpretation
|
|
|
|
+ profit_factor = risk_metrics.get('profit_factor', 0)
|
|
|
|
+ if profit_factor > 2.0:
|
|
|
|
+ risk_text += "• 🟢 <b>Excellent</b> profit factor (> 2.0)\n"
|
|
|
|
+ elif profit_factor > 1.5:
|
|
|
|
+ risk_text += "• 🟡 <b>Good</b> profit factor (> 1.5)\n"
|
|
|
|
+ elif profit_factor > 1.0:
|
|
|
|
+ risk_text += "• 🟠 <b>Profitable</b> but low profit factor (> 1.0)\n"
|
|
|
|
+ else:
|
|
|
|
+ risk_text += "• 🔴 <b>Unprofitable</b> trading strategy (< 1.0)\n"
|
|
|
|
+
|
|
|
|
+ risk_text += f"""
|
|
|
|
+
|
|
|
|
+💡 <b>Risk Definitions:</b>
|
|
|
|
+• <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
|
|
|
|
+• <b>Profit Factor:</b> Total winning trades / Total losing trades
|
|
|
|
+• <b>Win Rate:</b> Percentage of profitable trades
|
|
|
|
+• <b>Max Drawdown:</b> Largest peak-to-trough decline
|
|
|
|
+• <b>Max Consecutive Losses:</b> Longest streak of losing trades
|
|
|
|
|
|
- await self._reply(update, "\n".join(risk_text_parts).strip(), parse_mode='HTML')
|
|
|
|
|
|
+📈 <b>Data Based On:</b>
|
|
|
|
+• Completed Trades: {html.escape(str(basic_stats['completed_trades']))}
|
|
|
|
+• Trading Period: {html.escape(str(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:
|
|
except Exception as e:
|
|
error_message = f"❌ Error processing risk command: {str(e)}"
|
|
error_message = f"❌ Error processing risk command: {str(e)}"
|
|
- await self._reply(update, error_message)
|
|
|
|
|
|
+ await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
logger.error(f"Error in risk command: {e}")
|
|
logger.error(f"Error in risk command: {e}")
|