|
@@ -43,33 +43,68 @@ class PerformanceCommands(InfoCommandsBase):
|
|
|
# Get performance data for all tokens
|
|
|
token_performance = stats.get_token_performance()
|
|
|
if not token_performance:
|
|
|
- await self._reply(update, "📊 No performance data available yet")
|
|
|
+ await self._reply(update,
|
|
|
+ "📊 <b>Token Performance</b>\n\n"
|
|
|
+ "📭 No trading data available yet.\n\n"
|
|
|
+ "💡 Performance tracking starts after your first completed trades.\n"
|
|
|
+ "Use /long or /short to start trading!",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
return
|
|
|
|
|
|
- # Sort tokens by P&L
|
|
|
- sorted_tokens = sorted(
|
|
|
- token_performance,
|
|
|
- key=lambda x: x.get('total_pnl', 0),
|
|
|
- reverse=True
|
|
|
- )
|
|
|
-
|
|
|
- formatter = get_formatter()
|
|
|
- performance_text = ["📊 <b>Token Performance Ranking</b>"]
|
|
|
-
|
|
|
- for data in sorted_tokens:
|
|
|
- token = data.get('token', 'Unknown')
|
|
|
- total_pnl = data.get('total_pnl', 0)
|
|
|
- total_trades = data.get('total_trades', 0)
|
|
|
- win_rate = data.get('win_rate', 0)
|
|
|
+ # Tokens are already sorted by P&L from get_token_performance
|
|
|
+ performance_text = "🏆 <b>Token Performance Ranking</b>\n"
|
|
|
+ performance_text += "<i>Ordered by Total P&L (Dollar Amount)</i>\n\n"
|
|
|
+
|
|
|
+ # Add ranking with emojis
|
|
|
+ for i, stats_data in enumerate(token_performance, 1):
|
|
|
+ # Ranking emoji
|
|
|
+ if i == 1:
|
|
|
+ rank_emoji = "🥇"
|
|
|
+ elif i == 2:
|
|
|
+ rank_emoji = "🥈"
|
|
|
+ elif i == 3:
|
|
|
+ rank_emoji = "🥉"
|
|
|
+ else:
|
|
|
+ rank_emoji = f"#{i}"
|
|
|
|
|
|
- if total_trades > 0:
|
|
|
- pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
- performance_text.append(
|
|
|
- f"\n<b>{token}</b>: {pnl_emoji} {formatter.format_price_with_symbol(total_pnl)} "
|
|
|
- f"({total_trades} trades, {win_rate:.1f}% win rate)"
|
|
|
- )
|
|
|
-
|
|
|
- await self._reply(update, "\n".join(performance_text))
|
|
|
+ total_pnl = stats_data.get('total_pnl', 0)
|
|
|
+ roe_percentage = stats_data.get('roe_percentage', 0)
|
|
|
+
|
|
|
+ # ROE emoji (shows performance efficiency)
|
|
|
+ roe_emoji = "🟢" if roe_percentage >= 0 else "🔴"
|
|
|
+
|
|
|
+ # P&L emoji (primary ranking metric)
|
|
|
+ pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
+
|
|
|
+ token_name = stats_data.get('token', 'N/A')
|
|
|
+ completed_trades = stats_data.get('total_trades', 0)
|
|
|
+
|
|
|
+ # Format the line - show both P&L (primary) and ROE (efficiency)
|
|
|
+ performance_text += f"{rank_emoji} <b>{token_name}</b>\n"
|
|
|
+ performance_text += f" {pnl_emoji} P&L: ${total_pnl:,.2f} | {roe_emoji} ROE: {roe_percentage:+.2f}%\n"
|
|
|
+ performance_text += f" 📊 Trades: {completed_trades}"
|
|
|
+
|
|
|
+ # Add win rate if there are completed trades
|
|
|
+ if completed_trades > 0:
|
|
|
+ win_rate = stats_data.get('win_rate', 0)
|
|
|
+ performance_text += f" | Win: {win_rate:.0f}%"
|
|
|
+
|
|
|
+ performance_text += "\n\n"
|
|
|
+
|
|
|
+ # Add summary
|
|
|
+ total_pnl = sum(stats_data.get('total_pnl', 0) for stats_data in token_performance)
|
|
|
+ total_trades = sum(stats_data.get('total_trades', 0) for stats_data in token_performance)
|
|
|
+ total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
+
|
|
|
+ performance_text += f"💼 <b>Portfolio Summary:</b>\n"
|
|
|
+ performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
|
|
|
+ performance_text += f" 📈 Tokens Traded: {len(token_performance)}\n"
|
|
|
+ performance_text += f" 🔄 Completed Trades: {total_trades}\n\n"
|
|
|
+
|
|
|
+ performance_text += f"💡 <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
|
|
|
+
|
|
|
+ await self._reply(update, performance_text.strip(), parse_mode='HTML')
|
|
|
|
|
|
except Exception as e:
|
|
|
error_message = f"❌ Error showing performance ranking: {str(e)}"
|
|
@@ -79,50 +114,182 @@ class PerformanceCommands(InfoCommandsBase):
|
|
|
async def _show_token_performance(self, update: Update, token: str, stats) -> None:
|
|
|
"""Show detailed performance for a specific token."""
|
|
|
try:
|
|
|
- # Get performance data for the token
|
|
|
- token_performance = stats.get_token_performance(token)
|
|
|
- if not token_performance:
|
|
|
- await self._reply(update, f"📊 No performance data available for {token}")
|
|
|
+ token_stats = stats.get_token_detailed_stats(token)
|
|
|
+ formatter = get_formatter()
|
|
|
+
|
|
|
+ # Check if token has any data
|
|
|
+ if token_stats.get('summary_total_trades', 0) == 0:
|
|
|
+ await self._reply(update,
|
|
|
+ f"📊 <b>{token} Performance</b>\n\n"
|
|
|
+ f"📭 No trading history found for {token}.\n\n"
|
|
|
+ f"💡 Start trading {token} with:\n"
|
|
|
+ f"• <code>/long {token} 100</code>\n"
|
|
|
+ f"• <code>/short {token} 100</code>\n\n"
|
|
|
+ f"🔄 Use <code>/performance</code> to see all token rankings.",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
return
|
|
|
+
|
|
|
+ # Check if there's a message (no completed trades)
|
|
|
+ perf_summary = token_stats.get('performance_summary', {})
|
|
|
+ if 'message' in token_stats and perf_summary.get('completed_trades', 0) == 0:
|
|
|
+ total_volume_str = formatter.format_price_with_symbol(token_stats.get('total_volume', 0), quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ await self._reply(update,
|
|
|
+ f"📊 <b>{token} Performance</b>\n\n"
|
|
|
+ f"{token_stats['message']}\n\n"
|
|
|
+ f"📈 <b>Current Activity:</b>\n"
|
|
|
+ f"• Total Trades: {token_stats.get('summary_total_trades', 0)}\n"
|
|
|
+ f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n"
|
|
|
+ f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n"
|
|
|
+ f"• Volume: {total_volume_str}\n\n"
|
|
|
+ f"💡 Complete some trades to see P&L statistics!\n"
|
|
|
+ f"🔄 Use <code>/performance</code> to see all token rankings.",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ # Detailed stats display - use performance_summary data
|
|
|
+ perf_summary = token_stats.get('performance_summary', {})
|
|
|
+ pnl_emoji = "🟢" if perf_summary.get('total_pnl', 0) >= 0 else "🔴"
|
|
|
+
|
|
|
+ total_pnl_str = formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0))
|
|
|
+ completed_volume_str = formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0))
|
|
|
+ expectancy_str = formatter.format_price_with_symbol(perf_summary.get('expectancy', 0))
|
|
|
+ largest_win_str = formatter.format_price_with_symbol(perf_summary.get('largest_win', 0))
|
|
|
+ largest_loss_str = formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0))
|
|
|
+ avg_win_str = formatter.format_price_with_symbol(perf_summary.get('avg_win', 0))
|
|
|
+ avg_loss_str = formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0))
|
|
|
+
|
|
|
+ # Calculate ROE (Return on Equity)
|
|
|
+ entry_vol = perf_summary.get('completed_entry_volume', 0)
|
|
|
+ roe_pct = (perf_summary.get('total_pnl', 0) / entry_vol * 100) if entry_vol > 0 else 0
|
|
|
+ roe_emoji = "🟢" if roe_pct >= 0 else "🔴"
|
|
|
|
|
|
- formatter = get_formatter()
|
|
|
- performance_text = [f"📊 <b>{token} Performance</b>"]
|
|
|
-
|
|
|
- # Add summary statistics
|
|
|
- total_pnl = token_performance.get('total_pnl', 0)
|
|
|
- total_trades = token_performance.get('total_trades', 0)
|
|
|
- win_rate = token_performance.get('win_rate', 0)
|
|
|
- avg_trade = token_performance.get('avg_trade', 0)
|
|
|
- largest_win = token_performance.get('largest_win', 0)
|
|
|
- largest_loss = token_performance.get('largest_loss', 0)
|
|
|
-
|
|
|
- pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
- performance_text.extend([
|
|
|
- f"\n<b>Summary:</b>",
|
|
|
- f"• Total P&L: {pnl_emoji} {formatter.format_price_with_symbol(total_pnl)}",
|
|
|
- f"• Total Trades: {total_trades}",
|
|
|
- f"• Win Rate: {win_rate:.1f}%",
|
|
|
- f"• Avg Trade: {formatter.format_price_with_symbol(avg_trade)}",
|
|
|
- f"• Largest Win: {formatter.format_price_with_symbol(largest_win)}",
|
|
|
- f"• Largest Loss: {formatter.format_price_with_symbol(largest_loss)}"
|
|
|
- ])
|
|
|
+ performance_text = f"""
|
|
|
+📊 <b>{token} Detailed Performance</b>
|
|
|
+
|
|
|
+🎯 <b>Performance Summary:</b>
|
|
|
+• {roe_emoji} ROE (Return on Equity): {roe_pct:+.2f}%
|
|
|
+• {pnl_emoji} Total P&L: {total_pnl_str}
|
|
|
+• 💵 Total Volume: {completed_volume_str}
|
|
|
+• 📈 Expectancy: {expectancy_str}
|
|
|
|
|
|
+📊 <b>Trading Activity:</b>
|
|
|
+• Total Trades: {token_stats.get('summary_total_trades', 0)}
|
|
|
+• Completed: {perf_summary.get('completed_trades', 0)}
|
|
|
+• Buy Orders: {token_stats.get('buy_trades', 0)}
|
|
|
+• Sell Orders: {token_stats.get('sell_trades', 0)}
|
|
|
+
|
|
|
+🏆 <b>Performance Metrics:</b>
|
|
|
+• Win Rate: {perf_summary.get('win_rate', 0):.1f}%
|
|
|
+• Profit Factor: {perf_summary.get('profit_factor', 0):.2f}
|
|
|
+• Wins: {perf_summary.get('total_wins', 0)} | Losses: {perf_summary.get('total_losses', 0)}
|
|
|
+
|
|
|
+💡 <b>Best/Worst:</b>
|
|
|
+• Largest Win: {largest_win_str}
|
|
|
+• Largest Loss: {largest_loss_str}
|
|
|
+• Avg Win: {avg_win_str}
|
|
|
+• Avg Loss: {avg_loss_str}
|
|
|
+ """
|
|
|
+
|
|
|
# Add recent trades if available
|
|
|
- recent_trades = token_performance.get('recent_trades', [])
|
|
|
- if recent_trades:
|
|
|
- performance_text.append("\n<b>Recent Trades:</b>")
|
|
|
- for trade in recent_trades[:5]: # Show last 5 trades
|
|
|
- side = trade.get('side', '').upper()
|
|
|
- pnl = trade.get('pnl', 0)
|
|
|
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
- performance_text.append(
|
|
|
- f"• {side}: {pnl_emoji} {formatter.format_price_with_symbol(pnl)} "
|
|
|
- f"({trade.get('entry_price', 0)} → {trade.get('exit_price', 0)})"
|
|
|
- )
|
|
|
-
|
|
|
- await self._reply(update, "\n".join(performance_text))
|
|
|
+ if token_stats.get('recent_trades'):
|
|
|
+ performance_text += f"\n🔄 <b>Recent Trades:</b>\n"
|
|
|
+ for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
|
|
|
+ trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
|
|
|
+ side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
|
|
|
+
|
|
|
+ # trade_symbol is required for format_price and format_amount
|
|
|
+ trade_symbol = trade.get('symbol', token) # Fallback to token if symbol not in trade dict
|
|
|
+ trade_base_asset = trade_symbol.split('/')[0] if '/' in trade_symbol else trade_symbol
|
|
|
+
|
|
|
+ # Formatting trade value. Assuming 'value' is in quote currency.
|
|
|
+ trade_value_str = formatter.format_price_with_symbol(trade.get('value', 0))
|
|
|
+
|
|
|
+ pnl_display_str = ""
|
|
|
+ if trade.get('pnl', 0) != 0:
|
|
|
+ trade_pnl_str = formatter.format_price_with_symbol(trade.get('pnl', 0))
|
|
|
+ pnl_display_str = f" | P&L: {trade_pnl_str}"
|
|
|
+
|
|
|
+ performance_text += f"• {side_emoji} {trade['side'].upper()} {trade_value_str} @ {trade_time}{pnl_display_str}\n"
|
|
|
+
|
|
|
+ performance_text += f"\n🔄 Use <code>/performance</code> to see all token rankings"
|
|
|
+
|
|
|
+ await self._reply(update, performance_text.strip(), parse_mode='HTML')
|
|
|
|
|
|
except Exception as e:
|
|
|
error_message = f"❌ Error showing token performance: {str(e)}"
|
|
|
await self._reply(update, error_message)
|
|
|
logger.error(f"Error in _show_token_performance: {e}")
|
|
|
+
|
|
|
+ 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."""
|
|
|
+ formatter = get_formatter()
|
|
|
+
|
|
|
+ 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 (from token_stats table)
|
|
|
+ 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 "🔴"
|
|
|
+ # Calculate PnL percentage from entry volume
|
|
|
+ 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: {formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0.0))} ({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: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
|
|
|
+ parts.append(f"• Avg Win: {formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
|
|
|
+ parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))} | Largest Loss: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}")
|
|
|
+ parts.append(f"• Entry Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
|
|
|
+ parts.append(f"• Exit Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_exit_volume', 0.0))}")
|
|
|
+ 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("") # Newline
|
|
|
+
|
|
|
+ # 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 # Keep N/A
|
|
|
+
|
|
|
+ parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {formatter.format_amount(abs(pos.get('amount',0)), token_name)} {token_name}")
|
|
|
+ parts.append(f" Entry: {formatter.format_price_with_symbol(pos.get('entry_price',0), token_name)} | Mark: {formatter.format_price_with_symbol(pos.get('mark_price',0), token_name)}")
|
|
|
+ parts.append(f" {pos_pnl_emoji} Unrealized P&L: {formatter.format_price_with_symbol(pos.get('unrealized_pnl',0))}")
|
|
|
+ parts.append(f" Opened: {opened_at_str} | ID: ...{pos.get('lifecycle_id', '')[-6:]}")
|
|
|
+ parts.append(f" {open_pnl_emoji} <b>Total Open P&L: {formatter.format_price_with_symbol(total_open_unrealized_pnl)}</b>")
|
|
|
+ else:
|
|
|
+ parts.append("• No open positions for this token.")
|
|
|
+ parts.append("") # Newline
|
|
|
+
|
|
|
+ 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)
|