|
@@ -33,350 +33,450 @@ class InfoCommands:
|
|
|
self.trading_engine = trading_engine
|
|
|
self.notification_manager = notification_manager
|
|
|
|
|
|
- def _is_authorized(self, chat_id: str) -> bool:
|
|
|
- """Check if the chat ID is authorized."""
|
|
|
- return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
|
|
|
+ def _is_authorized(self, update: Update) -> bool:
|
|
|
+ """Check if the chat ID is authorized. Handles direct commands and callbacks."""
|
|
|
+ chat_id = None
|
|
|
+ if update.effective_chat: # For direct commands
|
|
|
+ chat_id = update.effective_chat.id
|
|
|
+ elif update.callback_query and update.callback_query.message: # For callback queries
|
|
|
+ chat_id = update.callback_query.message.chat_id
|
|
|
+
|
|
|
+ if not chat_id:
|
|
|
+ logger.warning("Could not determine chat_id for authorization in InfoCommands.")
|
|
|
+ return False
|
|
|
+
|
|
|
+ authorized = str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
|
|
|
+ if not authorized:
|
|
|
+ logger.warning(f"Unauthorized access attempt in InfoCommands by chat_id: {chat_id}")
|
|
|
+ return authorized
|
|
|
|
|
|
async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /balance command."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
- 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))
|
|
|
+ reply_method = None
|
|
|
+ if update.callback_query:
|
|
|
+ reply_method = update.callback_query.message.reply_text
|
|
|
+ elif update.message:
|
|
|
+ reply_method = update.message.reply_text
|
|
|
+ else:
|
|
|
+ logger.error("balance_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.")
|
|
|
+ return
|
|
|
+
|
|
|
+ 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}")
|
|
|
|
|
|
- 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"
|
|
|
+ # CCXT balance structure includes 'free', 'used', and 'total'
|
|
|
+ total_balance = balance.get('total', {})
|
|
|
+ free_balance = balance.get('free', {})
|
|
|
+ used_balance = balance.get('used', {})
|
|
|
|
|
|
- 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))
|
|
|
+ # 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>{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"
|
|
|
-
|
|
|
- # Portfolio summary
|
|
|
- usdc_balance = float(total_balance.get('USDC', 0))
|
|
|
- stats = self.trading_engine.get_stats()
|
|
|
- if stats:
|
|
|
- basic_stats = stats.get_basic_stats()
|
|
|
- initial_balance = basic_stats.get('initial_balance', usdc_balance)
|
|
|
- pnl = usdc_balance - initial_balance
|
|
|
- pnl_percent = (pnl / initial_balance * 100) if initial_balance > 0 else 0
|
|
|
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
+ 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
|
|
|
|
|
|
- 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"
|
|
|
-
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=balance_text, parse_mode='HTML')
|
|
|
- else:
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch balance information")
|
|
|
+ # 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"
|
|
|
+
|
|
|
+ # Portfolio summary
|
|
|
+ usdc_balance = float(total_balance.get('USDC', 0))
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if stats:
|
|
|
+ basic_stats = stats.get_basic_stats()
|
|
|
+ initial_balance = basic_stats.get('initial_balance', usdc_balance)
|
|
|
+ pnl = usdc_balance - 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"
|
|
|
+
|
|
|
+ trading_engine_active = "✅ Active" if self.trading_engine else "❌ Inactive (Error)"
|
|
|
+
|
|
|
+ # 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')}
|
|
|
+ """
|
|
|
+
|
|
|
+ await reply_method(text=balance_text.strip(), parse_mode='HTML')
|
|
|
+ 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}")
|
|
|
+ await reply_method(text="❌ Error retrieving balance information.", parse_mode='HTML')
|
|
|
|
|
|
async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /positions command."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
- # 🧹 PHASE 4: Use unified trades table as the single source of truth
|
|
|
- stats = self.trading_engine.get_stats()
|
|
|
- if not stats:
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Trading statistics not available.")
|
|
|
+ reply_method = None
|
|
|
+ if update.callback_query:
|
|
|
+ reply_method = update.callback_query.message.reply_text
|
|
|
+ elif update.message:
|
|
|
+ reply_method = update.message.reply_text
|
|
|
+ else:
|
|
|
+ logger.error("positions_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.")
|
|
|
return
|
|
|
|
|
|
- # 🆕 AUTO-SYNC logic removed as per user request.
|
|
|
- # Assuming heartbeat updates the DB sufficiently.
|
|
|
- sync_msg = ""
|
|
|
-
|
|
|
- # Get open positions from unified trades table
|
|
|
- open_positions = stats.get_open_positions()
|
|
|
-
|
|
|
- positions_text = f"📈 <b>Open Positions</b>\n\n{sync_msg}" # sync_msg will be empty
|
|
|
-
|
|
|
- if open_positions:
|
|
|
- total_unrealized = 0
|
|
|
- total_position_value = 0
|
|
|
-
|
|
|
- # Removed: fresh_exchange_positions = self.trading_engine.get_positions() or []
|
|
|
- # Removed: exchange_data_map = {pos.get('symbol'): pos for pos in fresh_exchange_positions}
|
|
|
-
|
|
|
- for position_trade in open_positions:
|
|
|
- symbol = position_trade['symbol']
|
|
|
- # base_asset is the asset being traded, quote_asset is the settlement currency (usually USDC)
|
|
|
- base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- # quote_asset = symbol.split('/')[1] if '/' in symbol else "USDC" # Not strictly needed for display here
|
|
|
-
|
|
|
- position_side = position_trade['position_side'] # 'long' or 'short'
|
|
|
- entry_price = position_trade['entry_price']
|
|
|
- current_amount = position_trade['current_position_size'] # This is the size of the position
|
|
|
- abs_current_amount = abs(current_amount)
|
|
|
- trade_type = position_trade.get('trade_type', 'manual') # Default to manual if not specified
|
|
|
-
|
|
|
- mark_price = position_trade.get('mark_price', entry_price) # Default to entry if not available
|
|
|
+ try:
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ await reply_method(text="❌ Trading statistics not available.", parse_mode='HTML')
|
|
|
+ return
|
|
|
+
|
|
|
+ # 🆕 AUTO-SYNC logic removed as per user request.
|
|
|
+ # Assuming heartbeat updates the DB sufficiently.
|
|
|
+ sync_msg = ""
|
|
|
+
|
|
|
+ # Get open positions from unified trades table
|
|
|
+ open_positions = stats.get_open_positions()
|
|
|
+
|
|
|
+ positions_text = f"📈 <b>Open Positions</b>\n\n{sync_msg}" # sync_msg will be empty
|
|
|
+
|
|
|
+ if open_positions:
|
|
|
+ total_unrealized = 0
|
|
|
+ total_position_value = 0
|
|
|
|
|
|
- # Calculate unrealized PnL
|
|
|
- unrealized_pnl = position_trade.get('unrealized_pnl') # Prefer DB value if up-to-date
|
|
|
- if unrealized_pnl is None: # Calculate if not directly available from DB
|
|
|
- if position_side == 'long':
|
|
|
- unrealized_pnl = (mark_price - entry_price) * abs_current_amount
|
|
|
- else: # Short position
|
|
|
- unrealized_pnl = (entry_price - mark_price) * abs_current_amount
|
|
|
- unrealized_pnl = unrealized_pnl or 0.0 # Ensure it's not None for calculations
|
|
|
+ for position_trade in open_positions:
|
|
|
+ symbol = position_trade['symbol']
|
|
|
+ # base_asset is the asset being traded, quote_asset is the settlement currency (usually USDC)
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ # quote_asset = symbol.split('/')[1] if '/' in symbol else "USDC" # Not strictly needed for display here
|
|
|
|
|
|
- # Tiered P&L Percentage Calculation
|
|
|
- pnl_percentage = 0.0
|
|
|
- exchange_pnl_percentage = position_trade.get('unrealized_pnl_percentage') # From exchange, e.g., 50.5 for 50.5%
|
|
|
- margin_used = position_trade.get('margin_used')
|
|
|
+ position_side = position_trade['position_side'] # 'long' or 'short'
|
|
|
+ entry_price = position_trade['entry_price']
|
|
|
+ current_amount = position_trade['current_position_size'] # This is the size of the position
|
|
|
+ abs_current_amount = abs(current_amount)
|
|
|
+ trade_type = position_trade.get('trade_type', 'manual') # Default to manual if not specified
|
|
|
+
|
|
|
+ mark_price = position_trade.get('mark_price', entry_price) # Default to entry if not available
|
|
|
+
|
|
|
+ # Calculate unrealized PnL
|
|
|
+ unrealized_pnl = position_trade.get('unrealized_pnl') # Prefer DB value if up-to-date
|
|
|
+ if unrealized_pnl is None: # Calculate if not directly available from DB
|
|
|
+ if position_side == 'long':
|
|
|
+ unrealized_pnl = (mark_price - entry_price) * abs_current_amount
|
|
|
+ else: # Short position
|
|
|
+ unrealized_pnl = (entry_price - mark_price) * abs_current_amount
|
|
|
+ unrealized_pnl = unrealized_pnl or 0.0 # Ensure it's not None for calculations
|
|
|
|
|
|
- if exchange_pnl_percentage is not None:
|
|
|
- pnl_percentage = exchange_pnl_percentage
|
|
|
- elif margin_used is not None and margin_used > 0 and unrealized_pnl != 0:
|
|
|
- pnl_percentage = (unrealized_pnl / margin_used) * 100
|
|
|
- elif entry_price != 0 and abs_current_amount != 0 and unrealized_pnl != 0:
|
|
|
- initial_value = entry_price * abs_current_amount
|
|
|
- pnl_percentage = (unrealized_pnl / initial_value) * 100
|
|
|
- # else pnl_percentage remains 0.0
|
|
|
+ # Tiered P&L Percentage Calculation
|
|
|
+ pnl_percentage = 0.0
|
|
|
+ exchange_pnl_percentage = position_trade.get('unrealized_pnl_percentage') # From exchange, e.g., 50.5 for 50.5%
|
|
|
+ margin_used = position_trade.get('margin_used')
|
|
|
|
|
|
- # Add to totals
|
|
|
- current_pos_value_at_mark = abs_current_amount * mark_price
|
|
|
- total_position_value += current_pos_value_at_mark
|
|
|
- total_unrealized += unrealized_pnl
|
|
|
-
|
|
|
- # --- Position Header Formatting (Emoji, Direction, Leverage) ---
|
|
|
- pos_emoji = ""
|
|
|
- direction_text = ""
|
|
|
- if position_side == 'long':
|
|
|
- pos_emoji = "🟢"
|
|
|
- direction_text = "LONG"
|
|
|
- else: # Short position
|
|
|
- pos_emoji = "🔴"
|
|
|
- direction_text = "SHORT"
|
|
|
-
|
|
|
- leverage = position_trade.get('leverage')
|
|
|
- if leverage is not None:
|
|
|
- try:
|
|
|
- leverage_val = float(leverage)
|
|
|
- leverage_str = f"x{leverage_val:.1f}".rstrip('0').rstrip('.') if '.' in f"{leverage_val:.1f}" else f"x{int(leverage_val)}"
|
|
|
- direction_text = f"{direction_text} {leverage_str}"
|
|
|
- except ValueError:
|
|
|
- logger.warning(f"Could not parse leverage value: {leverage} for {symbol}")
|
|
|
+ if exchange_pnl_percentage is not None:
|
|
|
+ pnl_percentage = exchange_pnl_percentage
|
|
|
+ elif margin_used is not None and margin_used > 0 and unrealized_pnl != 0:
|
|
|
+ pnl_percentage = (unrealized_pnl / margin_used) * 100
|
|
|
+ elif entry_price != 0 and abs_current_amount != 0 and unrealized_pnl != 0:
|
|
|
+ initial_value = entry_price * abs_current_amount
|
|
|
+ pnl_percentage = (unrealized_pnl / initial_value) * 100
|
|
|
+ # else pnl_percentage remains 0.0
|
|
|
|
|
|
- # --- Format Output String ---
|
|
|
- # Get token info for formatting prices
|
|
|
- # Assuming get_formatter() is available and provides necessary precision
|
|
|
- formatter = get_formatter() # Keep using if it wraps these precisions
|
|
|
+ # Add to totals
|
|
|
+ current_pos_value_at_mark = abs_current_amount * mark_price
|
|
|
+ total_position_value += current_pos_value_at_mark
|
|
|
+ total_unrealized += unrealized_pnl
|
|
|
+
|
|
|
+ # --- Position Header Formatting (Emoji, Direction, Leverage) ---
|
|
|
+ pos_emoji = ""
|
|
|
+ direction_text = ""
|
|
|
+ if position_side == 'long':
|
|
|
+ pos_emoji = "🟢"
|
|
|
+ direction_text = "LONG"
|
|
|
+ else: # Short position
|
|
|
+ pos_emoji = "🔴"
|
|
|
+ direction_text = "SHORT"
|
|
|
+
|
|
|
+ leverage = position_trade.get('leverage')
|
|
|
+ if leverage is not None:
|
|
|
+ try:
|
|
|
+ leverage_val = float(leverage)
|
|
|
+ leverage_str = f"x{leverage_val:.1f}".rstrip('0').rstrip('.') if '.' in f"{leverage_val:.1f}" else f"x{int(leverage_val)}"
|
|
|
+ direction_text = f"{direction_text} {leverage_str}"
|
|
|
+ except ValueError:
|
|
|
+ logger.warning(f"Could not parse leverage value: {leverage} for {symbol}")
|
|
|
|
|
|
- # Get price precisions
|
|
|
- entry_price_str = formatter.format_price_with_symbol(entry_price, base_asset)
|
|
|
- mark_price_str = formatter.format_price_with_symbol(mark_price, base_asset)
|
|
|
+ # --- Format Output String ---
|
|
|
+ # Get token info for formatting prices
|
|
|
+ # Assuming get_formatter() is available and provides necessary precision
|
|
|
+ formatter = get_formatter() # Keep using if it wraps these precisions
|
|
|
|
|
|
- # Get amount precision for position size
|
|
|
- # base_precision = int(token_info.get('precision', {}).get('amount', 6) if token_info and token_info.get('precision') else 6) # Old way
|
|
|
- # No longer need to fetch token_info separately for base_precision
|
|
|
- # The formatter now handles amount precision directly.
|
|
|
- size_str = formatter.format_amount(abs_current_amount, base_asset)
|
|
|
+ # Get price precisions
|
|
|
+ entry_price_str = formatter.format_price_with_symbol(entry_price, base_asset)
|
|
|
+ mark_price_str = formatter.format_price_with_symbol(mark_price, base_asset)
|
|
|
|
|
|
- type_indicator = ""
|
|
|
- # Determine type_indicator based on trade_lifecycle_id or trade_type
|
|
|
- if position_trade.get('trade_lifecycle_id'): # Primary indicator for bot managed
|
|
|
- type_indicator = " 🤖"
|
|
|
- elif trade_type == 'external':
|
|
|
- type_indicator = " 🔄"
|
|
|
-
|
|
|
- positions_text += f"{pos_emoji} <b>{base_asset} ({direction_text}){type_indicator}</b>\n"
|
|
|
- positions_text += f" 📏 Size: {size_str} {base_asset}\n" # Use the formatted size_str
|
|
|
- positions_text += f" 💰 Entry: {entry_price_str}\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"
|
|
|
-
|
|
|
- pnl_line_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
- positions_text += f" {pnl_line_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
|
|
|
-
|
|
|
- # Show exchange-provided risk data if available
|
|
|
- if margin_used is not None:
|
|
|
- positions_text += f" 💳 Margin Used: ${margin_used:,.2f}\n"
|
|
|
- if position_trade.get('liquidation_price') is not None and position_trade.get('liquidation_price') > 0:
|
|
|
- liq_price_str = formatter.format_price_with_symbol(position_trade.get('liquidation_price'), base_asset)
|
|
|
- positions_text += f" ⚠️ Liquidation: {liq_price_str}\n"
|
|
|
-
|
|
|
- # Show stop loss if linked
|
|
|
- if position_trade.get('stop_loss_price'):
|
|
|
- sl_price = position_trade['stop_loss_price']
|
|
|
- sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active"
|
|
|
- positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, base_asset)} ({sl_status})\n"
|
|
|
+ # Get amount precision for position size
|
|
|
+ # base_precision = int(token_info.get('precision', {}).get('amount', 6) if token_info and token_info.get('precision') else 6) # Old way
|
|
|
+ # No longer need to fetch token_info separately for base_precision
|
|
|
+ # The formatter now handles amount precision directly.
|
|
|
+ size_str = formatter.format_amount(abs_current_amount, base_asset)
|
|
|
+
|
|
|
+ type_indicator = ""
|
|
|
+ # Determine type_indicator based on trade_lifecycle_id or trade_type
|
|
|
+ if position_trade.get('trade_lifecycle_id'): # Primary indicator for bot managed
|
|
|
+ type_indicator = " 🤖"
|
|
|
+ elif trade_type == 'external':
|
|
|
+ type_indicator = " 🔄"
|
|
|
+
|
|
|
+ positions_text += f"{pos_emoji} <b>{base_asset} ({direction_text}){type_indicator}</b>\n"
|
|
|
+ positions_text += f" 📏 Size: {size_str} {base_asset}\n" # Use the formatted size_str
|
|
|
+ positions_text += f" 💰 Entry: {entry_price_str}\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"
|
|
|
+
|
|
|
+ pnl_line_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
+ positions_text += f" {pnl_line_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
|
|
|
+
|
|
|
+ # Show exchange-provided risk data if available
|
|
|
+ if margin_used is not None:
|
|
|
+ positions_text += f" 💳 Margin Used: ${margin_used:,.2f}\n"
|
|
|
+ if position_trade.get('liquidation_price') is not None and position_trade.get('liquidation_price') > 0:
|
|
|
+ liq_price_str = formatter.format_price_with_symbol(position_trade.get('liquidation_price'), base_asset)
|
|
|
+ positions_text += f" ⚠️ Liquidation: {liq_price_str}\n"
|
|
|
+
|
|
|
+ # Show stop loss if linked
|
|
|
+ if position_trade.get('stop_loss_price'):
|
|
|
+ sl_price = position_trade['stop_loss_price']
|
|
|
+ sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active"
|
|
|
+ positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, base_asset)} ({sl_status})\n"
|
|
|
+
|
|
|
+ # Show take profit if linked
|
|
|
+ if position_trade.get('take_profit_price'):
|
|
|
+ tp_price = position_trade['take_profit_price']
|
|
|
+ tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active"
|
|
|
+ positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, base_asset)} ({tp_status})\n"
|
|
|
+
|
|
|
+ positions_text += f" 🆔 Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
|
|
|
|
|
|
- # Show take profit if linked
|
|
|
- if position_trade.get('take_profit_price'):
|
|
|
- tp_price = position_trade['take_profit_price']
|
|
|
- tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active"
|
|
|
- positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, base_asset)} ({tp_status})\n"
|
|
|
+ # 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"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
|
|
|
+ positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
|
|
|
|
|
|
- positions_text += f" 🆔 Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
|
|
|
-
|
|
|
- # 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"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
|
|
|
- positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
|
|
|
+ else:
|
|
|
+ positions_text += "📭 No open positions\n\n"
|
|
|
+ positions_text += "💡 Use /long or /short to open a position"
|
|
|
|
|
|
- else:
|
|
|
- positions_text += "📭 No open positions\n\n"
|
|
|
- positions_text += "💡 Use /long or /short to open a position"
|
|
|
-
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML')
|
|
|
+ await reply_method(text=positions_text.strip(), parse_mode='HTML')
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error in positions command: {e}")
|
|
|
+ await reply_method(text="❌ Error retrieving position information.", parse_mode='HTML')
|
|
|
|
|
|
async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /orders command."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
- orders = self.trading_engine.get_orders()
|
|
|
+ reply_method = None
|
|
|
+ if update.callback_query:
|
|
|
+ reply_method = update.callback_query.message.reply_text
|
|
|
+ elif update.message:
|
|
|
+ reply_method = update.message.reply_text
|
|
|
+ else:
|
|
|
+ logger.error("orders_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.")
|
|
|
+ return
|
|
|
|
|
|
- if orders is not None:
|
|
|
- if len(orders) > 0:
|
|
|
- orders_text = "📋 <b>Open Orders</b>\n\n"
|
|
|
-
|
|
|
- # Group orders by symbol
|
|
|
- orders_by_symbol = {}
|
|
|
- for order in orders:
|
|
|
- symbol = order.get('symbol', '').replace('/USDC:USDC', '')
|
|
|
- if symbol not in orders_by_symbol:
|
|
|
- orders_by_symbol[symbol] = []
|
|
|
- orders_by_symbol[symbol].append(order)
|
|
|
-
|
|
|
- for symbol, symbol_orders in orders_by_symbol.items():
|
|
|
- orders_text += f"📊 <b>{symbol}</b>\n"
|
|
|
+ try:
|
|
|
+ orders = self.trading_engine.get_orders()
|
|
|
+
|
|
|
+ if orders is not None:
|
|
|
+ if len(orders) > 0:
|
|
|
+ orders_text = "📋 <b>Open Orders</b>\n\n"
|
|
|
|
|
|
- formatter = get_formatter()
|
|
|
- for order in symbol_orders:
|
|
|
- side = order.get('side', '').upper()
|
|
|
- amount = float(order.get('amount', 0))
|
|
|
- price = float(order.get('price', 0))
|
|
|
- order_type = order.get('type', 'unknown').title()
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
-
|
|
|
- # Order emoji
|
|
|
- side_emoji = "🟢" if side == "BUY" else "🔴"
|
|
|
-
|
|
|
- orders_text += f" {side_emoji} {side} {amount:.6f} @ {formatter.format_price_with_symbol(price, symbol)}\n"
|
|
|
- orders_text += f" 📋 Type: {order_type} | ID: {order_id}\n"
|
|
|
-
|
|
|
- # Check for pending stop losses linked to this order
|
|
|
- stats = self.trading_engine.get_stats()
|
|
|
- if stats:
|
|
|
- # Try to find this order in our database to get its bot_order_ref_id
|
|
|
- order_in_db = stats.get_order_by_exchange_id(order_id)
|
|
|
- if order_in_db:
|
|
|
- bot_ref_id = order_in_db.get('bot_order_ref_id')
|
|
|
- if bot_ref_id:
|
|
|
- # Look for pending stop losses with this order as parent
|
|
|
- pending_sls = stats.get_orders_by_status(
|
|
|
- status='pending_trigger',
|
|
|
- order_type_filter='stop_limit_trigger',
|
|
|
- parent_bot_order_ref_id=bot_ref_id
|
|
|
- )
|
|
|
-
|
|
|
- if pending_sls:
|
|
|
- sl_order = pending_sls[0] # Should only be one
|
|
|
- sl_price = sl_order.get('price', 0)
|
|
|
- sl_side = sl_order.get('side', '').upper()
|
|
|
- orders_text += f" 🛑 Pending SL: {sl_side} @ {formatter.format_price_with_symbol(sl_price, symbol)} (activates when filled)\n"
|
|
|
+ # Group orders by symbol
|
|
|
+ orders_by_symbol = {}
|
|
|
+ for order in orders:
|
|
|
+ symbol = order.get('symbol', '').replace('/USDC:USDC', '')
|
|
|
+ if symbol not in orders_by_symbol:
|
|
|
+ orders_by_symbol[symbol] = []
|
|
|
+ orders_by_symbol[symbol].append(order)
|
|
|
+
|
|
|
+ for symbol, symbol_orders in orders_by_symbol.items():
|
|
|
+ orders_text += f"📊 <b>{symbol}</b>\n"
|
|
|
|
|
|
- orders_text += "\n"
|
|
|
-
|
|
|
- orders_text += f"💼 <b>Total Orders:</b> {len(orders)}\n"
|
|
|
- orders_text += f"💡 Use /coo [token] to cancel orders"
|
|
|
+ formatter = get_formatter()
|
|
|
+ for order in symbol_orders:
|
|
|
+ side = order.get('side', '').upper()
|
|
|
+ amount = float(order.get('amount', 0))
|
|
|
+ price = float(order.get('price', 0))
|
|
|
+ order_type = order.get('type', 'unknown').title()
|
|
|
+ order_id = order.get('id', 'N/A')
|
|
|
+
|
|
|
+ # Order emoji
|
|
|
+ side_emoji = "🟢" if side == "BUY" else "🔴"
|
|
|
+
|
|
|
+ orders_text += f" {side_emoji} {side} {amount:.6f} @ {formatter.format_price_with_symbol(price, symbol)}\n"
|
|
|
+ orders_text += f" 📋 Type: {order_type} | ID: {order_id}\n"
|
|
|
+
|
|
|
+ # Check for pending stop losses linked to this order
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if stats:
|
|
|
+ # Try to find this order in our database to get its bot_order_ref_id
|
|
|
+ order_in_db = stats.get_order_by_exchange_id(order_id)
|
|
|
+ if order_in_db:
|
|
|
+ bot_ref_id = order_in_db.get('bot_order_ref_id')
|
|
|
+ if bot_ref_id:
|
|
|
+ # Look for pending stop losses with this order as parent
|
|
|
+ pending_sls = stats.get_orders_by_status(
|
|
|
+ status='pending_trigger',
|
|
|
+ order_type_filter='stop_limit_trigger',
|
|
|
+ parent_bot_order_ref_id=bot_ref_id
|
|
|
+ )
|
|
|
+
|
|
|
+ if pending_sls:
|
|
|
+ sl_order = pending_sls[0] # Should only be one
|
|
|
+ sl_price = sl_order.get('price', 0)
|
|
|
+ sl_side = sl_order.get('side', '').upper()
|
|
|
+ orders_text += f" 🛑 Pending SL: {sl_side} @ {formatter.format_price_with_symbol(sl_price, symbol)} (activates when filled)\n"
|
|
|
+
|
|
|
+ orders_text += "\n"
|
|
|
+
|
|
|
+ orders_text += f"💼 <b>Total Orders:</b> {len(orders)}\n"
|
|
|
+ orders_text += f"💡 Use /coo [token] to cancel orders"
|
|
|
+
|
|
|
+ else:
|
|
|
+ orders_text = "📋 <b>Open Orders</b>\n\n"
|
|
|
+ orders_text += "📭 No open orders\n\n"
|
|
|
+ orders_text += "💡 Use /long, /short, /sl, or /tp to create orders"
|
|
|
|
|
|
+ await reply_method(text=orders_text, parse_mode='HTML')
|
|
|
else:
|
|
|
- orders_text = "📋 <b>Open Orders</b>\n\n"
|
|
|
- orders_text += "📭 No open orders\n\n"
|
|
|
- orders_text += "💡 Use /long, /short, /sl, or /tp to create orders"
|
|
|
-
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=orders_text, parse_mode='HTML')
|
|
|
- else:
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch orders")
|
|
|
+ await reply_method(text="❌ Could not fetch orders", parse_mode='HTML')
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error in orders command: {e}")
|
|
|
+ await reply_method(text="❌ Error retrieving open orders.", parse_mode='HTML')
|
|
|
|
|
|
async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /stats command."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
- # 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)
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=stats_message, parse_mode='HTML')
|
|
|
+ reply_method = None
|
|
|
+ if update.callback_query:
|
|
|
+ reply_method = update.callback_query.message.reply_text
|
|
|
+ elif update.message:
|
|
|
+ reply_method = update.message.reply_text
|
|
|
else:
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
|
|
|
+ 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.")
|
|
|
+ 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)
|
|
|
+ await reply_method(text=stats_message, parse_mode='HTML')
|
|
|
+ else:
|
|
|
+ await reply_method(text="❌ Could not load trading statistics", parse_mode='HTML')
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error in stats command: {e}", exc_info=True) # Added exc_info for more details
|
|
|
+ await reply_method(text="❌ Error retrieving statistics.", parse_mode='HTML')
|
|
|
|
|
|
async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /trades command - Show recent trade history."""
|
|
|
+ chat_id = update.effective_chat.id
|
|
|
if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
+ reply_method = None
|
|
|
+ if update.callback_query:
|
|
|
+ reply_method = update.callback_query.message.reply_text
|
|
|
+ elif update.message:
|
|
|
+ reply_method = update.message.reply_text
|
|
|
+ else:
|
|
|
+ logger.error("trades_command: Cannot find a method to reply.")
|
|
|
+ # If it's a button click, the user won't see this if it's sent to Config.TELEGRAM_CHAT_ID
|
|
|
+ # unless that's the same chat.
|
|
|
+ # Consider editing the original message for callbacks if a reply_method can't be found.
|
|
|
+ # For now, let's try to send to the original chat if possible.
|
|
|
+ target_chat_id_for_error = chat_id if chat_id else Config.TELEGRAM_CHAT_ID
|
|
|
+ await context.bot.send_message(chat_id=target_chat_id_for_error, text="Error: Could not determine how to reply for /trades command.")
|
|
|
+ return
|
|
|
+
|
|
|
try:
|
|
|
stats = self.trading_engine.get_stats()
|
|
|
if not stats:
|
|
|
- await update.message.reply_text("❌ Trading statistics not available.", parse_mode='HTML')
|
|
|
+ await reply_method("❌ Trading statistics not available.", parse_mode='HTML')
|
|
|
return
|
|
|
|
|
|
# Get recent trades (limit to last 20)
|
|
|
recent_trades = stats.get_recent_trades(limit=20)
|
|
|
|
|
|
if not recent_trades:
|
|
|
- await update.message.reply_text("📊 <b>No trades found.</b>", parse_mode='HTML')
|
|
|
+ await reply_method("📊 <b>No trades found.</b>", parse_mode='HTML')
|
|
|
return
|
|
|
|
|
|
message = "📈 <b>Recent Trades (Last 20)</b>\n\n"
|
|
@@ -414,17 +514,16 @@ class InfoCommands:
|
|
|
message += f"{side_emoji} <b>{side}</b> {amount} {token} @ ${price:,.2f}\n"
|
|
|
message += f" {pnl_emoji} P&L: {pnl_str} | {time_str}\n\n"
|
|
|
|
|
|
- await update.message.reply_text(message, parse_mode='HTML')
|
|
|
+ await reply_method(message, parse_mode='HTML')
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in trades command: {e}")
|
|
|
- await update.message.reply_text("❌ Error retrieving trade history.", parse_mode='HTML')
|
|
|
+ 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(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
try:
|
|
@@ -511,8 +610,7 @@ class InfoCommands:
|
|
|
async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /market command."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
# Get token from arguments or use default
|
|
@@ -577,8 +675,7 @@ class InfoCommands:
|
|
|
async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /price command."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
# Get token from arguments or use default
|
|
@@ -622,8 +719,7 @@ class InfoCommands:
|
|
|
async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /performance command to show token performance ranking or detailed stats."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
try:
|
|
@@ -752,19 +848,19 @@ class InfoCommands:
|
|
|
# Detailed stats display
|
|
|
pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "🔴"
|
|
|
|
|
|
- total_pnl_str = formatter.format_price_with_symbol(token_stats['total_pnl'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- completed_volume_str = formatter.format_price_with_symbol(token_stats['completed_volume'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- expectancy_str = formatter.format_price_with_symbol(token_stats['expectancy'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- largest_win_str = formatter.format_price_with_symbol(token_stats['largest_win'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- largest_loss_str = formatter.format_price_with_symbol(token_stats['largest_loss'], quote_asset=Config.QUOTE_CURRENCY) # Assuming loss is positive number
|
|
|
- avg_win_str = formatter.format_price_with_symbol(token_stats['avg_win'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- avg_loss_str = formatter.format_price_with_symbol(token_stats['avg_loss'], quote_asset=Config.QUOTE_CURRENCY) # Assuming loss is positive number
|
|
|
+ total_pnl_str = formatter.format_price_with_symbol(token_stats['total_pnl'])
|
|
|
+ completed_volume_str = formatter.format_price_with_symbol(token_stats['completed_volume'])
|
|
|
+ expectancy_str = formatter.format_price_with_symbol(token_stats['expectancy'])
|
|
|
+ largest_win_str = formatter.format_price_with_symbol(token_stats['largest_win'])
|
|
|
+ largest_loss_str = formatter.format_price_with_symbol(token_stats['largest_loss']) # Assuming loss is positive number
|
|
|
+ avg_win_str = formatter.format_price_with_symbol(token_stats['avg_win'])
|
|
|
+ avg_loss_str = formatter.format_price_with_symbol(token_stats['avg_loss']) # Assuming loss is positive number
|
|
|
|
|
|
|
|
|
performance_text = f"""
|
|
|
📊 <b>{token} Detailed Performance</b>
|
|
|
|
|
|
-💰 <b>P&L Summary:</b>
|
|
|
+🎯 <b>P&L Summary:</b>
|
|
|
• {pnl_emoji} Total P&L: {total_pnl_str} ({token_stats['pnl_percentage']:+.2f}%)
|
|
|
• 💵 Total Volume: {completed_volume_str}
|
|
|
• 📈 Expectancy: {expectancy_str}
|
|
@@ -799,11 +895,11 @@ class InfoCommands:
|
|
|
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), quote_asset=Config.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), quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ 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"
|
|
@@ -815,8 +911,7 @@ class InfoCommands:
|
|
|
async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /daily command to show daily performance stats."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
try:
|
|
@@ -847,7 +942,7 @@ class InfoCommands:
|
|
|
for day_stats_item in daily_stats: # Renamed to avoid conflict
|
|
|
if day_stats_item['has_trades']:
|
|
|
pnl_emoji = "🟢" if day_stats_item['pnl'] >= 0 else "🔴"
|
|
|
- pnl_str = formatter.format_price_with_symbol(day_stats_item['pnl'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ pnl_str = formatter.format_price_with_symbol(day_stats_item['pnl'])
|
|
|
daily_text += f"📊 <b>{day_stats_item['date_formatted']}</b>\\n"
|
|
|
daily_text += f" {pnl_emoji} P&L: {pnl_str} ({day_stats_item['pnl_pct']:+.1f}%)\\n"
|
|
|
daily_text += f" 🔄 Trades: {day_stats_item['trades']}\\n\\n"
|
|
@@ -862,8 +957,8 @@ class InfoCommands:
|
|
|
if trading_days_count > 0:
|
|
|
avg_daily_pnl = total_pnl_all_days / trading_days_count
|
|
|
avg_pnl_emoji = "🟢" if avg_daily_pnl >= 0 else "🔴"
|
|
|
- total_pnl_all_days_str = formatter.format_price_with_symbol(total_pnl_all_days, quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- avg_daily_pnl_str = formatter.format_price_with_symbol(avg_daily_pnl, quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ total_pnl_all_days_str = formatter.format_price_with_symbol(total_pnl_all_days)
|
|
|
+ avg_daily_pnl_str = formatter.format_price_with_symbol(avg_daily_pnl)
|
|
|
|
|
|
daily_text += f"📈 <b>Period Summary:</b>\\n"
|
|
|
daily_text += f" {avg_pnl_emoji} Total P&L: {total_pnl_all_days_str}\\n"
|
|
@@ -881,8 +976,7 @@ class InfoCommands:
|
|
|
async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /weekly command to show weekly performance stats."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
try:
|
|
@@ -913,7 +1007,7 @@ class InfoCommands:
|
|
|
for week_stats_item in weekly_stats_list: # Renamed
|
|
|
if week_stats_item['has_trades']:
|
|
|
pnl_emoji = "🟢" if week_stats_item['pnl'] >= 0 else "🔴"
|
|
|
- pnl_str = formatter.format_price_with_symbol(week_stats_item['pnl'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ pnl_str = formatter.format_price_with_symbol(week_stats_item['pnl'])
|
|
|
weekly_text += f"📈 <b>{week_stats_item['week_formatted']}</b>\\n"
|
|
|
weekly_text += f" {pnl_emoji} P&L: {pnl_str} ({week_stats_item['pnl_pct']:+.1f}%)\\n"
|
|
|
weekly_text += f" 🔄 Trades: {week_stats_item['trades']}\\n\\n"
|
|
@@ -928,8 +1022,8 @@ class InfoCommands:
|
|
|
if trading_weeks_count > 0:
|
|
|
avg_weekly_pnl = total_pnl_all_weeks / trading_weeks_count
|
|
|
avg_pnl_emoji = "🟢" if avg_weekly_pnl >= 0 else "🔴"
|
|
|
- total_pnl_all_weeks_str = formatter.format_price_with_symbol(total_pnl_all_weeks, quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- avg_weekly_pnl_str = formatter.format_price_with_symbol(avg_weekly_pnl, quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ total_pnl_all_weeks_str = formatter.format_price_with_symbol(total_pnl_all_weeks)
|
|
|
+ avg_weekly_pnl_str = formatter.format_price_with_symbol(avg_weekly_pnl)
|
|
|
|
|
|
weekly_text += f"📅 <b>Period Summary:</b>\\n"
|
|
|
weekly_text += f" {avg_pnl_emoji} Total P&L: {total_pnl_all_weeks_str}\\n"
|
|
@@ -947,8 +1041,7 @@ class InfoCommands:
|
|
|
async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /monthly command to show monthly performance stats."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
try:
|
|
@@ -979,7 +1072,7 @@ class InfoCommands:
|
|
|
for month_stats_item in monthly_stats_list: # Renamed
|
|
|
if month_stats_item['has_trades']:
|
|
|
pnl_emoji = "🟢" if month_stats_item['pnl'] >= 0 else "🔴"
|
|
|
- pnl_str = formatter.format_price_with_symbol(month_stats_item['pnl'], quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ pnl_str = formatter.format_price_with_symbol(month_stats_item['pnl'])
|
|
|
monthly_text += f"📅 <b>{month_stats_item['month_formatted']}</b>\\n"
|
|
|
monthly_text += f" {pnl_emoji} P&L: {pnl_str} ({month_stats_item['pnl_pct']:+.1f}%)\\n"
|
|
|
monthly_text += f" 🔄 Trades: {month_stats_item['trades']}\\n\\n"
|
|
@@ -994,8 +1087,8 @@ class InfoCommands:
|
|
|
if trading_months_count > 0:
|
|
|
avg_monthly_pnl = total_pnl_all_months / trading_months_count
|
|
|
avg_pnl_emoji = "🟢" if avg_monthly_pnl >= 0 else "🔴"
|
|
|
- total_pnl_all_months_str = formatter.format_price_with_symbol(total_pnl_all_months, quote_asset=Config.QUOTE_CURRENCY)
|
|
|
- avg_monthly_pnl_str = formatter.format_price_with_symbol(avg_monthly_pnl, quote_asset=Config.QUOTE_CURRENCY)
|
|
|
+ total_pnl_all_months_str = formatter.format_price_with_symbol(total_pnl_all_months)
|
|
|
+ avg_monthly_pnl_str = formatter.format_price_with_symbol(avg_monthly_pnl)
|
|
|
|
|
|
monthly_text += f"📈 <b>Period Summary:</b>\\n"
|
|
|
monthly_text += f" {avg_pnl_emoji} Total P&L: {total_pnl_all_months_str}\\n"
|
|
@@ -1013,8 +1106,7 @@ class InfoCommands:
|
|
|
async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /risk command to show advanced risk metrics."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
try:
|
|
@@ -1040,7 +1132,7 @@ class InfoCommands:
|
|
|
"📭 <b>Insufficient Data</b>\n\n"
|
|
|
f"• Current completed trades: {basic_stats['completed_trades']}\n"
|
|
|
f"• Required for risk analysis: 2+ trades\n"
|
|
|
- f"• Daily balance snapshots: {len(stats.data.get('daily_balances', []))}\n\n"
|
|
|
+ f"• Daily balance snapshots: {stats.get_daily_balance_record_count()}\n\n"
|
|
|
"💡 <b>To enable risk analysis:</b>\n"
|
|
|
"• Complete more trades to generate returns data\n"
|
|
|
"• Bot automatically records daily balance snapshots\n"
|
|
@@ -1116,7 +1208,7 @@ class InfoCommands:
|
|
|
|
|
|
📈 <b>Data Based On:</b>
|
|
|
• Completed Trades: {basic_stats['completed_trades']}
|
|
|
-• Daily Balance Records: {len(stats.data.get('daily_balances', []))}
|
|
|
+• Daily Balance Records: {stats.get_daily_balance_record_count()}
|
|
|
• Trading Period: {basic_stats['days_active']} days
|
|
|
|
|
|
🔄 Use /stats for trading performance metrics
|
|
@@ -1132,8 +1224,7 @@ class InfoCommands:
|
|
|
async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /balance_adjustments command to show deposit/withdrawal history."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
try:
|
|
@@ -1214,8 +1305,7 @@ class InfoCommands:
|
|
|
async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /commands and /c command with quick action buttons."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
- if not self._is_authorized(chat_id):
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
commands_text = """
|