Przeglądaj źródła

Enhance positions and stats commands in TelegramTradingBot.

- Added display of unrealized P&L in the positions command, providing users with clearer insights into their current financial standing.
- Implemented a new method for formatting detailed token-specific statistics, improving the clarity and comprehensiveness of trading data presented to users.
- Updated overall stats command to support token-specific queries, enhancing user experience by allowing targeted data retrieval.
Carles Sentis 1 tydzień temu
rodzic
commit
7b61d54813
3 zmienionych plików z 171 dodań i 89 usunięć
  1. 2 0
      src/commands/info/positions.py
  2. 168 88
      src/commands/info/stats.py
  3. 1 1
      trading_bot.py

+ 2 - 0
src/commands/info/positions.py

@@ -131,6 +131,8 @@ class PositionsCommands(InfoCommandsBase):
                     positions_text += f"   🏦 Value: ${individual_position_value:,.2f}\n"
                     if mark_price != 0 and abs(mark_price - entry_price) > 1e-9:
                         positions_text += f"   📈 Mark: {mark_price_str}\n"
+                    pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
+                    positions_text += f"   {pnl_emoji} P&L: ${unrealized_pnl:,.2f}\n"
                     roe_emoji = "🟢" if roe_percentage >= 0 else "🔴"
                     positions_text += f"   {roe_emoji} ROE: {roe_percentage:+.2f}%\n"
 

+ 168 - 88
src/commands/info/stats.py

@@ -10,8 +10,77 @@ logger = logging.getLogger(__name__)
 class StatsCommands(InfoCommandsBase):
     """Handles all statistics-related commands."""
 
