|
@@ -6,7 +6,7 @@ Info Commands - Handles information-related Telegram commands.
|
|
|
import logging
|
|
|
from datetime import datetime, timezone, timedelta
|
|
|
from typing import Optional, Dict, Any, List
|
|
|
-from telegram import Update
|
|
|
+from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
from telegram.ext import ContextTypes
|
|
|
|
|
|
from src.config.config import Config
|
|
@@ -69,95 +69,75 @@ class InfoCommands:
|
|
|
try:
|
|
|
balance = self.trading_engine.get_balance()
|
|
|
if balance:
|
|
|
- balance_text = "💰 <b>Account Balance</b>\n\n"
|
|
|
-
|
|
|
- # Debug: Show raw balance structure (can be removed after debugging)
|
|
|
- logger.debug(f"Raw balance data: {balance}")
|
|
|
-
|
|
|
- # CCXT balance structure includes 'free', 'used', and 'total'
|
|
|
- total_balance = balance.get('total', {})
|
|
|
- free_balance = balance.get('free', {})
|
|
|
- used_balance = balance.get('used', {})
|
|
|
-
|
|
|
- # Get total portfolio value
|
|
|
- total_portfolio_value = 0
|
|
|
-
|
|
|
- # Show USDC balance prominently
|
|
|
- if 'USDC' in total_balance:
|
|
|
- usdc_total = float(total_balance['USDC'])
|
|
|
- usdc_free = float(free_balance.get('USDC', 0))
|
|
|
- usdc_used = float(used_balance.get('USDC', 0))
|
|
|
-
|
|
|
- balance_text += f"💵 <b>USDC:</b>\n"
|
|
|
- balance_text += f" 📊 Total: ${usdc_total:,.2f}\n"
|
|
|
- balance_text += f" ✅ Available: ${usdc_free:,.2f}\n"
|
|
|
- balance_text += f" 🔒 In Use: ${usdc_used:,.2f}\n\n"
|
|
|
-
|
|
|
- total_portfolio_value += usdc_total
|
|
|
-
|
|
|
- # Show other non-zero balances
|
|
|
- other_assets = []
|
|
|
- for asset, amount in total_balance.items():
|
|
|
- if asset != 'USDC' and float(amount) > 0:
|
|
|
- other_assets.append((asset, float(amount)))
|
|
|
-
|
|
|
- if other_assets:
|
|
|
- balance_text += "📊 <b>Other Assets:</b>\n"
|
|
|
- for asset, amount in other_assets:
|
|
|
- free_amount = float(free_balance.get(asset, 0))
|
|
|
- used_amount = float(used_balance.get(asset, 0))
|
|
|
-
|
|
|
- balance_text += f"💵 <b>{asset}:</b>\n"
|
|
|
- balance_text += f" 📊 Total: {amount:.6f}\n"
|
|
|
- balance_text += f" ✅ Available: {free_amount:.6f}\n"
|
|
|
- balance_text += f" 🔒 In Use: {used_amount:.6f}\n\n"
|
|
|
+ usdc_total = 0.0
|
|
|
+ usdc_free = 0.0
|
|
|
+ usdc_used = 0.0
|
|
|
+
|
|
|
+ if 'USDC' in balance.get('total', {}):
|
|
|
+ usdc_total = float(balance['total']['USDC'])
|
|
|
+ usdc_free = float(balance.get('free', {}).get('USDC', 0))
|
|
|
+ usdc_used = float(balance.get('used', {}).get('USDC', 0))
|
|
|
+
|
|
|
+ balance_text_parts = [
|
|
|
+ f"💰 <b>Account Balance</b>\n",
|
|
|
+ f" 💵 Total USDC: ${usdc_total:,.2f}",
|
|
|
+ f" ✅ Available USDC: ${usdc_free:,.2f}",
|
|
|
+ f" 🔒 USDC In Use: ${usdc_used:,.2f}"
|
|
|
+ ]
|
|
|
+
|
|
|
+ other_assets_text = []
|
|
|
+ for asset, amount_val in balance.get('total', {}).items():
|
|
|
+ if asset != 'USDC' and float(amount_val) > 0:
|
|
|
+ free_amount = float(balance.get('free', {}).get(asset, 0))
|
|
|
+ other_assets_text.append(f" 🪙 {asset}: {float(amount_val):.6f} (Free: {free_amount:.6f})")
|
|
|
|
|
|
- # Portfolio summary
|
|
|
- usdc_balance = float(total_balance.get('USDC', 0))
|
|
|
+ if other_assets_text:
|
|
|
+ balance_text_parts.append("\n📊 <b>Other Assets:</b>")
|
|
|
+ balance_text_parts.extend(other_assets_text)
|
|
|
+
|
|
|
+ # Performance Metrics
|
|
|
stats = self.trading_engine.get_stats()
|
|
|
+ initial_balance = 0.0
|
|
|
+ pnl = 0.0
|
|
|
+ pnl_percent = 0.0
|
|
|
+ pnl_emoji = "⚪"
|
|
|
+
|
|
|
if stats:
|
|
|
basic_stats = stats.get_basic_stats()
|
|
|
- initial_balance = basic_stats.get('initial_balance', usdc_balance)
|
|
|
- pnl = usdc_balance - initial_balance
|
|
|
+ initial_balance = basic_stats.get('initial_balance', usdc_total) # Fallback to current total if no initial
|
|
|
+ if initial_balance is None: # Should not happen if basic_stats is fetched
|
|
|
+ initial_balance = usdc_total
|
|
|
+
|
|
|
+ pnl = usdc_total - initial_balance
|
|
|
pnl_percent = (pnl / initial_balance * 100) if initial_balance > 0 else 0
|
|
|
pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
-
|
|
|
- balance_text += f"💼 <b>Portfolio Summary:</b>\n"
|
|
|
- balance_text += f" 💰 Total Value: ${total_portfolio_value:,.2f}\n"
|
|
|
- balance_text += f" 🚀 Available for Trading: ${float(free_balance.get('USDC', 0)):,.2f}\n"
|
|
|
- balance_text += f" 🔒 In Active Use: ${float(used_balance.get('USDC', 0)):,.2f}\n\n"
|
|
|
- balance_text += f"📊 <b>Performance:</b>\n"
|
|
|
- balance_text += f" 💵 Initial: ${initial_balance:,.2f}\n"
|
|
|
- balance_text += f" {pnl_emoji} P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
|
|
|
|
|
|
+ balance_text_parts.append("\n📈 <b>Performance:</b>")
|
|
|
+ balance_text_parts.append(f" 💵 Initial Balance: ${initial_balance:,.2f}")
|
|
|
+ balance_text_parts.append(f" {pnl_emoji} Overall P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)")
|
|
|
+
|
|
|
+ # System Status
|
|
|
trading_engine_active = "✅ Active" if self.trading_engine else "❌ Inactive (Error)"
|
|
|
+ balance_text_parts.append("\n⚙️ <b>System Status:</b>")
|
|
|
+ balance_text_parts.append(f"• Trading Engine: {trading_engine_active}")
|
|
|
+ balance_text_parts.append(f"• Data Source: Exchange (Live)") # Balance is usually live
|
|
|
+ balance_text_parts.append(f"• Last Update: {datetime.now().strftime('%H:%M:%S')}")
|
|
|
|
|
|
- # Construct the balance message
|
|
|
- balance_text = f"""
|
|
|
-💰 <b>Account Balance & Info</b>
|
|
|
-
|
|
|
-💰 <b>Account Balance:</b>
|
|
|
- 💵 Total: ${usdc_total:,.2f}
|
|
|
- ✅ Available: ${usdc_free:,.2f}
|
|
|
- 🔒 In Use: ${usdc_used:,.2f}
|
|
|
-
|
|
|
-📊 <b>Portfolio Summary:</b>
|
|
|
- 💰 Total Value: ${total_portfolio_value:,.2f}
|
|
|
- 🚀 Available for Trading: ${float(free_balance.get('USDC', 0)):,.2f}
|
|
|
- 🔒 In Active Use: ${float(used_balance.get('USDC', 0)):,.2f}
|
|
|
-
|
|
|
-⚙️ <b>System Status:</b>
|
|
|
-• Trading Engine: {trading_engine_active}
|
|
|
-• Data Source: Cached (updated on heartbeat)
|
|
|
-
|
|
|
-⏰ Last Update: {datetime.now().strftime('%H:%M:%S')}
|
|
|
- """
|
|
|
+ final_message = "\n".join(balance_text_parts)
|
|
|
+
|
|
|
+ # Quick Actions Keyboard (same as before)
|
|
|
+ quick_actions = [
|
|
|
+ [InlineKeyboardButton("Positions (/p)", callback_data="/positions"), InlineKeyboardButton("Orders (/o)", callback_data="/orders")],
|
|
|
+ [InlineKeyboardButton("Trades (/tr)", callback_data="/trades"), InlineKeyboardButton("Stats (/st)", callback_data="/stats")],
|
|
|
+ [InlineKeyboardButton("Performance (/pf)", callback_data="/performance"), InlineKeyboardButton("Commands (/c)", callback_data="/commands")]
|
|
|
+ ]
|
|
|
+ quick_action_markup = InlineKeyboardMarkup(quick_actions)
|
|
|
|
|
|
- await reply_method(text=balance_text.strip(), parse_mode='HTML')
|
|
|
+ await reply_method(text=final_message.strip(), parse_mode='HTML', reply_markup=quick_action_markup)
|
|
|
else:
|
|
|
await reply_method(text="❌ Could not fetch balance information", parse_mode='HTML')
|
|
|
except Exception as e:
|
|
|
- logger.error(f"Error in balance command: {e}")
|
|
|
+ logger.error(f"Error in balance command: {e}", exc_info=True)
|
|
|
await reply_method(text="❌ Error retrieving balance information.", parse_mode='HTML')
|
|
|
|
|
|
async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
@@ -233,8 +213,11 @@ class InfoCommands:
|
|
|
# else pnl_percentage remains 0.0
|
|
|
|
|
|
# Add to totals
|
|
|
- current_pos_value_at_mark = abs_current_amount * mark_price
|
|
|
- total_position_value += current_pos_value_at_mark
|
|
|
+ individual_position_value = position_trade.get('position_value')
|
|
|
+ if individual_position_value is None: # Fallback if not in DB
|
|
|
+ individual_position_value = abs_current_amount * mark_price
|
|
|
+
|
|
|
+ total_position_value += individual_position_value
|
|
|
total_unrealized += unrealized_pnl
|
|
|
|
|
|
# --- Position Header Formatting (Emoji, Direction, Leverage) ---
|
|
@@ -282,6 +265,9 @@ class InfoCommands:
|
|
|
positions_text += f" 📏 Size: {size_str} {base_asset}\n" # Use the formatted size_str
|
|
|
positions_text += f" 💰 Entry: {entry_price_str}\n"
|
|
|
|
|
|
+ # Display individual position value
|
|
|
+ positions_text += f" 🏦 Value: ${individual_position_value:,.2f}\n"
|
|
|
+
|
|
|
if mark_price != 0 and abs(mark_price - entry_price) > 1e-9: # Only show mark if significantly different
|
|
|
positions_text += f" 📈 Mark: {mark_price_str}\n"
|
|
|
|
|
@@ -312,8 +298,8 @@ class InfoCommands:
|
|
|
# Portfolio summary
|
|
|
portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
|
|
|
positions_text += f"💼 <b>Total Portfolio:</b>\n"
|
|
|
- positions_text += f" 💵 Total Value: ${total_position_value:,.2f}\n"
|
|
|
- positions_text += f" {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n\n"
|
|
|
+ positions_text += f" 🏦 Total Positions Value: ${total_position_value:,.2f}\n"
|
|
|
+ positions_text += f" {portfolio_emoji} Total Unrealized P&L: ${total_unrealized:,.2f}\n\n"
|
|
|
positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
|
|
|
positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
|
|
|
|
|
@@ -624,7 +610,8 @@ class InfoCommands:
|
|
|
|
|
|
if market_data:
|
|
|
ticker = market_data.get('ticker', {})
|
|
|
-
|
|
|
+ logger.debug(f"Market command: Ticker data for {symbol}: {ticker}") # Log the ticker data
|
|
|
+
|
|
|
current_price = float(ticker.get('last', 0.0) or 0.0)
|
|
|
bid_price = float(ticker.get('bid', 0.0) or 0.0)
|
|
|
ask_price = float(ticker.get('ask', 0.0) or 0.0)
|
|
@@ -1316,8 +1303,6 @@ Tap any button below for instant access to bot functions:
|
|
|
💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
|
|
|
"""
|
|
|
|
|
|
- from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
-
|
|
|
keyboard = [
|
|
|
[
|
|
|
InlineKeyboardButton("💰 Balance", callback_data="/balance"),
|