|
@@ -390,8 +390,75 @@ class InfoCommands:
|
|
|
logger.error(f"Error in orders command: {e}")
|
|
|
await reply_method(text="❌ Error retrieving open orders.", parse_mode='HTML')
|
|
|
|
|
|
+ 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: # Check summary_total_trades
|
|
|
+ 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 "🔴"
|
|
|
+ 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))} ({perf_summary.get('pnl_percentage', 0.0):+.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))} (Value per trade)")
|
|
|
+ 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"• 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)
|
|
|
+
|
|
|
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."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
if not self._is_authorized(update):
|
|
|
return
|
|
@@ -403,24 +470,41 @@ class InfoCommands:
|
|
|
reply_method = update.message.reply_text
|
|
|
else:
|
|
|
logger.error("stats_command: Cannot find a method to reply.")
|
|
|
- await context.bot.send_message(chat_id=Config.TELEGRAM_CHAT_ID, text="Error: Could not determine how to reply.")
|
|
|
+ # Attempt to send to context.bot.send_message if no direct reply method.
|
|
|
+ # It's possible this could be from a button click where original message is gone.
|
|
|
+ target_chat_id = chat_id if chat_id else Config.TELEGRAM_CHAT_ID
|
|
|
+ await context.bot.send_message(chat_id=target_chat_id, text="Error: Could not determine how to reply for /stats command.")
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
- # Get current balance for stats
|
|
|
- balance = self.trading_engine.get_balance()
|
|
|
- current_balance = 0
|
|
|
- if balance and balance.get('total'):
|
|
|
- current_balance = float(balance['total'].get('USDC', 0))
|
|
|
-
|
|
|
- stats = self.trading_engine.get_stats()
|
|
|
- if stats:
|
|
|
- stats_message = stats.format_stats_message(current_balance)
|
|
|
+ stats_manager = self.trading_engine.get_stats()
|
|
|
+ if not stats_manager:
|
|
|
+ await reply_method(text="❌ Could not load trading statistics manager.", parse_mode='HTML')
|
|
|
+ return
|
|
|
+
|
|
|
+ if context.args and len(context.args) > 0:
|
|
|
+ # Token-specific stats
|
|
|
+ token_name_arg = _normalize_token_case(context.args[0])
|
|
|
+ token_stats_data = stats_manager.get_token_detailed_stats(token_name_arg)
|
|
|
+
|
|
|
+ if not token_stats_data: # Should not happen if get_token_detailed_stats returns a dict
|
|
|
+ await reply_method(text=f"❌ Could not fetch detailed stats for {token_name_arg}.", parse_mode='HTML')
|
|
|
+ return
|
|
|
+
|
|
|
+ stats_message = self._format_token_specific_stats_message(token_stats_data, token_name_arg)
|
|
|
await reply_method(text=stats_message, parse_mode='HTML')
|
|
|
else:
|
|
|
- await reply_method(text="❌ Could not load trading statistics", parse_mode='HTML')
|
|
|
+ # Overall stats
|
|
|
+ balance = self.trading_engine.get_balance()
|
|
|
+ current_balance = 0
|
|
|
+ if balance and balance.get('total') and 'USDC' in balance['total']:
|
|
|
+ current_balance = float(balance['total']['USDC'])
|
|
|
+
|
|
|
+ stats_message = stats_manager.format_stats_message(current_balance)
|
|
|
+ await reply_method(text=stats_message, parse_mode='HTML')
|
|
|
+
|
|
|
except Exception as e:
|
|
|
- logger.error(f"Error in stats command: {e}", exc_info=True) # Added exc_info for more details
|
|
|
+ logger.error(f"Error in stats command: {e}", exc_info=True)
|
|
|
await reply_method(text="❌ Error retrieving statistics.", parse_mode='HTML')
|
|
|
|
|
|
async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
@@ -498,93 +582,6 @@ class InfoCommands:
|
|
|
logger.error(f"Error in trades command: {e}")
|
|
|
await reply_method("❌ Error retrieving trade history.", parse_mode='HTML')
|
|
|
|
|
|
- async def active_trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /active command to show active trades (using open positions)."""
|
|
|
- chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(update):
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- stats = self.trading_engine.get_stats()
|
|
|
- if not stats:
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
|
|
|
- return
|
|
|
-
|
|
|
- # Get open positions from unified trades table (current active trades)
|
|
|
- open_positions = stats.get_open_positions()
|
|
|
-
|
|
|
- if not open_positions:
|
|
|
- await context.bot.send_message(
|
|
|
- chat_id=chat_id,
|
|
|
- text="📊 <b>Active Positions</b>\n\n📭 No active positions found.\n\n💡 Use /long or /short to open positions.",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- message_text = "📊 <b>Active Positions</b>\n\n"
|
|
|
-
|
|
|
- # Show each position
|
|
|
- for position in open_positions:
|
|
|
- symbol = position['symbol']
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- position_side = position['position_side'] # 'long' or 'short'
|
|
|
- entry_price = position['entry_price']
|
|
|
- current_amount = position['current_position_size']
|
|
|
- trade_type = position.get('trade_type', 'manual')
|
|
|
-
|
|
|
- # Position emoji and formatting
|
|
|
- if position_side == 'long':
|
|
|
- pos_emoji = "🟢"
|
|
|
- direction = "LONG"
|
|
|
- else: # Short position
|
|
|
- pos_emoji = "🔴"
|
|
|
- direction = "SHORT"
|
|
|
-
|
|
|
- # Trade type indicator
|
|
|
- type_indicator = ""
|
|
|
- if trade_type == 'external':
|
|
|
- type_indicator = " 🔄" # External/synced position
|
|
|
- elif trade_type == 'bot':
|
|
|
- type_indicator = " 🤖" # Bot-created position
|
|
|
-
|
|
|
- message_text += f"{pos_emoji} <b>{token} ({direction}){type_indicator}</b>\n"
|
|
|
- message_text += f" 📏 Size: {abs(current_amount):.6f} {token}\n"
|
|
|
- message_text += f" 💰 Entry: ${entry_price:.4f}\n"
|
|
|
-
|
|
|
- # Show stop loss if linked
|
|
|
- if position.get('stop_loss_price'):
|
|
|
- sl_price = position['stop_loss_price']
|
|
|
- sl_status = "Pending" if not position.get('stop_loss_order_id') else "Active"
|
|
|
- message_text += f" 🛑 Stop Loss: ${sl_price:.4f} ({sl_status})\n"
|
|
|
-
|
|
|
- # Show take profit if linked
|
|
|
- if position.get('take_profit_price'):
|
|
|
- tp_price = position['take_profit_price']
|
|
|
- tp_status = "Pending" if not position.get('take_profit_order_id') else "Active"
|
|
|
- message_text += f" 🎯 Take Profit: ${tp_price:.4f} ({tp_status})\n"
|
|
|
-
|
|
|
- message_text += f" 🆔 Lifecycle ID: {position['trade_lifecycle_id'][:8]}\n\n"
|
|
|
-
|
|
|
- # Add summary
|
|
|
- total_positions = len(open_positions)
|
|
|
- bot_positions = len([p for p in open_positions if p.get('trade_type') == 'bot'])
|
|
|
- external_positions = len([p for p in open_positions if p.get('trade_type') == 'external'])
|
|
|
-
|
|
|
- message_text += f"📈 <b>Summary:</b>\n"
|
|
|
- message_text += f" Total: {total_positions} | "
|
|
|
- message_text += f"Bot: {bot_positions} | "
|
|
|
- message_text += f"External: {external_positions}\n\n"
|
|
|
-
|
|
|
- message_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
|
|
|
- message_text += f"💡 Use /positions for detailed P&L information"
|
|
|
-
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=message_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing active positions command: {str(e)}"
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
|
- logger.error(f"Error in active positions command: {e}")
|
|
|
-
|
|
|
async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /market command."""
|
|
|
chat_id = update.effective_chat.id
|