Quellcode durchsuchen

Add /sync command for resynchronizing bot with exchange

- Introduced the /sync command to clear local data and resynchronize with the exchange.
- Implemented safety features to prevent accidental usage, requiring a "force" parameter for execution.
- Detailed the command's functionality, including data clearing, fetching exchange state, and preserving historical performance data.
- Updated documentation to include /sync force command and its usage examples in commands.md.
- Enhanced error handling and logging for the synchronization process, ensuring better traceability and user feedback.
Carles Sentis vor 19 Stunden
Ursprung
Commit
058e9bc471
4 geänderte Dateien mit 349 neuen und 35 gelöschten Zeilen
  1. 62 0
      docs/commands.md
  2. 3 0
      src/bot/core.py
  3. 283 34
      src/commands/management_commands.py
  4. 1 1
      trading_bot.py

+ 62 - 0
docs/commands.md

@@ -19,6 +19,7 @@ The most important commands to get started:
 /alarm BTC 50000 # Set price alert for BTC at $50,000
 /risk        # Advanced risk metrics
 /version     # Bot version and system info
+/sync force  # Resync bot with exchange (if out of sync)
 /help        # Full command reference
 ```
 
@@ -1221,6 +1222,66 @@ Show bot version and system information.
 ⏰ Current Time: 2024-12-15 14:25:30
 ```
 
+### `/sync force`
+Clear all local bot data and resynchronize with the exchange. This is useful if the bot gets out of sync with your exchange positions/orders.
+
+**Example:**
+```
+/sync force
+```
+
+**What it does:**
+1. **Clears local data:** Removes bot's internal tracking of trades, orders, and pending stop losses
+2. **Fetches exchange state:** Gets current positions and orders from the exchange
+3. **Recreates tracking:** Rebuilds internal position and order tracking
+4. **Preserves history:** Keeps completed trade statistics and performance data
+
+**Safety Features:**
+- Requires "force" parameter to prevent accidental usage
+- Shows warning message without "force" parameter
+- Preserves historical performance data
+- Maintains balance adjustment history
+
+**Use Cases:**
+- Bot tracking gets out of sync with exchange
+- After manual trading outside the bot
+- Recovery from system errors
+- Fresh start while preserving statistics
+
+**What gets cleared:**
+- Active position tracking
+- Pending order monitoring
+- Internal stop loss database
+- Order status tracking
+
+**What gets preserved:**
+- Completed trade history
+- Performance statistics
+- Balance adjustment records
+- Historical P&L data
+
+**Response:**
+```
+✅ Synchronization Complete!
+
+📊 Sync Results:
+• Positions Recreated: 2
+• Orders Linked: 3
+• Current Balance: $1,250.00
+
+🔄 What was reset:
+• Local trade tracking
+• Pending orders database
+• Order monitoring state
+
+🎯 What was preserved:
+• Historical performance data
+• Balance adjustment history
+• Completed trade statistics
+
+💡 The bot is now synchronized with your exchange state and ready for trading!
+```
+
 ### `/balance_adjustments`
 Show deposit and withdrawal history for accurate P&L tracking.
 
@@ -1307,6 +1368,7 @@ Show deposit and withdrawal history for accurate P&L tracking.
 /monitoring       # Check system status
 /orders           # All open orders
 /logs             # System health
