Jelajahi Sumber

Refactor risk command and enhance reply method

- Updated the `_reply` method in `InfoCommandsBase` to accept additional keyword arguments, ensuring a default `parse_mode` is set if not provided.
- Modified the `risk_command` in `RiskCommands` to improve messaging and user feedback, including a note about token-specific arguments being ignored.
- Streamlined risk metrics reporting by calculating total unrealized P&L and portfolio risk percentage, while enhancing the message formatting for clarity.
Carles Sentis 2 hari lalu
induk
melakukan
8303ba175b
2 mengubah file dengan 53 tambahan dan 127 penghapusan
  1. 8 4
      src/commands/info/base.py
  2. 45 123
      src/commands/info/risk.py

+ 8 - 4
src/commands/info/base.py

@@ -20,14 +20,18 @@ class InfoCommandsBase:
             return False
         return True  # Add your authorization logic here
 
-    async def _reply(self, update: Update, text: str, parse_mode: str = 'HTML') -> Optional[Message]:
+    async def _reply(self, update: Update, text: str, **kwargs) -> Optional[Message]:
         """Common reply method for all commands."""
+        # Ensure default parse_mode is set if not provided
+        if 'parse_mode' not in kwargs:
+            kwargs['parse_mode'] = 'HTML'
+            
         try:
             if update.callback_query:
                 await update.callback_query.answer()
                 result = await update.callback_query.edit_message_text(
                     text=text,
-                    parse_mode=parse_mode
+                    **kwargs
                 )
                 if isinstance(result, Message):
                     return result
@@ -36,7 +40,7 @@ class InfoCommandsBase:
             elif update.message:
                 return await update.message.reply_text(
                     text=text,
-                    parse_mode=parse_mode
+                    **kwargs
                 )
         except Exception as e:
             self.logger.error(f"Error sending reply: {e}")
@@ -45,7 +49,7 @@ class InfoCommandsBase:
                 if update.message:
                     await update.message.reply_text(
                         text="❌ Error processing command. Please try again.",
-                        parse_mode=parse_mode
+                        parse_mode=kwargs.get('parse_mode', 'HTML')
                     )
             except Exception as e2:
                 self.logger.error(f"Error sending fallback reply: {e2}")

+ 45 - 123
src/commands/info/risk.py