+    async def _format_token_specific_stats_message(self, token_stats_data: Dict[str, Any], token_name: str) -> str:
+        """Format detailed statistics for a specific token."""
+        if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
+            return (
+                f"📊 <b>{token_name} Statistics</b>\n\n"
+                f"📭 No trading data found for {token_name}.\n\n"
+                f"💡 To trade this token, try commands like:\n"
+                f"   <code>/long {token_name} 100</code>\n"
+                f"   <code>/short {token_name} 100</code>"
+            )
+
+        perf_summary = token_stats_data.get('performance_summary', {})
+        open_positions = token_stats_data.get('open_positions', [])
+        
+        parts = [f"📊 <b>{token_name.upper()} Detailed Statistics</b>\n"]
+
+        # Completed Trades Summary
+        parts.append("📈 <b>Completed Trades Summary:</b>")
+        if perf_summary.get('completed_trades', 0) > 0:
+            pnl_emoji = "🟢" if perf_summary.get('total_pnl', 0) >= 0 else "🔴"
+            entry_vol = perf_summary.get('completed_entry_volume', 0.0)
+            pnl_pct = (perf_summary.get('total_pnl', 0.0) / entry_vol * 100) if entry_vol > 0 else 0.0
+            
+            parts.append(f"• Total Completed: {perf_summary.get('completed_trades', 0)}")
+            parts.append(f"• {pnl_emoji} Realized P&L: ${perf_summary.get('total_pnl', 0.0):,.2f} ({pnl_pct:+.2f}%)")
+            parts.append(f"• Win Rate: {perf_summary.get('win_rate', 0.0):.1f}% ({perf_summary.get('total_wins', 0)}W / {perf_summary.get('total_losses', 0)}L)")
+            parts.append(f"• Profit Factor: {perf_summary.get('profit_factor', 0.0):.2f}")
+            parts.append(f"• Expectancy: ${perf_summary.get('expectancy', 0.0):,.2f}")
+            parts.append(f"• Avg Win: ${perf_summary.get('avg_win', 0.0):,.2f} | Avg Loss: ${perf_summary.get('avg_loss', 0.0):,.2f}")
+            parts.append(f"• Largest Win: ${perf_summary.get('largest_win', 0.0):,.2f} | Largest Loss: ${perf_summary.get('largest_loss', 0.0):,.2f}")
+            parts.append(f"• Entry Volume: ${perf_summary.get('completed_entry_volume', 0.0):,.2f}")
+            parts.append(f"• Exit Volume: ${perf_summary.get('completed_exit_volume', 0.0):,.2f}")
+            parts.append(f"• Average Trade Duration: {perf_summary.get('avg_trade_duration', 'N/A')}")
+            parts.append(f"• Cancelled Cycles: {perf_summary.get('total_cancelled', 0)}")
+        else:
+            parts.append("• No completed trades for this token yet.")
+        parts.append("")
+
+        # Open Positions for this token
+        parts.append("📉 <b>Current Open Positions:</b>")
+        if open_positions:
+            total_open_unrealized_pnl = token_stats_data.get('summary_total_unrealized_pnl', 0.0)
+            open_pnl_emoji = "🟢" if total_open_unrealized_pnl >= 0 else "🔴"
+            
+            for pos in open_positions:
+                pos_side_emoji = "🟢" if pos.get('side') == 'long' else "🔴"
+                pos_pnl_emoji = "🟢" if pos.get('unrealized_pnl', 0) >= 0 else "🔴"
+                opened_at_str = "N/A"
+                if pos.get('opened_at'):
+                    try:
+                        opened_at_dt = datetime.fromisoformat(pos['opened_at'])
+                        opened_at_str = opened_at_dt.strftime('%Y-%m-%d %H:%M')
+                    except:
+                        pass
+                
+                parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {abs(pos.get('amount',0))} {token_name}")
+                parts.append(f"    Entry: ${pos.get('entry_price',0):,.2f} | Mark: ${pos.get('mark_price',0):,.2f}")
+                parts.append(f"    {pos_pnl_emoji} Unrealized P&L: ${pos.get('unrealized_pnl',0):,.2f}")
+                parts.append(f"    Opened: {opened_at_str} | ID: ...{pos.get('lifecycle_id', '')[-6:]}")
+            parts.append(f"  {open_pnl_emoji} <b>Total Open P&L: ${total_open_unrealized_pnl:,.2f}</b>")
+        else:
+            parts.append("• No open positions for this token.")
+        parts.append("")
+
+        parts.append(f"📋 Open Orders (Exchange): {token_stats_data.get('current_open_orders_count', 0)}")
+        parts.append(f"💡 Use <code>/performance {token_name}</code> for another view including recent trades.")
+        
+        return "\n".join(parts)
+
     async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /stats command."""
+        """Handle the /stats command. Shows overall stats or stats for a specific token."""
         try:
             if not self._is_authorized(update):
                 await self._reply(update, "❌ Unauthorized access.")
@@ -22,93 +91,104 @@ class StatsCommands(InfoCommandsBase):
                 await self._reply(update, "❌ Trading stats not available.")
                 return
 
-            # Get trading stats
-            trading_stats = stats.get_basic_stats()
-            if not trading_stats:
-                await self._reply(update, "❌ Trading statistics not available.")
-                return
-
-            # Format stats text
-            stats_text = "📊 <b>Trading Statistics</b>\n\n"
-
-            # Add total trades
-            total_trades = trading_stats.get('total_trades', 0)
-            stats_text += f"📈 Total Trades: {total_trades}\n"
-
-            # Add winning trades
-            winning_trades = trading_stats.get('winning_trades', 0)
-            win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
-            stats_text += f"✅ Winning Trades: {winning_trades} ({win_rate:.2f}%)\n"
-
-            # Add losing trades
-            losing_trades = trading_stats.get('losing_trades', 0)
-            loss_rate = (losing_trades / total_trades * 100) if total_trades > 0 else 0
-            stats_text += f"❌ Losing Trades: {losing_trades} ({loss_rate:.2f}%)\n"
-
-            # Add total P&L
-            total_pnl = trading_stats.get('total_pnl', 0.0)
-            pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-            stats_text += f"{pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
-
-            # Add average P&L per trade
-            avg_pnl = total_pnl / total_trades if total_trades > 0 else 0
-            avg_pnl_emoji = "🟢" if avg_pnl >= 0 else "🔴"
-            stats_text += f"{avg_pnl_emoji} Average P&L per Trade: ${avg_pnl:,.2f}\n"
-
-            # Add ROE metrics
-            total_roe = trading_stats.get('total_roe', 0.0)
-            avg_roe = trading_stats.get('average_roe', 0.0)
-            best_roe = trading_stats.get('best_roe', 0.0)
-            worst_roe = trading_stats.get('worst_roe', 0.0)
-            best_roe_token = trading_stats.get('best_roe_token', 'N/A')
-            worst_roe_token = trading_stats.get('worst_roe_token', 'N/A')
-
-            roe_emoji = "🟢" if total_roe >= 0 else "🔴"
-            stats_text += f"{roe_emoji} Total ROE: {total_roe:+.2f}%\n"
-            stats_text += f"{roe_emoji} Average ROE per Trade: {avg_roe:+.2f}%\n"
-            stats_text += f"🏆 Best ROE: {best_roe:+.2f}% ({best_roe_token})\n"
-            stats_text += f"💔 Worst ROE: {worst_roe:+.2f}% ({worst_roe_token})\n"
-
-            # Add largest win and loss
-            largest_win = trading_stats.get('largest_win', 0.0)
-            largest_loss = trading_stats.get('largest_loss', 0.0)
-            stats_text += f"🏆 Largest Win: ${largest_win:,.2f}\n"
-            stats_text += f"💔 Largest Loss: ${largest_loss:,.2f}\n"
-
-            # Add average win and loss
-            avg_win = trading_stats.get('average_win', 0.0)
-            avg_loss = trading_stats.get('average_loss', 0.0)
-            stats_text += f"📈 Average Win: ${avg_win:,.2f}\n"
-            stats_text += f"📉 Average Loss: ${avg_loss:,.2f}\n"
-
-            # Add profit factor
-            profit_factor = trading_stats.get('profit_factor', 0.0)
-            stats_text += f"⚖️ Profit Factor: {profit_factor:.2f}\n"
-
-            # Add time-based stats
-            first_trade_time = trading_stats.get('first_trade_time')
-            if first_trade_time:
-                try:
-                    first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
-                    trading_duration = datetime.now(first_trade.tzinfo) - first_trade
-                    days = trading_duration.days
-                    hours = trading_duration.seconds // 3600
-                    stats_text += f"⏱️ Trading Duration: {days}d {hours}h\n"
-                except ValueError:
-                    logger.warning(f"Could not parse first_trade_time: {first_trade_time}")
-
-            # Add trades per day
-            if first_trade_time:
-                try:
-                    first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
-                    trading_duration = datetime.now(first_trade.tzinfo) - first_trade
-                    days = max(trading_duration.days, 1)  # Avoid division by zero
-                    trades_per_day = total_trades / days
-                    stats_text += f"📅 Trades per Day: {trades_per_day:.2f}\n"
-                except ValueError:
-                    pass
-
-            await self._reply(update, stats_text.strip())
+            if context.args and len(context.args) > 0:
+                # Token-specific stats
+                token_name = context.args[0].upper()
+                token_stats = stats.get_token_stats(token_name)
+                if not token_stats:
+                    await self._reply(update, f"❌ No trading data found for {token_name}.")
+                    return
+                
+                stats_message = await self._format_token_specific_stats_message(token_stats, token_name)
+                await self._reply(update, stats_message)
+            else:
+                # Overall stats
+                trading_stats = stats.get_basic_stats()
+                if not trading_stats:
+                    await self._reply(update, "❌ Trading statistics not available.")
+                    return
+
+                # Format stats text
+                stats_text = "📊 <b>Trading Statistics</b>\n\n"
+
+                # Add total trades
+                total_trades = trading_stats.get('total_trades', 0)
+                stats_text += f"📈 Total Trades: {total_trades}\n"
+
+                # Add winning trades
+                winning_trades = trading_stats.get('winning_trades', 0)
+                win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
+                stats_text += f"✅ Winning Trades: {winning_trades} ({win_rate:.2f}%)\n"
+
+                # Add losing trades
+                losing_trades = trading_stats.get('losing_trades', 0)
+                loss_rate = (losing_trades / total_trades * 100) if total_trades > 0 else 0
+                stats_text += f"❌ Losing Trades: {losing_trades} ({loss_rate:.2f}%)\n"
+
+                # Add total P&L
+                total_pnl = trading_stats.get('total_pnl', 0.0)
+                pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
+                stats_text += f"{pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
+
+                # Add average P&L per trade
+                avg_pnl = total_pnl / total_trades if total_trades > 0 else 0
+                avg_pnl_emoji = "🟢" if avg_pnl >= 0 else "🔴"
+                stats_text += f"{avg_pnl_emoji} Average P&L per Trade: ${avg_pnl:,.2f}\n"
+
+                # Add ROE metrics
+                total_roe = trading_stats.get('total_roe', 0.0)
+                avg_roe = trading_stats.get('average_roe', 0.0)
+                best_roe = trading_stats.get('best_roe', 0.0)
+                worst_roe = trading_stats.get('worst_roe', 0.0)
+                best_roe_token = trading_stats.get('best_roe_token', 'N/A')
+                worst_roe_token = trading_stats.get('worst_roe_token', 'N/A')
+
+                roe_emoji = "🟢" if total_roe >= 0 else "🔴"
+                stats_text += f"{roe_emoji} Total ROE: {total_roe:+.2f}%\n"
+                stats_text += f"{roe_emoji} Average ROE per Trade: {avg_roe:+.2f}%\n"
+                stats_text += f"🏆 Best ROE: {best_roe:+.2f}% ({best_roe_token})\n"
+                stats_text += f"💔 Worst ROE: {worst_roe:+.2f}% ({worst_roe_token})\n"
+
+                # Add largest win and loss
+                largest_win = trading_stats.get('largest_win', 0.0)
+                largest_loss = trading_stats.get('largest_loss', 0.0)
+                stats_text += f"🏆 Largest Win: ${largest_win:,.2f}\n"
+                stats_text += f"💔 Largest Loss: ${largest_loss:,.2f}\n"
+
+                # Add average win and loss
+                avg_win = trading_stats.get('average_win', 0.0)
+                avg_loss = trading_stats.get('average_loss', 0.0)
+                stats_text += f"📈 Average Win: ${avg_win:,.2f}\n"
+                stats_text += f"📉 Average Loss: ${avg_loss:,.2f}\n"
+
+                # Add profit factor
+                profit_factor = trading_stats.get('profit_factor', 0.0)
+                stats_text += f"⚖️ Profit Factor: {profit_factor:.2f}\n"
+
+                # Add time-based stats
+                first_trade_time = trading_stats.get('first_trade_time')
+                if first_trade_time:
+                    try:
+                        first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
+                        trading_duration = datetime.now(first_trade.tzinfo) - first_trade
+                        days = trading_duration.days
+                        hours = trading_duration.seconds // 3600
+                        stats_text += f"⏱️ Trading Duration: {days}d {hours}h\n"
+                    except ValueError:
+                        logger.warning(f"Could not parse first_trade_time: {first_trade_time}")
+
+                # Add trades per day
+                if first_trade_time:
+                    try:
+                        first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
+                        trading_duration = datetime.now(first_trade.tzinfo) - first_trade
+                        days = max(trading_duration.days, 1)  # Avoid division by zero
+                        trades_per_day = total_trades / days
+                        stats_text += f"📅 Trades per Day: {trades_per_day:.2f}\n"
+                    except ValueError:
+                        pass
+
+                await self._reply(update, stats_text.strip())
 
         except Exception as e:
             logger.error(f"Error in stats command: {e}")

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.4.186"
+BOT_VERSION = "2.4.187"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))