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 = ["🛡️ Portfolio Risk Analysis\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("This report provides a snapshot of your portfolio's risk. Manage positions carefully.")
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 "Open Positions: None\n"
lines = ["Open Positions:"]
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 (
"Portfolio Summary:\n"
f"• Total Value: {await self.formatter.format_price_with_symbol(portfolio_value)}\n"
f"• Unrealized P&L: {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 "Trading Stats: Not available\n"
return (
"Trading Stats:\n"
f"• Win Rate: {stats.get('win_rate', 0.0):.2f}%\n"
f"• Profit Factor: {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 "Risk Metrics: 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 (
"Risk Metrics:\n"
f"• Max Drawdown: {max_drawdown_pct:.2f}%{drawdown_date_str}\n"
f"• Sharpe Ratio: {sharpe_str}\n"
)