|
@@ -1,1609 +0,0 @@
|
|
-#!/usr/bin/env python3
|
|
|
|
-"""
|
|
|
|
-Info Commands - Handles information-related Telegram commands.
|
|
|
|
-"""
|
|
|
|
-
|
|
|
|
-import logging
|
|
|
|
-import html # Added for escaping HTML characters
|
|
|
|
-from datetime import datetime, timezone, timedelta
|
|
|
|
-from typing import Optional, Dict, Any, List
|
|
|
|
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
|
-from telegram.ext import ContextTypes
|
|
|
|
-
|
|
|
|
-from src.config.config import Config
|
|
|
|
-from src.utils.token_display_formatter import format_price_with_symbol, get_formatter
|
|
|
|
-
|
|
|
|
-logger = logging.getLogger(__name__)
|
|
|
|
-
|
|
|
|
-def _normalize_token_case(token: str) -> str:
|
|
|
|
- """
|
|
|
|
- Normalize token case: if any characters are already uppercase, keep as-is.
|
|
|
|
- Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK.
|
|
|
|
- """
|
|
|
|
- # Check if any character is already uppercase
|
|
|
|
- if any(c.isupper() for c in token):
|
|
|
|
- return token # Keep original case for mixed-case tokens
|
|
|
|
- else:
|
|
|
|
- return token.upper() # Convert to uppercase for all-lowercase input
|
|
|
|
-
|
|
|
|
-class InfoCommands:
|
|
|
|
- """Handles all information-related Telegram commands."""
|
|
|
|
-
|
|
|
|
- def __init__(self, trading_engine, notification_manager=None):
|
|
|
|
- """Initialize with trading engine and notification manager."""
|
|
|
|
- self.trading_engine = trading_engine
|
|
|
|
- self.notification_manager = notification_manager
|
|
|
|
-
|
|
|
|
- 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(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("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:
|
|
|
|
- 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})")
|
|
|
|
-
|
|
|
|
- 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_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_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')}")
|
|
|
|
-
|
|
|
|
- final_message = "\n".join(balance_text_parts)
|
|
|
|
-
|
|
|
|
- await reply_method(text=final_message.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}", 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:
|
|
|
|
- """Handle the /positions command."""
|
|
|
|
- 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("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
|
|
|
|
-
|
|
|
|
- try:
|
|
|
|
- stats = self.trading_engine.get_stats()
|
|
|
|
- if not stats:
|
|
|
|
- await reply_method(text="❌ Trading statistics not available.", parse_mode='HTML')
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- # Get open positions from unified trades table
|
|
|
|
- open_positions = stats.get_open_positions()
|
|
|
|
-
|
|
|
|
- # Add position count to header
|
|
|
|
- position_count = len(open_positions) if open_positions else 0
|
|
|
|
- positions_text = f"📈 <b>Open Positions ({position_count})</b>\n\n"
|
|
|
|
-
|
|
|
|
- if open_positions:
|
|
|
|
- total_unrealized = 0
|
|
|
|
- total_position_value = 0
|
|
|
|
- total_margin_used = 0
|
|
|
|
-
|
|
|
|
- # Get exchange orders to detect external stop losses
|
|
|
|
- exchange_orders = self.trading_engine.get_orders() or []
|
|
|
|
-
|
|
|
|
- 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
|
|
|
|
-
|
|
|
|
- # Calculate position duration
|
|
|
|
- position_opened_at_str = position_trade.get('position_opened_at')
|
|
|
|
- duration_str = "N/A"
|
|
|
|
- if position_opened_at_str:
|
|
|
|
- try:
|
|
|
|
- opened_at_dt = datetime.fromisoformat(position_opened_at_str)
|
|
|
|
- if opened_at_dt.tzinfo is None: # Ensure timezone aware
|
|
|
|
- opened_at_dt = opened_at_dt.replace(tzinfo=timezone.utc)
|
|
|
|
- now_utc = datetime.now(timezone.utc)
|
|
|
|
- duration = now_utc - opened_at_dt
|
|
|
|
-
|
|
|
|
- days = duration.days
|
|
|
|
- hours, remainder = divmod(duration.seconds, 3600)
|
|
|
|
- minutes, _ = divmod(remainder, 60)
|
|
|
|
-
|
|
|
|
- parts = []
|
|
|
|
- if days > 0:
|
|
|
|
- parts.append(f"{days}d")
|
|
|
|
- if hours > 0:
|
|
|
|
- parts.append(f"{hours}h")
|
|
|
|
- if minutes > 0 or (days == 0 and hours == 0): # Show minutes if primary unit or others are zero
|
|
|
|
- parts.append(f"{minutes}m")
|
|
|
|
- duration_str = " ".join(parts) if parts else "0m"
|
|
|
|
- except ValueError:
|
|
|
|
- logger.warning(f"Could not parse position_opened_at: {position_opened_at_str} for {symbol}")
|
|
|
|
- duration_str = "Error"
|
|
|
|
-
|
|
|
|
- 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
|
|
|
|
-
|
|
|
|
- # P&L Percentage Calculation (use database data)
|
|
|
|
- pnl_percentage = 0.0
|
|
|
|
- db_pnl_percentage = position_trade.get('unrealized_pnl_percentage')
|
|
|
|
- margin_used = position_trade.get('margin_used')
|
|
|
|
-
|
|
|
|
- if db_pnl_percentage is not None:
|
|
|
|
- pnl_percentage = db_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
|
|
|
|
-
|
|
|
|
- # Add to totals
|
|
|
|
- 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
|
|
|
|
-
|
|
|
|
- # Add margin to total
|
|
|
|
- if margin_used is not None:
|
|
|
|
- total_margin_used += margin_used
|
|
|
|
-
|
|
|
|
- # --- 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}")
|
|
|
|
-
|
|
|
|
- # --- 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 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)
|
|
|
|
-
|
|
|
|
- # 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"
|
|
|
|
- positions_text += f" ⏳ Duration: {duration_str}\n" # Display position duration
|
|
|
|
-
|
|
|
|
- # 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"
|
|
|
|
-
|
|
|
|
- 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 in database
|
|
|
|
- if position_trade.get('stop_loss_price'):
|
|
|
|
- sl_price = position_trade['stop_loss_price']
|
|
|
|
- positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, base_asset)}\n"
|
|
|
|
-
|
|
|
|
- # Show take profit if linked in database
|
|
|
|
- if position_trade.get('take_profit_price'):
|
|
|
|
- tp_price = position_trade['take_profit_price']
|
|
|
|
- positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, base_asset)}\n"
|
|
|
|
-
|
|
|
|
- # Detect external/unlinked stop losses for this position
|
|
|
|
- external_sls = []
|
|
|
|
- for order in exchange_orders:
|
|
|
|
- try:
|
|
|
|
- order_symbol = order.get('symbol')
|
|
|
|
- order_side = order.get('side', '').lower()
|
|
|
|
- order_type = order.get('type', '').lower()
|
|
|
|
- order_price = float(order.get('price', 0))
|
|
|
|
- trigger_price = order.get('info', {}).get('triggerPrice')
|
|
|
|
- is_reduce_only = order.get('reduceOnly', False) or order.get('info', {}).get('reduceOnly', False)
|
|
|
|
- order_amount = float(order.get('amount', 0))
|
|
|
|
-
|
|
|
|
- # Check if this order could be a stop loss for this position
|
|
|
|
- if (order_symbol == symbol and is_reduce_only and
|
|
|
|
- abs(order_amount - abs_current_amount) < 0.01 * abs_current_amount):
|
|
|
|
-
|
|
|
|
- # Get trigger price (prefer triggerPrice over regular price)
|
|
|
|
- sl_trigger_price = 0
|
|
|
|
- if trigger_price:
|
|
|
|
- try:
|
|
|
|
- sl_trigger_price = float(trigger_price)
|
|
|
|
- except (ValueError, TypeError):
|
|
|
|
- pass
|
|
|
|
- if not sl_trigger_price and order_price > 0:
|
|
|
|
- sl_trigger_price = order_price
|
|
|
|
-
|
|
|
|
- # Check if it's positioned correctly relative to entry price
|
|
|
|
- is_valid_sl = False
|
|
|
|
- if position_side == 'long' and order_side == 'sell':
|
|
|
|
- # Long position: SL should be sell order below entry price
|
|
|
|
- if sl_trigger_price > 0 and sl_trigger_price < entry_price:
|
|
|
|
- is_valid_sl = True
|
|
|
|
- elif position_side == 'short' and order_side == 'buy':
|
|
|
|
- # Short position: SL should be buy order above entry price
|
|
|
|
- if sl_trigger_price > 0 and sl_trigger_price > entry_price:
|
|
|
|
- is_valid_sl = True
|
|
|
|
-
|
|
|
|
- # Check if it's already linked in database
|
|
|
|
- linked_sl_order_id = position_trade.get('stop_loss_order_id')
|
|
|
|
- is_already_linked = (linked_sl_order_id and linked_sl_order_id == order.get('id'))
|
|
|
|
-
|
|
|
|
- if is_valid_sl and not is_already_linked:
|
|
|
|
- external_sls.append(sl_trigger_price)
|
|
|
|
-
|
|
|
|
- except Exception as order_err:
|
|
|
|
- # Skip problematic orders
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- # Show external stop losses (unlinked)
|
|
|
|
- for ext_sl_price in external_sls:
|
|
|
|
- positions_text += f" 🛡️ External SL: {formatter.format_price_with_symbol(ext_sl_price, base_asset)}\n"
|
|
|
|
-
|
|
|
|
- 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 Positions Value: ${total_position_value:,.2f}\n"
|
|
|
|
- if total_margin_used > 0:
|
|
|
|
- positions_text += f" 💳 Total Margin Used: ${total_margin_used:,.2f}\n"
|
|
|
|
- leverage_ratio = total_position_value / total_margin_used if total_margin_used > 0 else 1.0
|
|
|
|
- positions_text += f" ⚖️ Portfolio Leverage: {leverage_ratio:.2f}x\n"
|
|
|
|
- positions_text += f" {portfolio_emoji} Total Unrealized P&L: ${total_unrealized:,.2f}\n"
|
|
|
|
- if total_margin_used > 0:
|
|
|
|
- margin_pnl_percentage = (total_unrealized / total_margin_used) * 100
|
|
|
|
- positions_text += f" 📊 Portfolio Return: {margin_pnl_percentage:+.2f}% (on margin)\n"
|
|
|
|
- positions_text += "\n"
|
|
|
|
- positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced • 🛡️ External SL\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"
|
|
|
|
-
|
|
|
|
- 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(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("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
|
|
|
|
-
|
|
|
|
- try:
|
|
|
|
- orders = self.trading_engine.get_orders()
|
|
|
|
- stats = self.trading_engine.get_stats()
|
|
|
|
-
|
|
|
|
- if orders is not None:
|
|
|
|
- if len(orders) > 0:
|
|
|
|
- orders_text = f"📋 <b>Open Orders ({len(orders)})</b>\n\n"
|
|
|
|
-
|
|
|
|
- # Group orders by symbol
|
|
|
|
- orders_by_symbol = {}
|
|
|
|
- # Keep track of parent_bot_order_ref_ids for pending SLs already displayed with an open parent order
|
|
|
|
- displayed_sl_parent_refs = set()
|
|
|
|
-
|
|
|
|
- for order in orders: # Iterate through orders from self.trading_engine.get_orders()
|
|
|
|
- symbol_display_key = order.get('symbol', '').replace('/USDC:USDC', '')
|
|
|
|
- if symbol_display_key not in orders_by_symbol:
|
|
|
|
- orders_by_symbol[symbol_display_key] = []
|
|
|
|
- orders_by_symbol[symbol_display_key].append(order) # Group by display key
|
|
|
|
-
|
|
|
|
- formatter = get_formatter() # Get formatter once
|
|
|
|
-
|
|
|
|
- for symbol, symbol_orders_list in orders_by_symbol.items():
|
|
|
|
- orders_text += f"📊 <b>{symbol}</b>\n"
|
|
|
|
-
|
|
|
|
- for order_data in symbol_orders_list: # This is an individual exchange order
|
|
|
|
- side = order_data.get('side', '').upper()
|
|
|
|
- amount = float(order_data.get('amount', 0))
|
|
|
|
- price = float(order_data.get('price', 0))
|
|
|
|
-
|
|
|
|
- # Check for trigger price (for stop loss orders)
|
|
|
|
- trigger_price = order_data.get('info', {}).get('triggerPrice')
|
|
|
|
- if trigger_price:
|
|
|
|
- try:
|
|
|
|
- price = float(trigger_price)
|
|
|
|
- except (ValueError, TypeError):
|
|
|
|
- pass # Keep original price if trigger price can't be parsed
|
|
|
|
-
|
|
|
|
- order_type = order_data.get('type', 'unknown').title()
|
|
|
|
- exchange_order_id = order_data.get('id', 'N/A') # Renamed for clarity
|
|
|
|
-
|
|
|
|
- side_emoji = "🟢" if side == "BUY" else "🔴"
|
|
|
|
-
|
|
|
|
- orders_text += f" {side_emoji} {side} {formatter.format_amount(amount, symbol)} @ {formatter.format_price_with_symbol(price, symbol)}\n"
|
|
|
|
- orders_text += f" 📋 Type: {order_type} | ID: {exchange_order_id}\n"
|
|
|
|
-
|
|
|
|
- # Check for pending SL in the trade lifecycle system
|
|
|
|
- if stats:
|
|
|
|
- # First check the old system for conceptual pending SLs
|
|
|
|
- order_in_db = stats.get_order_by_exchange_id(exchange_order_id)
|
|
|
|
- if order_in_db:
|
|
|
|
- bot_ref_id = order_in_db.get('bot_order_ref_id')
|
|
|
|
- if bot_ref_id:
|
|
|
|
- pending_sls = stats.get_orders_by_status(
|
|
|
|
- status='pending_activation',
|
|
|
|
- order_type_filter='pending_sl_activation',
|
|
|
|
- parent_bot_order_ref_id=bot_ref_id
|
|
|
|
- )
|
|
|
|
-
|
|
|
|
- if pending_sls:
|
|
|
|
- sl_order = pending_sls[0]
|
|
|
|
- sl_price = sl_order.get('price', 0)
|
|
|
|
- sl_conceptual_side = sl_order.get('side', '').upper()
|
|
|
|
-
|
|
|
|
- orders_text += f" ⏳ Pending SL Activation: {sl_conceptual_side} at {formatter.format_price_with_symbol(sl_price, symbol)}\n"
|
|
|
|
- orders_text += f" (Activates after main order fills)\n"
|
|
|
|
- displayed_sl_parent_refs.add(bot_ref_id)
|
|
|
|
-
|
|
|
|
- # Also check for pending SL in trade lifecycle (new system)
|
|
|
|
- lifecycle_manager = stats.trade_manager
|
|
|
|
- if lifecycle_manager:
|
|
|
|
- pending_trade = lifecycle_manager.get_lifecycle_by_entry_order_id(exchange_order_id, status='pending')
|
|
|
|
- if pending_trade and pending_trade.get('stop_loss_price'):
|
|
|
|
- sl_price = pending_trade['stop_loss_price']
|
|
|
|
- entry_side = pending_trade.get('side', '').lower()
|
|
|
|
- sl_side = 'SELL' if entry_side == 'buy' else 'BUY'
|
|
|
|
-
|
|
|
|
- orders_text += f" ⏳ Pending SL: {sl_side} at {formatter.format_price_with_symbol(sl_price, symbol)}\n"
|
|
|
|
- orders_text += f" (Activates when order fills)\n"
|
|
|
|
-
|
|
|
|
- orders_text += "\n"
|
|
|
|
-
|
|
|
|
- orders_text += f"💼 <b>Total Open Exchange Orders:</b> {len(orders)}\n"
|
|
|
|
-
|
|
|
|
- # Check for orphaned pending SLs from old system
|
|
|
|
- if stats:
|
|
|
|
- all_pending_sl_activations = stats.get_orders_by_status(
|
|
|
|
- status='pending_activation',
|
|
|
|
- order_type_filter='pending_sl_activation'
|
|
|
|
- )
|
|
|
|
-
|
|
|
|
- orphaned_pending_sls_to_display = []
|
|
|
|
- for sl_order_data in all_pending_sl_activations:
|
|
|
|
- parent_ref = sl_order_data.get('parent_bot_order_ref_id')
|
|
|
|
- if parent_ref not in displayed_sl_parent_refs:
|
|
|
|
- orphaned_pending_sls_to_display.append(sl_order_data)
|
|
|
|
-
|
|
|
|
- if orphaned_pending_sls_to_display:
|
|
|
|
- orders_text += "\n"
|
|
|
|
- orders_text += "⏳ <b>Pending SL Activations (Entry Order Assumed Filled/Closed)</b>\n\n"
|
|
|
|
-
|
|
|
|
- orphaned_sls_by_symbol_group = {}
|
|
|
|
- for sl_data in orphaned_pending_sls_to_display:
|
|
|
|
- sl_symbol_raw = sl_data.get('symbol', '')
|
|
|
|
- sl_symbol_display_key = sl_symbol_raw.replace('/USDC:USDC', '')
|
|
|
|
- if sl_symbol_display_key not in orphaned_sls_by_symbol_group:
|
|
|
|
- orphaned_sls_by_symbol_group[sl_symbol_display_key] = []
|
|
|
|
- orphaned_sls_by_symbol_group[sl_symbol_display_key].append(sl_data)
|
|
|
|
-
|
|
|
|
- for sl_sym_key, sl_list_items in orphaned_sls_by_symbol_group.items():
|
|
|
|
- orders_text += f"📊 <b>{sl_sym_key}</b>\n"
|
|
|
|
- for sl_item in sl_list_items:
|
|
|
|
- sl_price_val = sl_item.get('price', 0)
|
|
|
|
- sl_side_val = sl_item.get('side', '').upper()
|
|
|
|
- orders_text += f" ⏳ Pending SL: {sl_side_val} at {formatter.format_price_with_symbol(sl_price_val, sl_sym_key)}\n"
|
|
|
|
- orders_text += f" (Awaiting activation by bot)\n\n"
|
|
|
|
-
|
|
|
|
- orders_text += f"📦 <b>Total Pending Activations (Entry Filled):</b> {len(orphaned_pending_sls_to_display)}\n"
|
|
|
|
-
|
|
|
|
- orders_text += f"💡 Use /coo [token] to cancel orders"
|
|
|
|
-
|
|
|
|
- else:
|
|
|
|
- orders_text = "📋 <b>Open Orders (0)</b>\n\n"
|
|
|
|
- orders_text += "📭 No open orders\n\n"
|
|
|
|
-
|
|
|
|
- # Check for pending SLs from trade lifecycle even if no exchange orders
|
|
|
|
- if stats and stats.trade_manager:
|
|
|
|
- pending_sl_trades = stats.trade_manager.get_pending_stop_loss_activations()
|
|
|
|
-
|
|
|
|
- if pending_sl_trades:
|
|
|
|
- orders_text += "\n⏳ <b>Pending Stop Loss Activations</b>\n\n"
|
|
|
|
- formatter_for_empty = get_formatter()
|
|
|
|
-
|
|
|
|
- for trade in pending_sl_trades:
|
|
|
|
- symbol_raw = trade.get('symbol', '')
|
|
|
|
- symbol_display = symbol_raw.replace('/USDC:USDC', '')
|
|
|
|
- sl_price = trade.get('stop_loss_price', 0)
|
|
|
|
- entry_side = trade.get('side', '').lower()
|
|
|
|
- sl_side = 'SELL' if entry_side == 'buy' else 'BUY'
|
|
|
|
- lifecycle_id = trade.get('trade_lifecycle_id', '')[:8]
|
|
|
|
-
|
|
|
|
- orders_text += f"📊 <b>{symbol_display}</b>\n"
|
|
|
|
- orders_text += f" ⏳ Pending SL: {sl_side} at {formatter_for_empty.format_price_with_symbol(sl_price, symbol_display)}\n"
|
|
|
|
- orders_text += f" (Position opened, awaiting SL activation)\n"
|
|
|
|
- orders_text += f" Lifecycle: {lifecycle_id}\n\n"
|
|
|
|
-
|
|
|
|
- orders_text += f"📦 <b>Total Pending SL Activations:</b> {len(pending_sl_trades)}\n"
|
|
|
|
- else:
|
|
|
|
- # Check for purely conceptual pending SLs from old system
|
|
|
|
- all_pending_sl_activations_empty = stats.get_orders_by_status(
|
|
|
|
- status='pending_activation',
|
|
|
|
- order_type_filter='pending_sl_activation'
|
|
|
|
- )
|
|
|
|
- if all_pending_sl_activations_empty:
|
|
|
|
- orders_text += "\n⏳ <b>Pending SL Activations (Entry Order Assumed Filled/Closed)</b>\n\n"
|
|
|
|
- formatter_for_empty = get_formatter()
|
|
|
|
-
|
|
|
|
- orphaned_sls_by_symbol_group_empty = {}
|
|
|
|
- for sl_data_empty in all_pending_sl_activations_empty:
|
|
|
|
- sl_symbol_raw_empty = sl_data_empty.get('symbol', '')
|
|
|
|
- sl_symbol_display_key_empty = sl_symbol_raw_empty.replace('/USDC:USDC', '')
|
|
|
|
- if sl_symbol_display_key_empty not in orphaned_sls_by_symbol_group_empty:
|
|
|
|
- orphaned_sls_by_symbol_group_empty[sl_symbol_display_key_empty] = []
|
|
|
|
- orphaned_sls_by_symbol_group_empty[sl_symbol_display_key_empty].append(sl_data_empty)
|
|
|
|
-
|
|
|
|
- for sl_sym_key_empty, sl_list_items_empty in orphaned_sls_by_symbol_group_empty.items():
|
|
|
|
- orders_text += f"📊 <b>{sl_sym_key_empty}</b>\n"
|
|
|
|
- for sl_item_empty in sl_list_items_empty:
|
|
|
|
- sl_price_val_empty = sl_item_empty.get('price', 0)
|
|
|
|
- sl_side_val_empty = sl_item_empty.get('side', '').upper()
|
|
|
|
- orders_text += f" ⏳ Pending SL: {sl_side_val_empty} at {formatter_for_empty.format_price_with_symbol(sl_price_val_empty, sl_sym_key_empty)}\n"
|
|
|
|
- orders_text += f" (Awaiting activation by bot)\n\n"
|
|
|
|
- orders_text += f"📦 <b>Total Pending Activations (Entry Filled):</b> {len(all_pending_sl_activations_empty)}\n"
|
|
|
|
- else:
|
|
|
|
- orders_text += "💡 Use /long, /short, /sl, or /tp to create orders"
|
|
|
|
- else:
|
|
|
|
- orders_text += "💡 Use /long, /short, /sl, or /tp to create orders"
|
|
|
|
-
|
|
|
|
- await reply_method(text=orders_text, parse_mode='HTML')
|
|
|
|
- else:
|
|
|
|
- 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 _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 "🔴"
|
|
|
|
- # 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)
|
|
|
|
-
|
|
|
|
- async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
- """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
|
|
|
|
-
|
|
|
|
- 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("stats_command: Cannot find a method 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:
|
|
|
|
- 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])
|
|
|
|
-
|
|
|
|
- # Use the centralized formatting method from TradingStats
|
|
|
|
- stats_message = stats_manager.format_token_stats_message(token_name_arg)
|
|
|
|
- await reply_method(text=stats_message, parse_mode='HTML')
|
|
|
|
- else:
|
|
|
|
- # 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)
|
|
|
|
- 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 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 reply_method("📊 <b>No trades found.</b>", parse_mode='HTML')
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- message = "📈 <b>Recent Trades (Last 20)</b>\n\n"
|
|
|
|
-
|
|
|
|
- for trade in recent_trades:
|
|
|
|
- symbol = trade['symbol']
|
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] if ':' in symbol else symbol
|
|
|
|
- side = trade['side'].upper()
|
|
|
|
- amount = trade['amount']
|
|
|
|
- price = trade['price']
|
|
|
|
- timestamp = trade['timestamp']
|
|
|
|
- pnl = trade.get('realized_pnl', 0)
|
|
|
|
-
|
|
|
|
- # Format timestamp
|
|
|
|
- try:
|
|
|
|
- from datetime import datetime
|
|
|
|
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
|
|
- time_str = dt.strftime('%m/%d %H:%M')
|
|
|
|
- except:
|
|
|
|
- time_str = "Unknown"
|
|
|
|
-
|
|
|
|
- # PnL emoji and formatting
|
|
|
|
- if pnl > 0:
|
|
|
|
- pnl_emoji = "🟢"
|
|
|
|
- pnl_str = f"+${pnl:.2f}"
|
|
|
|
- elif pnl < 0:
|
|
|
|
- pnl_emoji = "🔴"
|
|
|
|
- pnl_str = f"${pnl:.2f}"
|
|
|
|
- else:
|
|
|
|
- pnl_emoji = "⚪"
|
|
|
|
- pnl_str = "$0.00"
|
|
|
|
-
|
|
|
|
- side_emoji = "🟢" if side == 'BUY' else "🔴"
|
|
|
|
-
|
|
|
|
- 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 reply_method(message, parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- logger.error(f"Error in trades command: {e}")
|
|
|
|
- await reply_method("❌ Error retrieving trade history.", parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- 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(update):
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- # Get token from arguments or use default
|
|
|
|
- if context.args and len(context.args) > 0:
|
|
|
|
- token = _normalize_token_case(context.args[0])
|
|
|
|
- else:
|
|
|
|
- token = Config.DEFAULT_TRADING_TOKEN
|
|
|
|
-
|
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
|
- market_data = self.trading_engine.get_market_data(symbol)
|
|
|
|
-
|
|
|
|
- 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)
|
|
|
|
- raw_base_volume = ticker.get('baseVolume')
|
|
|
|
- volume_24h = float(raw_base_volume if raw_base_volume is not None else 0.0)
|
|
|
|
- raw_change_24h = ticker.get('change')
|
|
|
|
- change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
|
|
|
|
- raw_percentage = ticker.get('percentage')
|
|
|
|
- change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
|
|
|
|
- high_24h = float(ticker.get('high', 0.0) or 0.0)
|
|
|
|
- low_24h = float(ticker.get('low', 0.0) or 0.0)
|
|
|
|
-
|
|
|
|
- # Market direction emoji
|
|
|
|
- trend_emoji = "🟢" if change_24h >= 0 else "🔴"
|
|
|
|
-
|
|
|
|
- # Format prices with proper precision for this token
|
|
|
|
- formatter = get_formatter()
|
|
|
|
- current_price_str = formatter.format_price_with_symbol(current_price, token)
|
|
|
|
- bid_price_str = formatter.format_price_with_symbol(bid_price, token)
|
|
|
|
- ask_price_str = formatter.format_price_with_symbol(ask_price, token)
|
|
|
|
- spread_str = formatter.format_price_with_symbol(ask_price - bid_price, token)
|
|
|
|
- high_24h_str = formatter.format_price_with_symbol(high_24h, token)
|
|
|
|
- low_24h_str = formatter.format_price_with_symbol(low_24h, token)
|
|
|
|
- change_24h_str = formatter.format_price_with_symbol(change_24h, token)
|
|
|
|
-
|
|
|
|
- market_text = f"""
|
|
|
|
-📊 <b>{token} Market Data</b>
|
|
|
|
-
|
|
|
|
-💰 <b>Price Information:</b>
|
|
|
|
- 💵 Current: {current_price_str}
|
|
|
|
- 🟢 Bid: {bid_price_str}
|
|
|
|
- 🔴 Ask: {ask_price_str}
|
|
|
|
- 📊 Spread: {spread_str}
|
|
|
|
-
|
|
|
|
-📈 <b>24h Statistics:</b>
|
|
|
|
- {trend_emoji} Change: {change_24h_str} ({change_percent:+.2f}%)
|
|
|
|
- 🔝 High: {high_24h_str}
|
|
|
|
- 🔻 Low: {low_24h_str}
|
|
|
|
- 📊 Volume: {volume_24h:,.2f} {token}
|
|
|
|
-
|
|
|
|
-⏰ <b>Last Updated:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
|
- """
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=market_text.strip(), parse_mode='HTML')
|
|
|
|
- else:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch market data for {token}")
|
|
|
|
-
|
|
|
|
- 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(update):
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- # Get token from arguments or use default
|
|
|
|
- if context.args and len(context.args) > 0:
|
|
|
|
- token = _normalize_token_case(context.args[0])
|
|
|
|
- else:
|
|
|
|
- token = Config.DEFAULT_TRADING_TOKEN
|
|
|
|
-
|
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
|
- market_data = self.trading_engine.get_market_data(symbol)
|
|
|
|
-
|
|
|
|
- if market_data:
|
|
|
|
- ticker = market_data.get('ticker', {})
|
|
|
|
- current_price = float(ticker.get('last', 0.0) or 0.0)
|
|
|
|
- raw_change_24h = ticker.get('change')
|
|
|
|
- change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
|
|
|
|
- raw_percentage = ticker.get('percentage')
|
|
|
|
- change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
|
|
|
|
-
|
|
|
|
- # Price direction emoji
|
|
|
|
- trend_emoji = "🟢" if change_24h >= 0 else "🔴"
|
|
|
|
-
|
|
|
|
- # Format prices with proper precision for this token
|
|
|
|
- formatter = get_formatter()
|
|
|
|
- current_price_str = formatter.format_price_with_symbol(current_price, token)
|
|
|
|
- change_24h_str = formatter.format_price_with_symbol(change_24h, token)
|
|
|
|
-
|
|
|
|
- price_text = f"""
|
|
|
|
-💵 <b>{token} Price</b>
|
|
|
|
-
|
|
|
|
-💰 {current_price_str}
|
|
|
|
-{trend_emoji} {change_percent:+.2f}% ({change_24h_str})
|
|
|
|
-
|
|
|
|
-⏰ {datetime.now().strftime('%H:%M:%S')}
|
|
|
|
- """
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=price_text.strip(), parse_mode='HTML')
|
|
|
|
- else:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch price for {token}")
|
|
|
|
-
|
|
|
|
- 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(update):
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- try:
|
|
|
|
- # Check if specific token is requested
|
|
|
|
- if context.args and len(context.args) >= 1:
|
|
|
|
- # Detailed performance for specific token
|
|
|
|
- token = _normalize_token_case(context.args[0])
|
|
|
|
- await self._show_token_performance(chat_id, token, context)
|
|
|
|
- else:
|
|
|
|
- # Show token performance ranking
|
|
|
|
- await self._show_performance_ranking(chat_id, context)
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- error_message = f"❌ Error processing performance command: {str(e)}"
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
|
|
- logger.error(f"Error in performance command: {e}")
|
|
|
|
-
|
|
|
|
- async def _show_performance_ranking(self, chat_id: str, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
- """Show token performance ranking (compressed view)."""
|
|
|
|
- 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
|
|
|
|
-
|
|
|
|
- token_performance = stats.get_token_performance()
|
|
|
|
-
|
|
|
|
- if not token_performance:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- "📊 <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
|
|
|
|
-
|
|
|
|
- # Tokens are already sorted by ROE from the performance calculator
|
|
|
|
- sorted_tokens = 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(sorted_tokens, 1):
|
|
|
|
- # Ranking emoji
|
|
|
|
- if i == 1:
|
|
|
|
- rank_emoji = "🥇"
|
|
|
|
- elif i == 2:
|
|
|
|
- rank_emoji = "🥈"
|
|
|
|
- elif i == 3:
|
|
|
|
- rank_emoji = "🥉"
|
|
|
|
- else:
|
|
|
|
- rank_emoji = f"#{i}"
|
|
|
|
-
|
|
|
|
- total_pnl = stats_data.get('total_realized_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_completed_cycles', 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_realized_pnl', 0) for stats_data in token_performance)
|
|
|
|
- total_trades = sum(stats_data.get('total_completed_cycles', 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 context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- async def _show_token_performance(self, chat_id: str, token: str, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
- """Show detailed performance for a specific token."""
|
|
|
|
- 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
|
|
|
|
-
|
|
|
|
- token_stats = stats.get_token_detailed_stats(token)
|
|
|
|
- formatter = get_formatter() # Get the formatter instance
|
|
|
|
-
|
|
|
|
- # Check if token has any data
|
|
|
|
- if token_stats.get('summary_total_trades', 0) == 0:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- 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) # Assuming total volume is in quote currency
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- 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 "🔴"
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- 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
|
|
|
|
- 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 context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- 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(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
|
|
|
|
-
|
|
|
|
- daily_stats = stats.get_daily_stats(10)
|
|
|
|
- formatter = get_formatter() # Get formatter
|
|
|
|
-
|
|
|
|
- if not daily_stats:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- "📅 <b>Daily Performance</b>\n"
|
|
|
|
- "📭 No daily performance data available yet.\n"
|
|
|
|
- "💡 Daily stats are calculated from completed trades. Start trading to see them!",
|
|
|
|
- parse_mode='HTML'
|
|
|
|
- )
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- daily_text_parts = [f"📅 <b>Daily Performance (Last 10 Days)</b>"]
|
|
|
|
-
|
|
|
|
- total_pnl_all_days = 0
|
|
|
|
- total_trades_all_days = 0
|
|
|
|
- trading_days_count = 0
|
|
|
|
-
|
|
|
|
- period_lines = []
|
|
|
|
- for day_stats_item in daily_stats:
|
|
|
|
- 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'])
|
|
|
|
- period_lines.append(f"📊 <b>{day_stats_item['date_formatted']}</b>: {pnl_emoji} {pnl_str} ({day_stats_item['pnl_pct']:+.1f}%) | Trades: {day_stats_item['trades']}")
|
|
|
|
- total_pnl_all_days += day_stats_item['pnl']
|
|
|
|
- total_trades_all_days += day_stats_item['trades']
|
|
|
|
- trading_days_count += 1
|
|
|
|
- else:
|
|
|
|
- period_lines.append(f"📊 <b>{day_stats_item['date_formatted']}</b>: 📭 No trading activity")
|
|
|
|
-
|
|
|
|
- if period_lines: # Add collected period lines if any
|
|
|
|
- daily_text_parts.append("\n".join(period_lines))
|
|
|
|
-
|
|
|
|
- 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)
|
|
|
|
- avg_daily_pnl_str = formatter.format_price_with_symbol(avg_daily_pnl)
|
|
|
|
-
|
|
|
|
- daily_text_parts.append(f"\n\n📈 <b>Period Summary:</b>")
|
|
|
|
- daily_text_parts.append(f" Total P&L: {avg_pnl_emoji} {total_pnl_all_days_str} | Avg Daily: {avg_daily_pnl_str}")
|
|
|
|
- daily_text_parts.append(f" Trading Days: {trading_days_count}/10 | Total Trades: {total_trades_all_days}")
|
|
|
|
- else:
|
|
|
|
- if not period_lines: # If there were no stat items at all, the header is the only part
|
|
|
|
- daily_text_parts = [daily_text_parts[0]] # Keep only the title
|
|
|
|
- daily_text_parts.append("\n\n📉 No trading activity in the last 10 days.")
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="\n".join(daily_text_parts).strip(), parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- error_message = f"❌ Error processing daily command: {str(e)}"
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
|
|
- logger.error(f"Error in daily command: {e}")
|
|
|
|
-
|
|
|
|
- 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(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
|
|
|
|
-
|
|
|
|
- weekly_stats_list = stats.get_weekly_stats(10) # Renamed variable
|
|
|
|
- formatter = get_formatter() # Get formatter
|
|
|
|
-
|
|
|
|
- if not weekly_stats_list:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- "📊 <b>Weekly Performance</b>\n"
|
|
|
|
- "📭 No weekly performance data available yet.\n"
|
|
|
|
- "💡 Weekly stats are calculated from completed trades. Start trading to see them!",
|
|
|
|
- parse_mode='HTML'
|
|
|
|
- )
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- weekly_text_parts = [f"📊 <b>Weekly Performance (Last 10 Weeks)</b>"]
|
|
|
|
-
|
|
|
|
- total_pnl_all_weeks = 0
|
|
|
|
- total_trades_all_weeks = 0
|
|
|
|
- trading_weeks_count = 0
|
|
|
|
-
|
|
|
|
- period_lines = []
|
|
|
|
- for week_stats_item in weekly_stats_list:
|
|
|
|
- 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'])
|
|
|
|
- period_lines.append(f"📈 <b>{week_stats_item['week_formatted']}</b>: {pnl_emoji} {pnl_str} ({week_stats_item['pnl_pct']:+.1f}%) | Trades: {week_stats_item['trades']}")
|
|
|
|
- total_pnl_all_weeks += week_stats_item['pnl']
|
|
|
|
- total_trades_all_weeks += week_stats_item['trades']
|
|
|
|
- trading_weeks_count += 1
|
|
|
|
- else:
|
|
|
|
- period_lines.append(f"📈 <b>{week_stats_item['week_formatted']}</b>: 📭 No trading activity")
|
|
|
|
-
|
|
|
|
- if period_lines:
|
|
|
|
- weekly_text_parts.append("\n".join(period_lines))
|
|
|
|
-
|
|
|
|
- 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)
|
|
|
|
- avg_weekly_pnl_str = formatter.format_price_with_symbol(avg_weekly_pnl)
|
|
|
|
-
|
|
|
|
- weekly_text_parts.append(f"\n\n📅 <b>Period Summary:</b>")
|
|
|
|
- weekly_text_parts.append(f" Total P&L: {avg_pnl_emoji} {total_pnl_all_weeks_str} | Avg Weekly: {avg_weekly_pnl_str}")
|
|
|
|
- weekly_text_parts.append(f" Trading Weeks: {trading_weeks_count}/10 | Total Trades: {total_trades_all_weeks}")
|
|
|
|
- else:
|
|
|
|
- if not period_lines:
|
|
|
|
- weekly_text_parts = [weekly_text_parts[0]]
|
|
|
|
- weekly_text_parts.append("\n\n📉 No trading activity in the last 10 weeks.")
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="\n".join(weekly_text_parts).strip(), parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- error_message = f"❌ Error processing weekly command: {str(e)}"
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
|
|
- logger.error(f"Error in weekly command: {e}")
|
|
|
|
-
|
|
|
|
- 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(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
|
|
|
|
-
|
|
|
|
- monthly_stats_list = stats.get_monthly_stats(12) # Renamed variable, 12 months
|
|
|
|
- formatter = get_formatter() # Get formatter
|
|
|
|
-
|
|
|
|
- if not monthly_stats_list:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- "🗓️ <b>Monthly Performance</b>\n"
|
|
|
|
- "📭 No monthly performance data available yet.\n"
|
|
|
|
- "💡 Monthly stats are calculated from completed trades. Start trading to see them!",
|
|
|
|
- parse_mode='HTML'
|
|
|
|
- )
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- monthly_text_parts = [f"🗓️ <b>Monthly Performance (Last 12 Months)</b>"]
|
|
|
|
-
|
|
|
|
- total_pnl_all_months = 0
|
|
|
|
- total_trades_all_months = 0
|
|
|
|
- trading_months_count = 0
|
|
|
|
-
|
|
|
|
- period_lines = []
|
|
|
|
- for month_stats_item in monthly_stats_list:
|
|
|
|
- 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'])
|
|
|
|
- period_lines.append(f"📅 <b>{month_stats_item['month_formatted']}</b>: {pnl_emoji} {pnl_str} ({month_stats_item['pnl_pct']:+.1f}%) | Trades: {month_stats_item['trades']}")
|
|
|
|
- total_pnl_all_months += month_stats_item['pnl']
|
|
|
|
- total_trades_all_months += month_stats_item['trades']
|
|
|
|
- trading_months_count += 1
|
|
|
|
- else:
|
|
|
|
- period_lines.append(f"📅 <b>{month_stats_item['month_formatted']}</b>: 📭 No trading activity")
|
|
|
|
-
|
|
|
|
- if period_lines:
|
|
|
|
- monthly_text_parts.append("\n".join(period_lines))
|
|
|
|
-
|
|
|
|
- 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)
|
|
|
|
- avg_monthly_pnl_str = formatter.format_price_with_symbol(avg_monthly_pnl)
|
|
|
|
-
|
|
|
|
- monthly_text_parts.append(f"\n\n📈 <b>Period Summary:</b>")
|
|
|
|
- monthly_text_parts.append(f" Total P&L: {avg_pnl_emoji} {total_pnl_all_months_str} | Avg Monthly: {avg_monthly_pnl_str}")
|
|
|
|
- monthly_text_parts.append(f" Trading Months: {trading_months_count}/12 | Total Trades: {total_trades_all_months}")
|
|
|
|
- else:
|
|
|
|
- if not period_lines:
|
|
|
|
- monthly_text_parts = [monthly_text_parts[0]]
|
|
|
|
- monthly_text_parts.append("\n\n📉 No trading activity in the last 12 months.")
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="\n".join(monthly_text_parts).strip(), parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- error_message = f"❌ Error processing monthly command: {str(e)}"
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
|
|
- logger.error(f"Error in monthly command: {e}")
|
|
|
|
-
|
|
|
|
- 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(update):
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- try:
|
|
|
|
- # Get current balance for context
|
|
|
|
- balance = self.trading_engine.get_balance()
|
|
|
|
- current_balance = 0
|
|
|
|
- if balance and balance.get('total'):
|
|
|
|
- current_balance = float(balance['total'].get('USDC', 0))
|
|
|
|
-
|
|
|
|
- # Get risk metrics and basic stats
|
|
|
|
- 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
|
|
|
|
-
|
|
|
|
- risk_metrics = stats.get_risk_metrics()
|
|
|
|
- basic_stats = stats.get_basic_stats()
|
|
|
|
-
|
|
|
|
- # Check if we have enough data for risk calculations
|
|
|
|
- if basic_stats['completed_trades'] < 2:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- "📊 <b>Risk Analysis</b>\\n\\n"
|
|
|
|
- "📭 <b>Insufficient Data</b>\\n\\n"
|
|
|
|
- f"• Current completed trades: {html.escape(str(basic_stats['completed_trades']))}\\n"
|
|
|
|
- f"• Required for risk analysis: 2+ trades\\n"
|
|
|
|
- f"• Daily balance snapshots: {html.escape(str(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"
|
|
|
|
- "• Risk metrics will be available after sufficient trading history\\n\\n"
|
|
|
|
- "📈 Use /stats for current performance metrics",
|
|
|
|
- parse_mode='HTML'
|
|
|
|
- )
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- # Get risk metric values with safe defaults
|
|
|
|
- sharpe_ratio = risk_metrics.get('sharpe_ratio')
|
|
|
|
- max_drawdown_pct = risk_metrics.get('max_drawdown_live_percentage', 0.0)
|
|
|
|
-
|
|
|
|
- # Format values safely
|
|
|
|
- sharpe_str = f"{sharpe_ratio:.3f}" if sharpe_ratio is not None else "N/A"
|
|
|
|
-
|
|
|
|
- # Format the risk analysis message
|
|
|
|
- risk_text = f"""
|
|
|
|
-📊 <b>Risk Analysis & Advanced Metrics</b>
|
|
|
|
-
|
|
|
|
-🎯 <b>Risk-Adjusted Performance:</b>
|
|
|
|
-• Sharpe Ratio: {sharpe_str}
|
|
|
|
-• Profit Factor: {risk_metrics.get('profit_factor', 0):.2f}
|
|
|
|
-• Win Rate: {risk_metrics.get('win_rate', 0):.1f}%
|
|
|
|
-
|
|
|
|
-📉 <b>Drawdown Analysis:</b>
|
|
|
|
-• Maximum Drawdown: {max_drawdown_pct:.2f}%
|
|
|
|
-• Max Consecutive Losses: {risk_metrics.get('max_consecutive_losses', 0)}
|
|
|
|
-
|
|
|
|
-💰 <b>Portfolio Context:</b>
|
|
|
|
-• Current Balance: ${current_balance:,.2f}
|
|
|
|
-• Initial Balance: ${basic_stats['initial_balance']:,.2f}
|
|
|
|
-• Total P&L: ${basic_stats['total_pnl']:,.2f}
|
|
|
|
-• Days Active: {html.escape(str(basic_stats['days_active']))}
|
|
|
|
-
|
|
|
|
-📊 <b>Risk Interpretation:</b>
|
|
|
|
-"""
|
|
|
|
-
|
|
|
|
- # Add interpretive guidance
|
|
|
|
- if sharpe_ratio is not None:
|
|
|
|
- if sharpe_ratio > 2.0:
|
|
|
|
- risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
|
|
|
|
- elif sharpe_ratio > 1.0:
|
|
|
|
- risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
|
|
|
|
- elif sharpe_ratio > 0.5:
|
|
|
|
- risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
|
|
|
|
- elif sharpe_ratio > 0:
|
|
|
|
- risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
|
|
|
|
- else:
|
|
|
|
- risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
|
|
|
|
- else:
|
|
|
|
- risk_text += "• ⚪ <b>Insufficient data</b> for Sharpe ratio calculation\n"
|
|
|
|
-
|
|
|
|
- if max_drawdown_pct < 5:
|
|
|
|
- risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
|
|
|
|
- elif max_drawdown_pct < 15:
|
|
|
|
- risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
|
|
|
|
- elif max_drawdown_pct < 30:
|
|
|
|
- risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
|
|
|
|
- else:
|
|
|
|
- risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
|
|
|
|
-
|
|
|
|
- # Add profit factor interpretation
|
|
|
|
- profit_factor = risk_metrics.get('profit_factor', 0)
|
|
|
|
- if profit_factor > 2.0:
|
|
|
|
- risk_text += "• 🟢 <b>Excellent</b> profit factor (> 2.0)\n"
|
|
|
|
- elif profit_factor > 1.5:
|
|
|
|
- risk_text += "• 🟡 <b>Good</b> profit factor (> 1.5)\n"
|
|
|
|
- elif profit_factor > 1.0:
|
|
|
|
- risk_text += "• 🟠 <b>Profitable</b> but low profit factor (> 1.0)\n"
|
|
|
|
- else:
|
|
|
|
- risk_text += "• 🔴 <b>Unprofitable</b> trading strategy (< 1.0)\n"
|
|
|
|
-
|
|
|
|
- risk_text += f"""
|
|
|
|
-
|
|
|
|
-💡 <b>Risk Definitions:</b>
|
|
|
|
-• <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
|
|
|
|
-• <b>Profit Factor:</b> Total winning trades / Total losing trades
|
|
|
|
-• <b>Win Rate:</b> Percentage of profitable trades
|
|
|
|
-• <b>Max Drawdown:</b> Largest peak-to-trough decline
|
|
|
|
-• <b>Max Consecutive Losses:</b> Longest streak of losing trades
|
|
|
|
-
|
|
|
|
-📈 <b>Data Based On:</b>
|
|
|
|
-• Completed Trades: {html.escape(str(basic_stats['completed_trades']))}
|
|
|
|
-• Trading Period: {html.escape(str(basic_stats['days_active']))} days
|
|
|
|
-
|
|
|
|
-🔄 Use /stats for trading performance metrics
|
|
|
|
- """
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- error_message = f"❌ Error processing risk command: {str(e)}"
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
|
|
- logger.error(f"Error in risk command: {e}")
|
|
|
|
-
|
|
|
|
- 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(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 balance adjustments summary
|
|
|
|
- adjustments_summary = stats.get_balance_adjustments_summary()
|
|
|
|
-
|
|
|
|
- # Get detailed adjustments
|
|
|
|
- all_adjustments = stats.data.get('balance_adjustments', [])
|
|
|
|
-
|
|
|
|
- if not all_adjustments:
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=
|
|
|
|
- "💰 <b>Balance Adjustments</b>\n\n"
|
|
|
|
- "📭 No deposits or withdrawals detected yet.\n\n"
|
|
|
|
- "💡 The bot automatically monitors for deposits and withdrawals\n"
|
|
|
|
- "every hour to maintain accurate P&L calculations.",
|
|
|
|
- parse_mode='HTML'
|
|
|
|
- )
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- # Format the message
|
|
|
|
- adjustments_text = f"""
|
|
|
|
-💰 <b>Balance Adjustments History</b>
|
|
|
|
-
|
|
|
|
-📊 <b>Summary:</b>
|
|
|
|
-• Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
|
|
|
|
-• Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
|
|
|
|
-• Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
|
|
|
|
-• Total Transactions: {adjustments_summary['adjustment_count']}
|
|
|
|
-
|
|
|
|
-📅 <b>Recent Adjustments:</b>
|
|
|
|
-"""
|
|
|
|
-
|
|
|
|
- # Show last 10 adjustments
|
|
|
|
- recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
|
|
|
|
-
|
|
|
|
- for adj in recent_adjustments:
|
|
|
|
- try:
|
|
|
|
- # Format timestamp
|
|
|
|
- adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
|
|
|
|
-
|
|
|
|
- # Format type and amount
|
|
|
|
- if adj['type'] == 'deposit':
|
|
|
|
- emoji = "💰"
|
|
|
|
- amount_str = f"+${adj['amount']:,.2f}"
|
|
|
|
- else: # withdrawal
|
|
|
|
- emoji = "💸"
|
|
|
|
- amount_str = f"-${abs(adj['amount']):,.2f}"
|
|
|
|
-
|
|
|
|
- adjustments_text += f"• {emoji} {adj_time}: {amount_str}\n"
|
|
|
|
-
|
|
|
|
- except Exception as adj_error:
|
|
|
|
- logger.warning(f"Error formatting adjustment: {adj_error}")
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- adjustments_text += f"""
|
|
|
|
-
|
|
|
|
-💡 <b>How it Works:</b>
|
|
|
|
-• Bot checks for deposits/withdrawals every hour
|
|
|
|
-• Adjustments maintain accurate P&L calculations
|
|
|
|
-• Non-trading balance changes don't affect performance metrics
|
|
|
|
-• Trading statistics remain pure and accurate
|
|
|
|
-
|
|
|
|
-⏰ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
|
|
|
|
- """
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=adjustments_text.strip(), parse_mode='HTML')
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- error_message = f"❌ Error processing balance adjustments command: {str(e)}"
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=error_message)
|
|
|
|
- logger.error(f"Error in balance_adjustments command: {e}")
|
|
|
|
-
|
|
|
|
- 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(update):
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- commands_text = """
|
|
|
|
-📱 <b>Quick Commands</b>
|
|
|
|
-
|
|
|
|
-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!
|
|
|
|
- """
|
|
|
|
-
|
|
|
|
- keyboard = [
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("💰 Balance", callback_data="/balance"),
|
|
|
|
- InlineKeyboardButton("📈 Positions", callback_data="/positions")
|
|
|
|
- ],
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("📋 Orders", callback_data="/orders"),
|
|
|
|
- InlineKeyboardButton("📊 Stats", callback_data="/stats")
|
|
|
|
- ],
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("💵 Price", callback_data="/price"),
|
|
|
|
- InlineKeyboardButton("📊 Market", callback_data="/market")
|
|
|
|
- ],
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("🏆 Performance", callback_data="/performance"),
|
|
|
|
- InlineKeyboardButton("🔔 Alarms", callback_data="/alarm")
|
|
|
|
- ],
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("📅 Daily", callback_data="/daily"),
|
|
|
|
- InlineKeyboardButton("📊 Weekly", callback_data="/weekly")
|
|
|
|
- ],
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("📆 Monthly", callback_data="/monthly"),
|
|
|
|
- InlineKeyboardButton("🔄 Trades", callback_data="/trades")
|
|
|
|
- ],
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("🔄 Monitoring", callback_data="/monitoring"),
|
|
|
|
- InlineKeyboardButton("📝 Logs", callback_data="/logs")
|
|
|
|
- ],
|
|
|
|
- [
|
|
|
|
- InlineKeyboardButton("🎲 Risk", callback_data="/risk"),
|
|
|
|
- InlineKeyboardButton("⚙️ Help", callback_data="/help")
|
|
|
|
- ]
|
|
|
|
- ]
|
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
-
|
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
|
-
|
|
|
|
- async def _estimate_entry_price_for_orphaned_position(self, symbol: str, contracts: float) -> float:
|
|
|
|
- """Estimate entry price for an orphaned position by checking recent fills and market data."""
|
|
|
|
- try:
|
|
|
|
- # Method 1: Check recent fills from the exchange
|
|
|
|
- recent_fills = self.trading_engine.get_recent_fills()
|
|
|
|
- if recent_fills:
|
|
|
|
- # Look for recent fills for this symbol
|
|
|
|
- symbol_fills = [fill for fill in recent_fills if fill.get('symbol') == symbol]
|
|
|
|
-
|
|
|
|
- if symbol_fills:
|
|
|
|
- # Get the most recent fill as entry price estimate
|
|
|
|
- latest_fill = symbol_fills[0] # Assuming sorted by newest first
|
|
|
|
- fill_price = float(latest_fill.get('price', 0))
|
|
|
|
-
|
|
|
|
- if fill_price > 0:
|
|
|
|
- logger.info(f"💡 Found recent fill price for {symbol}: ${fill_price:.4f}")
|
|
|
|
- return fill_price
|
|
|
|
-
|
|
|
|
- # Method 2: Use current market price as fallback
|
|
|
|
- market_data = self.trading_engine.get_market_data(symbol)
|
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
|
-
|
|
|
|
- if current_price > 0:
|
|
|
|
- logger.warning(f"⚠️ Using current market price as entry estimate for {symbol}: ${current_price:.4f}")
|
|
|
|
- return current_price
|
|
|
|
-
|
|
|
|
- # Method 3: Last resort - try bid/ask average
|
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
|
- bid = float(market_data['ticker'].get('bid', 0))
|
|
|
|
- ask = float(market_data['ticker'].get('ask', 0))
|
|
|
|
-
|
|
|
|
- if bid > 0 and ask > 0:
|
|
|
|
- avg_price = (bid + ask) / 2
|
|
|
|
- logger.warning(f"⚠️ Using bid/ask average as entry estimate for {symbol}: ${avg_price:.4f}")
|
|
|
|
- return avg_price
|
|
|
|
-
|
|
|
|
- # Method 4: Absolute fallback - return a small positive value to avoid 0
|
|
|
|
- logger.error(f"❌ Could not estimate entry price for {symbol}, using fallback value of $1.00")
|
|
|
|
- return 1.0
|
|
|
|
-
|
|
|
|
- except Exception as e:
|
|
|
|
- logger.error(f"❌ Error estimating entry price for {symbol}: {e}")
|
|
|
|
- return 1.0 # Safe fallback
|
|
|