+/sync force       # Resync with exchange
 ```
 
 ## ⚠️ Important Notes

+ 3 - 0
src/bot/core.py

@@ -159,6 +159,7 @@ class TelegramTradingBot:
         self.application.add_handler(CommandHandler("debug", self.management_commands.debug_command))
         self.application.add_handler(CommandHandler("version", self.management_commands.version_command))
         self.application.add_handler(CommandHandler("keyboard", self.management_commands.keyboard_command))
+        self.application.add_handler(CommandHandler("sync", self.management_commands.sync_command))
         
         # Callback and message handlers
         self.application.add_handler(CallbackQueryHandler(self.trading_commands.button_callback))
@@ -304,6 +305,7 @@ Type /help for detailed command information.
 <b>🔄 Order Monitoring:</b>
 • /monitoring - View monitoring status
 • /logs - View log file statistics and cleanup
+• /sync force - Resync bot with exchange (clears local data)
 
 <b>⚙️ Configuration:</b>
 • Default Token: {Config.DEFAULT_TRADING_TOKEN}
@@ -375,6 +377,7 @@ For support, contact your bot administrator.
 • /alarm [token] [price] - Set price alerts
 • /logs - View recent bot logs
 • /debug - Bot internal state (troubleshooting)
+• /sync force - Clear local data and resync with exchange
 
 For support or issues, check the logs or contact the administrator.
         """

+ 283 - 34
src/commands/management_commands.py

@@ -494,47 +494,296 @@ Will trigger when {token} price moves {alarm['direction']} {target_price_str}
             logger.error(f"Error in version command: {e}")
     
     async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /keyboard command to enable/show custom keyboard."""
+        """Handle the /keyboard command to show the main keyboard."""
         chat_id = update.effective_chat.id
         if not self._is_authorized(chat_id):
             await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
             return
+
+        # Define default keyboard layout
+        default_keyboard = [
+            [KeyboardButton("LONG"), KeyboardButton("SHORT"), KeyboardButton("EXIT")],
+            [KeyboardButton("BALANCE"), KeyboardButton("POSITIONS"), KeyboardButton("ORDERS")],
+            [KeyboardButton("STATS"), KeyboardButton("MARKET"), KeyboardButton("PERFORMANCE")],
+            [KeyboardButton("DAILY"), KeyboardButton("WEEKLY"), KeyboardButton("MONTHLY")],
+            [KeyboardButton("RISK"), KeyboardButton("ALARM"), KeyboardButton("MONITORING")],
+            [KeyboardButton("LOGS"), KeyboardButton("DEBUG"), KeyboardButton("VERSION")],
+            [KeyboardButton("COMMANDS"), KeyboardButton("KEYBOARD"), KeyboardButton("COO")]
+        ]
+
+        # Try to use custom keyboard from config if enabled
+        if Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED and Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT:
+            try:
+                # Parse layout from config: "cmd1,cmd2|cmd3,cmd4|cmd5,cmd6"
+                rows = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT.split('|')
+                keyboard = []
+                for row in rows:
+                    buttons = []
+                    for cmd in row.split(','):
+                        cmd = cmd.strip()
+                        # Remove leading slash if present and convert to button text
+                        if cmd.startswith('/'):
+                            cmd = cmd[1:]
+                        buttons.append(KeyboardButton(cmd.upper()))
+                    if buttons:  # Only add non-empty rows
+                        keyboard.append(buttons)
+            except Exception as e:
+                logger.warning(f"Error parsing custom keyboard layout: {e}, falling back to default")
+                keyboard = default_keyboard
+        else:
+            # Use default keyboard when custom keyboard is disabled
+            keyboard = default_keyboard
+        
+        reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
         
-        keyboard_enabled = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_ENABLED', False)
-        keyboard_layout_str = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_LAYOUT', '')
-
-        if keyboard_enabled and keyboard_layout_str:
-            rows_str = keyboard_layout_str.split('|')
-            keyboard = []
-            button_details_message = ""
-            for row_str in rows_str:
-                button_commands = row_str.split(',')
-                row_buttons = []
-                for cmd_text in button_commands:
-                    cmd_text = cmd_text.strip()
-                    if cmd_text: # Ensure not empty after strip
-                        row_buttons.append(KeyboardButton(cmd_text))
-                        # For the message, show the command and a brief description if possible
-                        # This part would require a mapping from command to description
-                        # For now, just list the command text.
-                        button_details_message += f"• {cmd_text}\n"
-                if row_buttons:
-                    keyboard.append(row_buttons)
+        await context.bot.send_message(
+            chat_id=chat_id, 
+            text="🎹 <b>Trading Bot Keyboard</b>\n\nUse the buttons below for quick access to commands:", 
+            reply_markup=reply_markup, 
+            parse_mode='HTML'
+        )
+
+    async def sync_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Handle the /sync command to clear local data and resync with exchange."""
+        chat_id = update.effective_chat.id
+        if not self._is_authorized(chat_id):
+            await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
+            return
+
+        try:
+            # Get confirmation from user unless force flag is provided
+            force_sync = len(context.args) > 0 and context.args[0].lower() == "force"
             
-            if not keyboard:
-                await context.bot.send_message(chat_id=chat_id, text="⚠️ Custom keyboard layout is empty or invalid in config.", parse_mode='HTML')
+            if not force_sync:
+                # Send confirmation message
+                await context.bot.send_message(
+                    chat_id=chat_id,
+                    text=(
+                        "⚠️ <b>Data Synchronization Warning</b>\n\n"
+                        "This will:\n"
+                        "• Clear ALL local trades, orders, and pending stop losses\n"
+                        "• Reset bot tracking state\n"
+                        "• Sync with current exchange positions/orders\n"
+                        "• Preserve balance and performance history\n\n"
+                        "🔄 Use <code>/sync force</code> to proceed\n"
+                        "❌ Use any other command to cancel"
+                    ),
+                    parse_mode='HTML'
+                )
                 return
 
-            reply_markup = ReplyKeyboardMarkup(
-                keyboard,
-                resize_keyboard=True,
-                one_time_keyboard=False, # Persistent keyboard
-                selective=True
+            # Send processing message
+            processing_msg = await context.bot.send_message(
+                chat_id=chat_id,
+                text="🔄 <b>Synchronizing with Exchange...</b>\n\n⏳ Step 1/5: Clearing local data...",
+                parse_mode='HTML'
+            )
+
+            # Step 1: Clear local trading data
+            stats = self.trading_engine.get_stats()
+            if stats:
+                # Clear trades table (keep only position_closed for history)
+                stats.db_manager._execute_query(
+                    "DELETE FROM trades WHERE status IN ('pending', 'executed', 'position_opened', 'cancelled')"
+                )
+                
+                # Clear orders table (keep only filled/cancelled for history)
+                stats.db_manager._execute_query(
+                    "DELETE FROM orders WHERE status IN ('pending_submission', 'open', 'submitted', 'pending_trigger')"
+                )
+                
+                logger.info("🧹 Cleared local trading state from database")
+
+            # Update status
+            await context.bot.edit_message_text(
+                chat_id=chat_id,
+                message_id=processing_msg.message_id,
+                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n⏳ Step 2/5: Clearing pending stop losses...",
+                parse_mode='HTML'
+            )
+
+            # Step 2: Clear pending stop loss orders
+            try:
+                if hasattr(self.monitoring_coordinator, 'pending_orders_manager'):
+                    pending_manager = self.monitoring_coordinator.pending_orders_manager
+                    if pending_manager and hasattr(pending_manager, 'db_path'):
+                        import sqlite3
+                        with sqlite3.connect(pending_manager.db_path) as conn:
+                            conn.execute("DELETE FROM pending_stop_loss WHERE status IN ('pending', 'placed')")
+                            conn.commit()
+                        logger.info("🧹 Cleared pending stop loss orders")
+            except Exception as e:
+                logger.warning(f"Could not clear pending orders: {e}")
+
+            # Update status
+            await context.bot.edit_message_text(
+                chat_id=chat_id,
+                message_id=processing_msg.message_id,
+                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n⏳ Step 3/5: Fetching exchange state...",
+                parse_mode='HTML'
+            )
+
+            # Step 3: Fetch current exchange state
+            exchange_positions = self.trading_engine.get_positions() or []
+            exchange_orders = self.trading_engine.get_orders() or []
+            
+            # Update status
+            await context.bot.edit_message_text(
+                chat_id=chat_id,
+                message_id=processing_msg.message_id,
+                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n⏳ Step 4/5: Recreating position tracking...",
+                parse_mode='HTML'
             )
+
+            # Step 4: Recreate position tracking for open positions
+            positions_synced = 0
+            orders_synced = 0
+            
+            if stats:
+                # Create trade lifecycles for open positions
+                for position in exchange_positions:
+                    try:
+                        size = float(position.get('contracts', 0))
+                        if abs(size) > 1e-8:  # Position exists
+                            symbol = position.get('symbol', '')
+                            if symbol:
+                                # Extract token from symbol (e.g., "BTC/USDC:USDC" -> "BTC")
+                                token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                                
+                                # Create a new trade lifecycle for this existing position
+                                side = 'buy' if size > 0 else 'sell'
+                                position_side = 'long' if size > 0 else 'short'
+                                entry_price = float(position.get('entryPrice', 0))
+                                
+                                if entry_price > 0:  # Valid position data
+                                    # Create the lifecycle entry (this generates and returns lifecycle_id)
+                                    lifecycle_id = stats.create_trade_lifecycle(
+                                        symbol=symbol,
+                                        side=side,
+                                        stop_loss_price=None,  # Will be detected from orders
+                                        trade_type='sync'
+                                    )
+                                    
+                                    if lifecycle_id:
+                                        # Update to position opened status
+                                        await stats.update_trade_position_opened(
+                                            lifecycle_id=lifecycle_id,
+                                            entry_price=entry_price,
+                                            entry_amount=abs(size),
+                                            exchange_fill_id=f"sync_{lifecycle_id[:8]}"
+                                        )
+                                        
+                                        # Update with current market data
+                                        unrealized_pnl = float(position.get('unrealizedPnl', 0))
+                                        mark_price = float(position.get('markPrice', entry_price))
+                                        
+                                        stats.update_trade_market_data(
+                                            trade_lifecycle_id=lifecycle_id,
+                                            current_position_size=abs(size),
+                                            unrealized_pnl=unrealized_pnl,
+                                            mark_price=mark_price,
+                                            position_value=abs(size) * mark_price
+                                        )
+                                        
+                                        positions_synced += 1
+                                        logger.info(f"🔄 Recreated position tracking for {token}: {position_side} {abs(size)} @ {entry_price}")
+                    
+                    except Exception as e:
+                        logger.error(f"Error recreating position for {position}: {e}")
+
+                # Link existing orders to positions
+                for order in exchange_orders:
+                    try:
+                        order_symbol = order.get('symbol', '')
+                        order_id = order.get('id', '')
+                        order_type = order.get('type', '').lower()
+                        is_reduce_only = order.get('reduceOnly', False)
+                        
+                        if order_symbol and order_id and is_reduce_only:
+                            # This might be a stop loss or take profit order
+                            # Find the corresponding position
+                            matching_trade = stats.get_trade_by_symbol_and_status(order_symbol, 'position_opened')
+                            if matching_trade:
+                                lifecycle_id = matching_trade.get('trade_lifecycle_id')
+                                if lifecycle_id:
+                                    order_price = float(order.get('price', 0))
+                                    stop_price = float(order.get('stopPrice', 0)) or order_price
+                                    
+                                    if 'stop' in order_type and stop_price > 0:
+                                        # Link as stop loss
+                                        await stats.link_stop_loss_to_trade(lifecycle_id, order_id, stop_price)
+                                        orders_synced += 1
+                                        logger.info(f"🔗 Linked stop loss order {order_id} to position {lifecycle_id[:8]}")
+                                    elif order_type in ['limit', 'take_profit'] and order_price > 0:
+                                        # Link as take profit
+                                        await stats.link_take_profit_to_trade(lifecycle_id, order_id, order_price)
+                                        orders_synced += 1
+                                        logger.info(f"🔗 Linked take profit order {order_id} to position {lifecycle_id[:8]}")
+                    
+                    except Exception as e:
+                        logger.error(f"Error linking order {order}: {e}")
+
+            # Update status
+            await context.bot.edit_message_text(
+                chat_id=chat_id,
+                message_id=processing_msg.message_id,
+                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n✅ Step 4/5: Position tracking recreated\n⏳ Step 5/5: Updating balance...",
+                parse_mode='HTML'
+            )
+
+            # Step 5: Update current balance
+            current_balance = 0.0
+            try:
+                balance_data = self.trading_engine.get_balance()
+                if balance_data and balance_data.get('total'):
+                    current_balance = float(balance_data['total'].get('USDC', 0))
+                    if stats:
+                        await stats.record_balance_snapshot(current_balance, notes="Post-sync balance")
+            except Exception as e:
+                logger.warning(f"Could not update balance after sync: {e}")
+
+            # Final success message
+            formatter = get_formatter()
+            success_message = f"""
+✅ <b>Synchronization Complete!</b>
+
+📊 <b>Sync Results:</b>
+• Positions Recreated: {positions_synced}
+• Orders Linked: {orders_synced}
+• Current Balance: {await formatter.format_price_with_symbol(current_balance)}
+
+🔄 <b>What was reset:</b>
+• Local trade tracking
+• Pending orders database
+• Order monitoring state
+
+🎯 <b>What was preserved:</b>
+• Historical performance data
+• Balance adjustment history
+• Completed trade statistics
+
+💡 The bot is now synchronized with your exchange state and ready for trading!
+            """
+
+            await context.bot.edit_message_text(
+                chat_id=chat_id,
+                message_id=processing_msg.message_id,
+                text=success_message.strip(),
+                parse_mode='HTML'
+            )
+
+            logger.info(f"🎉 Sync completed: {positions_synced} positions, {orders_synced} orders linked")
+
+        except Exception as e:
+            error_message = f"❌ <b>Sync Failed</b>\n\nError: {str(e)}\n\n💡 Check logs for details."
+            try:
+                await context.bot.edit_message_text(
+                    chat_id=chat_id,
+                    message_id=processing_msg.message_id,
+                    text=error_message,
+                    parse_mode='HTML'
+                )
+            except:
+                await context.bot.send_message(chat_id=chat_id, text=error_message, parse_mode='HTML')
             
-            message_text = f"⌨️ <b>Custom Keyboard Activated!</b>\n\n🎯 <b>Your quick buttons based on config are ready:</b>\n{button_details_message}\n💡 <b>How to use:</b>\nTap any button below to send the command instantly!\n\n🔧 These buttons will stay at the bottom of your chat."
-            await context.bot.send_message(chat_id=chat_id, text=message_text, reply_markup=reply_markup, parse_mode='HTML')
-        elif not keyboard_enabled:
-            await context.bot.send_message(chat_id=chat_id, text="❌ <b>Custom Keyboard Disabled</b>\n\n🔧 <b>To enable:</b>\n• Set TELEGRAM_CUSTOM_KEYBOARD_ENABLED=true in your .env file\n• Restart the bot\n• Run /keyboard again\n\n📋 <b>Current config:</b>\n• Enabled: {keyboard_enabled}", parse_mode='HTML')
-        else: # keyboard_enabled is true, but layout_str is empty
-            await context.bot.send_message(chat_id=chat_id, text="⚠️ Custom keyboard is enabled but the layout string (TELEGRAM_CUSTOM_KEYBOARD_LAYOUT) is empty in your configuration. Please define a layout.", parse_mode='HTML') 
+            logger.error(f"Error in sync command: {e}", exc_info=True) 

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.6.284"
+BOT_VERSION = "2.6.285"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))