risk.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import logging
  2. import html
  3. from telegram import Update
  4. from telegram.ext import ContextTypes
  5. from .base import InfoCommandsBase
  6. from src.config.config import Config
  7. from src.utils.token_display_formatter import get_formatter
  8. from datetime import datetime
  9. logger = logging.getLogger(__name__)
  10. class RiskCommands(InfoCommandsBase):
  11. """Handles all risk management-related commands."""
  12. def __init__(self, trading_engine, notification_manager):
  13. super().__init__(trading_engine, notification_manager)
  14. self.formatter = get_formatter()
  15. async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  16. """Handle the /risk command to show a comprehensive portfolio risk report."""
  17. try:
  18. if context.args:
  19. await self._reply(update, "Note: The `/risk` command shows portfolio-level data and does not accept arguments.")
  20. message = ["<b>🛡️ Portfolio Risk Analysis</b>\n"]
  21. # 1. Open Positions
  22. positions = self.trading_engine.get_positions()
  23. message.append(self._format_open_positions(positions))
  24. # 2. Portfolio Summary
  25. balance_data = self.trading_engine.get_balance()
  26. message.append(await self._format_portfolio_summary(balance_data, positions))
  27. # 3. Trading Stats
  28. stats = self.trading_engine.stats.get_trading_stats()
  29. message.append(self._format_trading_stats(stats))
  30. # 4. Risk Metrics
  31. risk_metrics = self.trading_engine.stats.get_risk_metrics()
  32. message.append(self._format_risk_metrics(risk_metrics))
  33. # 5. Footer
  34. message.append("<i>This report provides a snapshot of your portfolio's risk. Manage positions carefully.</i>")
  35. await self._reply(update, "\n".join(message))
  36. except Exception as e:
  37. logger.error(f"Error in risk command: {e}", exc_info=True)
  38. await self._reply(update, "❌ Error generating risk report. Please try again later.")
  39. def _format_open_positions(self, positions):
  40. """Formats the open positions section."""
  41. if not positions:
  42. return "<b>Open Positions:</b> None\n"
  43. lines = ["<b>Open Positions:</b>"]
  44. for p in positions:
  45. try:
  46. position_info = p.get('position', p)
  47. pnl = float(p.get('unrealizedPnl', 0.0))
  48. pnl_emoji = "🟢" if pnl >= 0 else "🔴"
  49. # Use .get() with defaults to avoid KeyErrors
  50. asset = position_info.get('asset', 'N/A')
  51. size = position_info.get('szi', 'N/A')
  52. side = position_info.get('side', 'N/A')
  53. entry_price = position_info.get('entryPx', 'N/A')
  54. lines.append(f"• {asset}: {size} {side} @ ${entry_price} {pnl_emoji}")
  55. except Exception as e:
  56. logger.error(f"Error formatting a position: {p}, error: {e}")
  57. lines.append("• Error displaying one position.")
  58. return "\n".join(lines) + "\n"
  59. async def _format_portfolio_summary(self, balance_data, positions):
  60. """Formats the portfolio summary section."""
  61. total_unrealized_pnl = sum(float(p.get('unrealizedPnl', 0.0)) for p in positions)
  62. portfolio_value = float(balance_data.get('total', {}).get('USDC', 0.0))
  63. pnl_emoji = "🟢" if total_unrealized_pnl >= 0 else "🔴"
  64. return (
  65. "<b>Portfolio Summary:</b>\n"
  66. f"• <b>Total Value:</b> {await self.formatter.format_price_with_symbol(portfolio_value)}\n"
  67. f"• <b>Unrealized P&L:</b> {pnl_emoji} {await self.formatter.format_price_with_symbol(total_unrealized_pnl)}\n"
  68. )
  69. def _format_trading_stats(self, stats):
  70. """Formats the trading statistics section."""
  71. if not stats:
  72. return "<b>Trading Stats:</b> Not available\n"
  73. return (
  74. "<b>Trading Stats:</b>\n"
  75. f"• <b>Win Rate:</b> {stats.get('win_rate', 0.0):.2f}%\n"
  76. f"• <b>Profit Factor:</b> {stats.get('profit_factor', 0.0):.2f}\n"
  77. )
  78. def _format_risk_metrics(self, risk_metrics):
  79. """Formats the risk metrics section."""
  80. if not risk_metrics:
  81. return "<b>Risk Metrics:</b> Not available\n"
  82. max_drawdown_pct = risk_metrics.get('max_drawdown_percentage', 0.0)
  83. drawdown_start_date_iso = risk_metrics.get('drawdown_start_date')
  84. drawdown_date_str = ""
  85. if drawdown_start_date_iso:
  86. try:
  87. drawdown_date = datetime.fromisoformat(drawdown_start_date_iso).strftime('%Y-%m-%d')
  88. drawdown_date_str = f" (since {drawdown_date})"
  89. except (ValueError, TypeError):
  90. logger.warning(f"Could not parse drawdown_start_date: {drawdown_start_date_iso}")
  91. sharpe_ratio = risk_metrics.get('sharpe_ratio')
  92. sharpe_str = f"{sharpe_ratio:.2f}" if sharpe_ratio is not None else "N/A"
  93. return (
  94. "<b>Risk Metrics:</b>\n"
  95. f"• <b>Max Drawdown:</b> {max_drawdown_pct:.2f}%{drawdown_date_str}\n"
  96. f"• <b>Sharpe Ratio:</b> {sharpe_str}\n"
  97. )