|
@@ -60,6 +60,9 @@ class TelegramTradingBot:
|
|
|
# Pending stop loss storage
|
|
|
self.pending_stop_losses = {} # Format: {order_id: {'token': str, 'stop_price': float, 'side': str, 'amount': float, 'order_type': str}}
|
|
|
|
|
|
+ # Track bot-generated trades to avoid double processing
|
|
|
+ self.bot_trade_ids = set() # Track trade IDs generated by bot commands
|
|
|
+
|
|
|
# Load bot state first, then initialize stats
|
|
|
self._load_bot_state()
|
|
|
self._initialize_stats()
|
|
@@ -76,6 +79,9 @@ class TelegramTradingBot:
|
|
|
self.last_known_orders = set(state_data.get('last_known_orders', []))
|
|
|
self.last_known_positions = state_data.get('last_known_positions', {})
|
|
|
|
|
|
+ # Restore bot trade IDs (prevent double processing)
|
|
|
+ self.bot_trade_ids = set(state_data.get('bot_trade_ids', []))
|
|
|
+
|
|
|
# Restore timestamps (convert from ISO string if present)
|
|
|
last_trade_time = state_data.get('last_processed_trade_time')
|
|
|
if last_trade_time:
|
|
@@ -107,6 +113,7 @@ class TelegramTradingBot:
|
|
|
self.pending_stop_losses = {}
|
|
|
self.last_known_orders = set()
|
|
|
self.last_known_positions = {}
|
|
|
+ self.bot_trade_ids = set()
|
|
|
self.last_processed_trade_time = None
|
|
|
self.last_deposit_withdrawal_check = None
|
|
|
|
|
@@ -134,6 +141,7 @@ class TelegramTradingBot:
|
|
|
'pending_stop_losses': self.pending_stop_losses,
|
|
|
'last_known_orders': list(self.last_known_orders), # Convert set to list for JSON
|
|
|
'last_known_positions': self.last_known_positions,
|
|
|
+ 'bot_trade_ids': list(self.bot_trade_ids), # Track bot-generated trades
|
|
|
'last_processed_trade_time': safe_datetime_to_iso(self.last_processed_trade_time),
|
|
|
'last_deposit_withdrawal_check': safe_datetime_to_iso(self.last_deposit_withdrawal_check),
|
|
|
'last_updated': datetime.now().isoformat(),
|
|
@@ -651,7 +659,14 @@ Tap any button below for instant access to bot functions:
|
|
|
|
|
|
# Extract token name for cleaner display
|
|
|
token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- position_type = "LONG" if contracts > 0 else "SHORT"
|
|
|
+
|
|
|
+ # Use CCXT's side field if available, otherwise fall back to contracts sign
|
|
|
+ side_field = position.get('side', '').lower()
|
|
|
+ if side_field in ['long', 'short']:
|
|
|
+ position_type = side_field.upper()
|
|
|
+ else:
|
|
|
+ # Fallback for exchanges that don't provide side field
|
|
|
+ position_type = "LONG" if contracts > 0 else "SHORT"
|
|
|
|
|
|
positions_text += f"📊 <b>{token}</b> ({position_type})\n"
|
|
|
positions_text += f" 📏 Size: {abs(contracts):.6f} {token}\n"
|
|
@@ -1058,6 +1073,12 @@ Tap any button below for instant access to bot functions:
|
|
|
actual_price = price
|
|
|
action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'buy', token_amount, actual_price, order_id, "bot")
|
|
|
|
|
|
+ # Track this as a bot-generated trade to prevent double processing
|
|
|
+ if order_id and order_id != 'N/A':
|
|
|
+ self.bot_trade_ids.add(order_id)
|
|
|
+ self._save_bot_state() # Save state to persist bot trade tracking
|
|
|
+ logger.info(f"🤖 Tracked bot trade ID: {order_id}")
|
|
|
+
|
|
|
# Save pending stop loss if provided
|
|
|
if stop_loss_price is not None:
|
|
|
self.pending_stop_losses[order_id] = {
|
|
@@ -1145,6 +1166,12 @@ Tap any button below for instant access to bot functions:
|
|
|
actual_price = price
|
|
|
action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'sell', token_amount, actual_price, order_id, "bot")
|
|
|
|
|
|
+ # Track this as a bot-generated trade to prevent double processing
|
|
|
+ if order_id and order_id != 'N/A':
|
|
|
+ self.bot_trade_ids.add(order_id)
|
|
|
+ self._save_bot_state() # Save state to persist bot trade tracking
|
|
|
+ logger.info(f"🤖 Tracked bot trade ID: {order_id}")
|
|
|
+
|
|
|
# Save pending stop loss if provided
|
|
|
if stop_loss_price is not None:
|
|
|
self.pending_stop_losses[order_id] = {
|
|
@@ -1213,6 +1240,12 @@ Tap any button below for instant access to bot functions:
|
|
|
actual_price = price
|
|
|
action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
|
|
|
|
|
|
+ # Track this as a bot-generated trade to prevent double processing
|
|
|
+ if order_id and order_id != 'N/A':
|
|
|
+ self.bot_trade_ids.add(order_id)
|
|
|
+ self._save_bot_state() # Save state to persist bot trade tracking
|
|
|
+ logger.info(f"🤖 Tracked bot trade ID: {order_id}")
|
|
|
+
|
|
|
position_type = "LONG" if exit_side == "sell" else "SHORT"
|
|
|
|
|
|
success_message = f"""
|
|
@@ -1426,29 +1459,49 @@ Tap any button below for instant access to bot functions:
|
|
|
logger.error(f"Error setting take profit: {e}")
|
|
|
|
|
|
async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle messages from custom keyboard buttons (without /)."""
|
|
|
+ """Handle messages from custom keyboard buttons."""
|
|
|
if not self.is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
return
|
|
|
|
|
|
message_text = update.message.text.lower()
|
|
|
|
|
|
- # Map clean button text to command handlers
|
|
|
- command_handlers = {
|
|
|
- 'daily': self.daily_command,
|
|
|
- 'performance': self.performance_command,
|
|
|
- 'balance': self.balance_command,
|
|
|
- 'stats': self.stats_command,
|
|
|
- 'positions': self.positions_command,
|
|
|
- 'orders': self.orders_command,
|
|
|
- 'price': self.price_command,
|
|
|
- 'market': self.market_command,
|
|
|
- 'help': self.help_command,
|
|
|
- 'commands': self.commands_command
|
|
|
+ # Map keyboard button text to commands
|
|
|
+ command_map = {
|
|
|
+ 'balance': '/balance',
|
|
|
+ 'positions': '/positions',
|
|
|
+ 'orders': '/orders',
|
|
|
+ 'stats': '/stats',
|
|
|
+ 'trades': '/trades',
|
|
|
+ 'market': '/market',
|
|
|
+ 'price': '/price',
|
|
|
+ 'help': '/help',
|
|
|
+ 'commands': '/commands',
|
|
|
+ 'monitoring': '/monitoring',
|
|
|
+ 'logs': '/logs',
|
|
|
+ 'performance': '/performance',
|
|
|
+ 'daily': '/daily',
|
|
|
+ 'weekly': '/weekly',
|
|
|
+ 'monthly': '/monthly',
|
|
|
+ 'risk': '/risk',
|
|
|
+ 'alarm': '/alarm',
|
|
|
+ 'keyboard': '/keyboard'
|
|
|
}
|
|
|
|
|
|
- # Execute the corresponding command handler
|
|
|
- if message_text in command_handlers:
|
|
|
- await command_handlers[message_text](update, context)
|
|
|
+ # Check if the message matches any keyboard command
|
|
|
+ if message_text in command_map:
|
|
|
+ # Create a fake update object with the corresponding command
|
|
|
+ update.message.text = command_map[message_text]
|
|
|
+ # Get the handler for this command and call it
|
|
|
+ handlers = self.application.handlers[0] # Get default group handlers
|
|
|
+ for handler in handlers:
|
|
|
+ if hasattr(handler, 'callback') and hasattr(handler, 'filters'):
|
|
|
+ if await handler.check_update(update):
|
|
|
+ await handler.callback(update, context)
|
|
|
+ return
|
|
|
+
|
|
|
+ # If no keyboard command matched, show a help message
|
|
|
+ await update.message.reply_text("❓ Unknown command. Use /help to see available commands.")
|
|
|
|
|
|
async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle unknown commands."""
|
|
@@ -1494,6 +1547,7 @@ Tap any button below for instant access to bot functions:
|
|
|
self.application.add_handler(CommandHandler("version", self.version_command))
|
|
|
self.application.add_handler(CommandHandler("balance_adjustments", self.balance_adjustments_command))
|
|
|
self.application.add_handler(CommandHandler("keyboard", self.keyboard_command))
|
|
|
+ self.application.add_handler(CommandHandler("debug", self.debug_command))
|
|
|
|
|
|
# Callback query handler for inline keyboards
|
|
|
self.application.add_handler(CallbackQueryHandler(self.button_callback))
|
|
@@ -2079,8 +2133,10 @@ This action cannot be undone.
|
|
|
entry_price = float(current_position.get('entryPx', 0))
|
|
|
unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
|
|
|
|
|
|
- # Determine position direction and validate stop loss price
|
|
|
- if contracts > 0:
|
|
|
+ # Use CCXT's side field to determine position direction
|
|
|
+ side_field = current_position.get('side', '').lower()
|
|
|
+
|
|
|
+ if side_field == 'long':
|
|
|
# Long position - stop loss should be below entry price
|
|
|
position_type = "LONG"
|
|
|
exit_side = "sell"
|
|
@@ -2096,22 +2152,32 @@ This action cannot be undone.
|
|
|
f"💡 Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
|
|
|
)
|
|
|
return
|
|
|
- else:
|
|
|
+ elif side_field == 'short':
|
|
|
# Short position - stop loss should be above entry price
|
|
|
position_type = "SHORT"
|
|
|
exit_side = "buy"
|
|
|
exit_emoji = "🟢"
|
|
|
- contracts_abs = abs(contracts)
|
|
|
+ contracts_abs = contracts # Already positive from CCXT
|
|
|
+
|
|
|
+ # Debug logging for short position validation
|
|
|
+ logger.info(f"🛑 Stop loss validation for SHORT: entry_price=${entry_price}, stop_price=${stop_price}, contracts={contracts}")
|
|
|
|
|
|
if stop_price <= entry_price:
|
|
|
await update.message.reply_text(
|
|
|
f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
|
|
|
f"📊 Your {token} SHORT position:\n"
|
|
|
f"• Entry Price: ${entry_price:,.2f}\n"
|
|
|
- f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
|
|
|
- f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
|
|
|
+ f"• Stop Price: ${stop_price:,.2f} ❌\n"
|
|
|
+ f"• Contracts: {contracts} (side: {side_field})\n\n"
|
|
|
+ f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}\n\n"
|
|
|
+ f"🔧 Debug: stop_price ({stop_price}) <= entry_price ({entry_price}) = {stop_price <= entry_price}"
|
|
|
)
|
|
|
return
|
|
|
+ else:
|
|
|
+ logger.info(f"✅ Stop loss validation PASSED for SHORT: ${stop_price} > ${entry_price}")
|
|
|
+ else:
|
|
|
+ await update.message.reply_text(f"❌ Could not determine position direction for {token}. Side field: '{side_field}'")
|
|
|
+ return
|
|
|
|
|
|
# Get current market price for reference
|
|
|
market_data = self.client.get_market_data(symbol)
|
|
@@ -2120,9 +2186,9 @@ This action cannot be undone.
|
|
|
current_price = float(market_data['ticker'].get('last', 0))
|
|
|
|
|
|
# Calculate estimated P&L at stop loss
|
|
|
- if contracts > 0: # Long position
|
|
|
+ if side_field == 'long':
|
|
|
pnl_at_stop = (stop_price - entry_price) * contracts_abs
|
|
|
- else: # Short position
|
|
|
+ else: # short
|
|
|
pnl_at_stop = (entry_price - stop_price) * contracts_abs
|
|
|
|
|
|
# Create confirmation message
|
|
@@ -2206,8 +2272,10 @@ This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your
|
|
|
entry_price = float(current_position.get('entryPx', 0))
|
|
|
unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
|
|
|
|
|
|
- # Determine position direction and validate take profit price
|
|
|
- if contracts > 0:
|
|
|
+ # Use CCXT's side field to determine position direction
|
|
|
+ side_field = current_position.get('side', '').lower()
|
|
|
+
|
|
|
+ if side_field == 'long':
|
|
|
# Long position - take profit should be above entry price
|
|
|
position_type = "LONG"
|
|
|
exit_side = "sell"
|
|
@@ -2223,12 +2291,12 @@ This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your
|
|
|
f"💡 Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
|
|
|
)
|
|
|
return
|
|
|
- else:
|
|
|
+ elif side_field == 'short':
|
|
|
# Short position - take profit should be below entry price
|
|
|
position_type = "SHORT"
|
|
|
exit_side = "buy"
|
|
|
exit_emoji = "🟢"
|
|
|
- contracts_abs = abs(contracts)
|
|
|
+ contracts_abs = contracts # Already positive from CCXT
|
|
|
|
|
|
if profit_price >= entry_price:
|
|
|
await update.message.reply_text(
|
|
@@ -2239,6 +2307,9 @@ This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your
|
|
|
f"💡 Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
|
|
|
)
|
|
|
return
|
|
|
+ else:
|
|
|
+ await update.message.reply_text(f"❌ Could not determine position direction for {token}. Side field: '{side_field}'")
|
|
|
+ return
|
|
|
|
|
|
# Get current market price for reference
|
|
|
market_data = self.client.get_market_data(symbol)
|
|
@@ -2247,9 +2318,9 @@ This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your
|
|
|
current_price = float(market_data['ticker'].get('last', 0))
|
|
|
|
|
|
# Calculate estimated P&L at take profit
|
|
|
- if contracts > 0: # Long position
|
|
|
+ if side_field == 'long':
|
|
|
pnl_at_tp = (profit_price - entry_price) * contracts_abs
|
|
|
- else: # Short position
|
|
|
+ else: # short
|
|
|
pnl_at_tp = (entry_price - profit_price) * contracts_abs
|
|
|
|
|
|
# Create confirmation message
|
|
@@ -2370,12 +2441,26 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
# Find filled orders (orders that were in last_known_orders but not in current_orders)
|
|
|
filled_order_ids = self.last_known_orders - current_order_ids
|
|
|
|
|
|
+ # Add some debugging for filled order detection
|
|
|
if filled_order_ids:
|
|
|
- logger.info(f"🎯 Detected {len(filled_order_ids)} filled orders")
|
|
|
+ logger.info(f"🎯 Detected {len(filled_order_ids)} filled orders: {list(filled_order_ids)}")
|
|
|
+
|
|
|
+ # Log pending stop losses before processing
|
|
|
+ if self.pending_stop_losses:
|
|
|
+ logger.info(f"📋 Current pending stop losses: {list(self.pending_stop_losses.keys())}")
|
|
|
+ for order_id in filled_order_ids:
|
|
|
+ if order_id in self.pending_stop_losses:
|
|
|
+ stop_loss_info = self.pending_stop_losses[order_id]
|
|
|
+ logger.info(f"🛑 Will process stop loss for filled order {order_id}: {stop_loss_info['token']} @ ${stop_loss_info['stop_price']}")
|
|
|
+
|
|
|
await self._process_filled_orders(filled_order_ids, current_positions)
|
|
|
|
|
|
# Process pending stop losses for filled orders
|
|
|
await self._process_pending_stop_losses(filled_order_ids)
|
|
|
+ else:
|
|
|
+ # Log if we have pending stop losses but no filled orders
|
|
|
+ if self.pending_stop_losses:
|
|
|
+ logger.debug(f"📋 No filled orders detected, but {len(self.pending_stop_losses)} pending stop losses remain")
|
|
|
|
|
|
# Update tracking data
|
|
|
self.last_known_orders = current_order_ids
|
|
@@ -2677,6 +2762,15 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
|
|
|
# Process new trades
|
|
|
for trade in new_trades:
|
|
|
+ # Log trade processing for debugging
|
|
|
+ trade_id = trade.get('id', 'external')
|
|
|
+ symbol = trade.get('symbol', 'Unknown')
|
|
|
+ side = trade.get('side', 'Unknown')
|
|
|
+ amount = trade.get('amount', 0)
|
|
|
+ price = trade.get('price', 0)
|
|
|
+
|
|
|
+ logger.info(f"🔍 Processing trade: {trade_id} - {side} {amount} {symbol} @ ${price}")
|
|
|
+
|
|
|
await self._process_external_trade(trade)
|
|
|
|
|
|
# Update last processed time (keep as datetime object)
|
|
@@ -2850,6 +2944,11 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
if not all([symbol, side, amount, price]):
|
|
|
return
|
|
|
|
|
|
+ # Skip bot-generated trades to prevent double processing
|
|
|
+ if trade_id in self.bot_trade_ids:
|
|
|
+ logger.debug(f"🤖 Skipping bot-generated trade: {trade_id}")
|
|
|
+ return
|
|
|
+
|
|
|
# Record trade in stats and get action type using enhanced tracking
|
|
|
action_type = self.stats.record_trade_with_enhanced_tracking(symbol, side, amount, price, trade_id, "external")
|
|
|
|
|
@@ -3979,7 +4078,13 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
|
|
|
})
|
|
|
|
|
|
logger.info(f"📈 Position updated: {symbol} LONG {position['contracts']:.6f} @ avg ${position['avg_entry_price']:.2f}")
|
|
|
- return 'long_opened' if old_contracts == 0 else 'long_increased'
|
|
|
+
|
|
|
+ # Check if this is truly a position open vs increase
|
|
|
+ # For very small previous positions (< 0.001), consider it a new position open
|
|
|
+ if old_contracts < 0.001:
|
|
|
+ return 'long_opened'
|
|
|
+ else:
|
|
|
+ return 'long_increased'
|
|
|
else:
|
|
|
# Reducing short position
|
|
|
reduction = min(amount, abs(position['contracts']))
|
|
@@ -4004,6 +4109,7 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
|
|
|
# Adding to short position or reducing long position
|
|
|
if position['contracts'] <= 0:
|
|
|
# Opening/adding to short position
|
|
|
+ old_contracts = abs(position['contracts'])
|
|
|
position['contracts'] -= amount
|
|
|
position['entry_count'] += 1
|
|
|
position['entry_history'].append({
|
|
@@ -4014,7 +4120,13 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
|
|
|
})
|
|
|
|
|
|
logger.info(f"📉 Position updated: {symbol} SHORT {abs(position['contracts']):.6f} @ ${price:.2f}")
|
|
|
- return 'short_opened' if position['contracts'] == -amount else 'short_increased'
|
|
|
+
|
|
|
+ # Check if this is truly a position open vs increase
|
|
|
+ # For very small previous positions (< 0.001), consider it a new position open
|
|
|
+ if old_contracts < 0.001:
|
|
|
+ return 'short_opened'
|
|
|
+ else:
|
|
|
+ return 'short_increased'
|
|
|
else:
|
|
|
# Reducing long position
|
|
|
reduction = min(amount, position['contracts'])
|
|
@@ -4347,29 +4459,124 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
|
|
|
)
|
|
|
|
|
|
async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle messages from custom keyboard buttons (without /)."""
|
|
|
+ """Handle messages from custom keyboard buttons."""
|
|
|
if not self.is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
return
|
|
|
|
|
|
message_text = update.message.text.lower()
|
|
|
|
|
|
- # Map clean button text to command handlers
|
|
|
- command_handlers = {
|
|
|
- 'daily': self.daily_command,
|
|
|
- 'performance': self.performance_command,
|
|
|
- 'balance': self.balance_command,
|
|
|
- 'stats': self.stats_command,
|
|
|
- 'positions': self.positions_command,
|
|
|
- 'orders': self.orders_command,
|
|
|
- 'price': self.price_command,
|
|
|
- 'market': self.market_command,
|
|
|
- 'help': self.help_command,
|
|
|
- 'commands': self.commands_command
|
|
|
+ # Map keyboard button text to commands
|
|
|
+ command_map = {
|
|
|
+ 'balance': '/balance',
|
|
|
+ 'positions': '/positions',
|
|
|
+ 'orders': '/orders',
|
|
|
+ 'stats': '/stats',
|
|
|
+ 'trades': '/trades',
|
|
|
+ 'market': '/market',
|
|
|
+ 'price': '/price',
|
|
|
+ 'help': '/help',
|
|
|
+ 'commands': '/commands',
|
|
|
+ 'monitoring': '/monitoring',
|
|
|
+ 'logs': '/logs',
|
|
|
+ 'performance': '/performance',
|
|
|
+ 'daily': '/daily',
|
|
|
+ 'weekly': '/weekly',
|
|
|
+ 'monthly': '/monthly',
|
|
|
+ 'risk': '/risk',
|
|
|
+ 'alarm': '/alarm',
|
|
|
+ 'keyboard': '/keyboard'
|
|
|
}
|
|
|
|
|
|
- # Execute the corresponding command handler
|
|
|
- if message_text in command_handlers:
|
|
|
- await command_handlers[message_text](update, context)
|
|
|
+ # Check if the message matches any keyboard command
|
|
|
+ if message_text in command_map:
|
|
|
+ # Create a fake update object with the corresponding command
|
|
|
+ update.message.text = command_map[message_text]
|
|
|
+ # Get the handler for this command and call it
|
|
|
+ handlers = self.application.handlers[0] # Get default group handlers
|
|
|
+ for handler in handlers:
|
|
|
+ if hasattr(handler, 'callback') and hasattr(handler, 'filters'):
|
|
|
+ if await handler.check_update(update):
|
|
|
+ await handler.callback(update, context)
|
|
|
+ return
|
|
|
+
|
|
|
+ # If no keyboard command matched, show a help message
|
|
|
+ await update.message.reply_text("❓ Unknown command. Use /help to see available commands.")
|
|
|
+
|
|
|
+ async def debug_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Debug command to show internal bot state."""
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Get bot state information
|
|
|
+ debug_info = f"🔧 <b>Bot Debug Information</b>\\n\\n"
|
|
|
+
|
|
|
+ # Bot version and status
|
|
|
+ debug_info += f"🤖 <b>Version:</b> {self.version}\\n"
|
|
|
+ debug_info += f"⚡ <b>Status:</b> Running\\n"
|
|
|
+
|
|
|
+ # Position tracking information
|
|
|
+ debug_info += f"\\n📊 <b>Position State Tracking:</b>\\n"
|
|
|
+ if hasattr(self, '_internal_positions') and self._internal_positions:
|
|
|
+ for symbol, pos_state in self._internal_positions.items():
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ contracts = pos_state.get('contracts', 0)
|
|
|
+ avg_price = pos_state.get('average_price', 0)
|
|
|
+ debug_info += f" • {token}: {contracts:.6f} @ ${avg_price:.2f}\\n"
|
|
|
+ else:
|
|
|
+ debug_info += f" No internal position tracking data\\n"
|
|
|
+
|
|
|
+ # Get raw Hyperliquid position data for comparison
|
|
|
+ debug_info += f"\\n🔍 <b>Raw Hyperliquid Position Data:</b>\\n"
|
|
|
+ raw_positions = self.client.get_positions()
|
|
|
+ if raw_positions:
|
|
|
+ for pos in raw_positions:
|
|
|
+ if float(pos.get('contracts', 0)) != 0:
|
|
|
+ symbol = pos.get('symbol', 'Unknown')
|
|
|
+ contracts = pos.get('contracts', 0)
|
|
|
+ side = pos.get('side', 'Unknown')
|
|
|
+ entry_price = pos.get('entryPrice', 0)
|
|
|
+ unrealized_pnl = pos.get('unrealizedPnl', 0)
|
|
|
+ notional = pos.get('notional', 0)
|
|
|
+
|
|
|
+ # Get raw Hyperliquid data from info field
|
|
|
+ raw_info = pos.get('info', {})
|
|
|
+ raw_position = raw_info.get('position', {}) if raw_info else {}
|
|
|
+ raw_szi = raw_position.get('szi', 'N/A')
|
|
|
+
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ debug_info += f" • <b>{token}:</b>\\n"
|
|
|
+ debug_info += f" - CCXT contracts: {contracts} (always positive)\\n"
|
|
|
+ debug_info += f" - CCXT side: {side}\\n"
|
|
|
+ debug_info += f" - Raw Hyperliquid szi: {raw_szi} (negative = short)\\n"
|
|
|
+ debug_info += f" - Entry price: ${entry_price}\\n"
|
|
|
+ debug_info += f" - Unrealized P&L: ${unrealized_pnl}\\n"
|
|
|
+ debug_info += f" - Notional: ${notional}\\n"
|
|
|
+
|
|
|
+ # Show what the bot would interpret this as
|
|
|
+ side_field = pos.get('side', '').lower()
|
|
|
+ if side_field in ['long', 'short']:
|
|
|
+ interpreted_direction = side_field.upper()
|
|
|
+ else:
|
|
|
+ interpreted_direction = "LONG" if float(contracts) > 0 else "SHORT"
|
|
|
+ debug_info += f" - Bot interprets as: {interpreted_direction} (using CCXT side field)\\n\\n"
|
|
|
+ else:
|
|
|
+ debug_info += f" No positions found or API error\\n"
|
|
|
+
|
|
|
+ # Show monitoring status
|
|
|
+ debug_info += f"\\n🔍 <b>Monitoring:</b> {'Active' if self.monitoring_active else 'Inactive'}\\n"
|
|
|
+ debug_info += f"📋 <b>Tracked Orders:</b> {len(self.last_known_orders)}\\n"
|
|
|
+ debug_info += f"🤖 <b>Bot Trade IDs:</b> {len(self.bot_trade_ids)}\\n"
|
|
|
+ if self.bot_trade_ids:
|
|
|
+ debug_info += " Recent bot trades: " + ", ".join(list(self.bot_trade_ids)[-5:]) + "\\n"
|
|
|
+
|
|
|
+ await update.message.reply_text(debug_info, parse_mode='HTML')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error in debug command: {e}")
|
|
|
+ await update.message.reply_text(f"❌ Debug error: {e}")
|
|
|
|
|
|
|
|
|
async def main_async():
|