123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- import logging
- import html
- from telegram import Update
- from telegram.ext import ContextTypes
- from .base import InfoCommandsBase
- from src.config.config import Config
- from src.utils.token_display_formatter import get_formatter
- from datetime import datetime
- logger = logging.getLogger(__name__)
- class RiskCommands(InfoCommandsBase):
- """Handles all risk management-related commands."""
- def __init__(self, trading_engine, notification_manager):
- super().__init__(trading_engine, notification_manager)
- self.formatter = get_formatter()
- async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /risk command to show a comprehensive portfolio risk report."""
- try:
- if context.args:
- await self._reply(update, "Note: The `/risk` command shows portfolio-level data and does not accept arguments.")
- message = ["<b>🛡️ Portfolio Risk Analysis</b>\n"]
- # 1. Open Positions
- positions = self.trading_engine.get_positions()
- message.append(self._format_open_positions(positions))
- # 2. Portfolio Summary
- balance_data = self.trading_engine.get_balance()
- message.append(await self._format_portfolio_summary(balance_data, positions))
- # 3. Trading Stats
- stats = self.trading_engine.stats.get_trading_stats()
- message.append(self._format_trading_stats(stats))
- # 4. Risk Metrics
- risk_metrics = self.trading_engine.stats.get_risk_metrics()
- message.append(self._format_risk_metrics(risk_metrics))
- # 5. Footer
- message.append("<i>This report provides a snapshot of your portfolio's risk. Manage positions carefully.</i>")
- await self._reply(update, "\n".join(message))
-
- except Exception as e:
- logger.error(f"Error in risk command: {e}", exc_info=True)
- await self._reply(update, "❌ Error generating risk report. Please try again later.")
- def _format_open_positions(self, positions):
- """Formats the open positions section."""
- if not positions:
- return "<b>Open Positions:</b> None\n"
- lines = ["<b>Open Positions:</b>"]
- for p in positions:
- try:
- position_info = p.get('position', p)
- pnl = float(p.get('unrealizedPnl', 0.0))
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-
- # Use .get() with defaults to avoid KeyErrors
- asset = position_info.get('asset', 'N/A')
- size = position_info.get('szi', 'N/A')
- side = position_info.get('side', 'N/A')
- entry_price = position_info.get('entryPx', 'N/A')
- lines.append(f"• {asset}: {size} {side} @ ${entry_price} {pnl_emoji}")
- except Exception as e:
- logger.error(f"Error formatting a position: {p}, error: {e}")
- lines.append("• Error displaying one position.")
- return "\n".join(lines) + "\n"
- async def _format_portfolio_summary(self, balance_data, positions):
- """Formats the portfolio summary section."""
- total_unrealized_pnl = sum(float(p.get('unrealizedPnl', 0.0)) for p in positions)
- portfolio_value = float(balance_data.get('total', {}).get('USDC', 0.0))
- pnl_emoji = "🟢" if total_unrealized_pnl >= 0 else "🔴"
-
- return (
- "<b>Portfolio Summary:</b>\n"
- f"• <b>Total Value:</b> {await self.formatter.format_price_with_symbol(portfolio_value)}\n"
- f"• <b>Unrealized P&L:</b> {pnl_emoji} {await self.formatter.format_price_with_symbol(total_unrealized_pnl)}\n"
- )
- def _format_trading_stats(self, stats):
- """Formats the trading statistics section."""
- if not stats:
- return "<b>Trading Stats:</b> Not available\n"
-
- return (
- "<b>Trading Stats:</b>\n"
- f"• <b>Win Rate:</b> {stats.get('win_rate', 0.0):.2f}%\n"
- f"• <b>Profit Factor:</b> {stats.get('profit_factor', 0.0):.2f}\n"
- )
- def _format_risk_metrics(self, risk_metrics):
- """Formats the risk metrics section."""
- if not risk_metrics:
- return "<b>Risk Metrics:</b> Not available\n"
- max_drawdown_pct = risk_metrics.get('max_drawdown_percentage', 0.0)
- drawdown_start_date_iso = risk_metrics.get('drawdown_start_date')
- drawdown_date_str = ""
- if drawdown_start_date_iso:
- try:
- drawdown_date = datetime.fromisoformat(drawdown_start_date_iso).strftime('%Y-%m-%d')
- drawdown_date_str = f" (since {drawdown_date})"
- except (ValueError, TypeError):
- logger.warning(f"Could not parse drawdown_start_date: {drawdown_start_date_iso}")
- sharpe_ratio = risk_metrics.get('sharpe_ratio')
- sharpe_str = f"{sharpe_ratio:.2f}" if sharpe_ratio is not None else "N/A"
- return (
- "<b>Risk Metrics:</b>\n"
- f"• <b>Max Drawdown:</b> {max_drawdown_pct:.2f}%{drawdown_date_str}\n"
- f"• <b>Sharpe Ratio:</b> {sharpe_str}\n"
- )
|