@@ -17,139 +17,61 @@ class RiskCommands(InfoCommandsBase):
         self.formatter = get_formatter()
 
     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 key portfolio risk metrics."""
         try:
-            # Get token from command arguments or use default
-            token = context.args[0].upper() if context.args else Config.DEFAULT_TRADING_TOKEN
-            
-            # Get current positions and orders
+            # Check for unused arguments and notify user
+            if context.args:
+                await self._reply(update, "Note: The `/risk` command shows portfolio-level data. Token-specific arguments are ignored.")
+
+            # Fetch necessary data
+            risk_metrics = self.trading_engine.stats.get_risk_metrics()
             positions = self.trading_engine.get_positions()
-            orders = self.trading_engine.get_orders()
-            
-            # Get trading statistics
-            stats = self.trading_engine.get_stats()
-            
-            # Calculate unrealized P&L for open positions
-            total_unrealized_pnl = 0.0
-            position_risks = []
-            
-            for position in positions:
-                try:
-                    # Get position details
-                    symbol = position.get('symbol', '')
-                    size = float(position.get('size', 0) or 0)
-                    entry_price = float(position.get('entryPrice', 0) or 0)
-                    mark_price = float(position.get('markPrice', 0) or 0)
-                    liquidation_price = float(position.get('liquidationPrice', 0) or 0)
-                    
-                    if size == 0 or entry_price == 0:
-                        continue
-                        
-                    # Calculate position value and P&L
-                    position_value = abs(size * entry_price)
-                    unrealized_pnl = size * (mark_price - entry_price)
-                    total_unrealized_pnl += unrealized_pnl
-                    
-                    # Calculate risk metrics
-                    price_risk = abs((mark_price - entry_price) / entry_price * 100)
-                    liquidation_risk = abs((liquidation_price - mark_price) / mark_price * 100) if liquidation_price > 0 else 0
-                    
-                    # Find stop loss orders for this position
-                    stop_loss_orders = [o for o in orders if o.get('symbol') == symbol and o.get('type') == 'stop']
-                    stop_loss_risk = 0
-                    if stop_loss_orders:
-                        stop_price = float(stop_loss_orders[0].get('price', 0) or 0)
-                        if stop_price > 0:
-                            stop_loss_risk = abs((stop_price - mark_price) / mark_price * 100)
-                    
-                    position_risks.append({
-                        'symbol': symbol,
-                        'size': size,
-                        'entry_price': entry_price,
-                        'mark_price': mark_price,
-                        'position_value': position_value,
-                        'unrealized_pnl': unrealized_pnl,
-                        'price_risk': price_risk,
-                        'liquidation_risk': liquidation_risk,
-                        'stop_loss_risk': stop_loss_risk
-                    })
-                except (ValueError, TypeError) as e:
-                    logger.error(f"Error processing position: {e}")
-                    continue
-            
-            # Get portfolio value
             balance_data = self.trading_engine.get_balance()
-            portfolio_value = float(balance_data.get('total', {}).get('USDC', 0) or 0) if balance_data else 0.0
-            
-            # Calculate portfolio risk metrics
-            portfolio_risk = (total_unrealized_pnl / portfolio_value * 100) if portfolio_value > 0 else 0
-            
-            # Format the message
-            message = f"📊 <b>Risk Analysis for {token}</b>\n\n"
-            
-            # Add position risks
-            if position_risks:
-                message += "🔍 <b>Position Risks:</b>\n"
-                for risk in position_risks:
-                    pnl_emoji = "🟢" if risk['unrealized_pnl'] >= 0 else "🔴"
-                    message += (
-                        f"\n<b>{risk['symbol']}</b>\n"
-                        f"Size: {await self.formatter.format_amount(risk['size'], risk['symbol'].split('/')[0])}\n"
-                        f"Entry: {await self.formatter.format_price_with_symbol(risk['entry_price'], risk['symbol'].split('/')[0])}\n"
-                        f"Mark: {await self.formatter.format_price_with_symbol(risk['mark_price'], risk['symbol'].split('/')[0])}\n"
-                        f"P&L: {pnl_emoji} {await self.formatter.format_price_with_symbol(risk['unrealized_pnl'], risk['symbol'].split('/')[0])}\n"
-                        f"Price Risk: {risk['price_risk']:.2f}%\n"
-                        f"Liquidation Risk: {risk['liquidation_risk']:.2f}%\n"
-                        f"Stop Loss Risk: {risk['stop_loss_risk']:.2f}%\n"
-                    )
-            else:
-                message += "No open positions\n"
-            
-            # Add portfolio summary
-            message += f"\n📈 <b>Portfolio Summary:</b>\n"
-            message += f"Total Value: {await self.formatter.format_price_with_symbol(portfolio_value)}\n"
-            message += f"Unrealized P&L: {await self.formatter.format_price_with_symbol(total_unrealized_pnl)}\n"
-            message += f"Portfolio Risk: {portfolio_risk:.2f}%\n"
-            
-            # Add trading statistics
-            if stats:
-                performance_stats = stats.get_performance_stats()
-                message += f"\n📊 <b>Trading Statistics:</b>\n"
-                message += f"Win Rate: {performance_stats.get('win_rate', 0):.2f}%\n"
-                message += f"Profit Factor: {performance_stats.get('profit_factor', 0):.2f}\n"
-                message += f"Average Win: {await self.formatter.format_price_with_symbol(performance_stats.get('average_win', 0))}\n"
-                message += f"Average Loss: {await self.formatter.format_price_with_symbol(performance_stats.get('average_loss', 0))}\n"
-            
-            # Add risk metrics
-            risk_metrics = self.trading_engine.stats.get_risk_metrics()
+
             if not risk_metrics:
                 await self._reply(update, "Risk metrics are not available yet.")
                 return
+
+            # Calculate total unrealized P&L
+            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))
             
-            if risk_metrics:
-                message += f"\n�� <b>Risk Metrics:</b>\n"
-                # Drawdown
-                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}")
+            # Calculate portfolio risk percentage
+            portfolio_risk_pct = (total_unrealized_pnl / portfolio_value * 100) if portfolio_value > 0 else 0.0
 
-                message += f"• Max Drawdown: {max_drawdown_pct:.2f}%{drawdown_date_str}\n"
+            # Format the message
+            message = [
+                "<b>🛡️ Portfolio Risk Analysis</b>\n",
+                f"Here are the key risk metrics for your portfolio:\n"
+            ]
 
-                # Sharpe Ratio
-                sharpe_ratio = risk_metrics.get('sharpe_ratio')
-                if sharpe_ratio is not None:
-                    message += f"• Sharpe Ratio: {sharpe_ratio:.2f}\n"
-                else:
-                    message += f"• Sharpe Ratio: N/A (Insufficient data)\n"
+            # Max Drawdown
+            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}")
+            
+            message.append(f"• <b>Max Drawdown:</b> {max_drawdown_pct:.2f}%{drawdown_date_str}")
             
-            await self._reply(update, message)
+            # Sharpe Ratio
+            sharpe_ratio = risk_metrics.get('sharpe_ratio')
+            if sharpe_ratio is not None:
+                message.append(f"• <b>Sharpe Ratio:</b> {sharpe_ratio:.2f}")
+            else:
+                message.append("• <b>Sharpe Ratio:</b> N/A (Insufficient data)")
+
+            # Overall Portfolio Risk
+            pnl_emoji = "🟢" if total_unrealized_pnl >= 0 else "🔴"
+            message.append(f"• <b>Open P&L Risk:</b> {pnl_emoji} {portfolio_risk_pct:.2f}% ({await self.formatter.format_price_with_symbol(total_unrealized_pnl)})")
+
+            message.append("\n<i>This provides a high-level overview of portfolio risk. For position-specific details, use /positions.</i>")
+
+            await self._reply(update, "\n".join(message))
             
         except Exception as e:
             logger.error(f"Error in risk command: {e}", exc_info=True)