Selaa lähdekoodia

Enhance bot state management in Telegram bot - Implemented persistent storage for bot state, including pending stop losses and order tracking, allowing for state restoration across sessions. Added functionality to handle custom keyboard commands and improved logging for state changes, ensuring a more robust user experience.

Carles Sentis 5 päivää sitten
vanhempi
sitoutus
9f8ab97bb9
2 muutettua tiedostoa jossa 214 lisäystä ja 4 poistoa
  1. 14 1
      reset_data.sh
  2. 200 3
      src/telegram_bot.py

+ 14 - 1
reset_data.sh

@@ -23,9 +23,19 @@ fi
 rm -f trading_stats.json.backup*
 echo "✅ Removed stats backups"
 
+# Remove bot state persistence file
+if [ -f "bot_state.json" ]; then
+    rm bot_state.json
+    echo "✅ Removed bot_state.json"
+fi
+
 # Remove alarm data
+if [ -f "price_alarms.json" ]; then
+    rm price_alarms.json
+    echo "✅ Removed price_alarms.json"
+fi
 rm -f alarms.json alarms.db
-echo "✅ Removed alarm data"
+echo "✅ Removed legacy alarm data"
 
 # Remove log files
 rm -f *.log trading_bot.log*
@@ -49,6 +59,7 @@ echo "📝 What was cleared:"
 echo "   • Trading statistics and P&L history"
 echo "   • All completed trade records"
 echo "   • Daily/weekly/monthly performance data"
+echo "   • Bot state persistence (pending stop losses, order tracking)"
 echo "   • Price alarms and notifications"
 echo "   • Log files and debugging data"
 echo "   • Enhanced position tracking data"
@@ -57,6 +68,8 @@ echo "🚀 Your bot is now ready for a fresh start!"
 echo "   • Initial balance will be reset"
 echo "   • All stats will start from zero"
 echo "   • Position tracking will reinitialize"
+echo "   • Bot state persistence will restart fresh"
+echo "   • Pending stop losses will be cleared"
 echo ""
 echo "💡 Next steps:"
 echo "   1. Run your bot: python src/telegram_bot.py"

+ 200 - 3
src/telegram_bot.py

@@ -9,8 +9,10 @@ with comprehensive statistics tracking and phone-friendly controls.
 import logging
 import asyncio
 import re
+import json
+import os
 from datetime import datetime, timedelta
-from typing import Optional, Dict, Any
+from typing import Optional, Dict, Any, List, Tuple
 from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
 from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
 from hyperliquid_client import HyperliquidClient
@@ -39,6 +41,9 @@ class TelegramTradingBot:
         self.stats = None
         self.version = "Unknown"  # Will be set by launcher
         
+        # Bot state persistence file
+        self.bot_state_file = "bot_state.json"
+        
         # Order monitoring attributes
         self.monitoring_active = False
         self.last_known_orders = set()  # Track order IDs we've seen
@@ -57,9 +62,77 @@ 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}}
         
-        # Initialize stats
+        # Load bot state first, then initialize stats
+        self._load_bot_state()
         self._initialize_stats()
     
+    def _load_bot_state(self):
+        """Load bot state from disk."""
+        try:
+            if os.path.exists(self.bot_state_file):
+                with open(self.bot_state_file, 'r') as f:
+                    state_data = json.load(f)
+                
+                # Restore critical state
+                self.pending_stop_losses = state_data.get('pending_stop_losses', {})
+                self.last_known_orders = set(state_data.get('last_known_orders', []))
+                self.last_known_positions = state_data.get('last_known_positions', {})
+                
+                # Restore timestamps (convert from ISO string if present)
+                last_trade_time = state_data.get('last_processed_trade_time')
+                if last_trade_time:
+                    try:
+                        self.last_processed_trade_time = datetime.fromisoformat(last_trade_time)
+                    except (ValueError, TypeError):
+                        self.last_processed_trade_time = None
+                
+                last_deposit_check = state_data.get('last_deposit_withdrawal_check')
+                if last_deposit_check:
+                    try:
+                        self.last_deposit_withdrawal_check = datetime.fromisoformat(last_deposit_check)
+                    except (ValueError, TypeError):
+                        self.last_deposit_withdrawal_check = None
+                
+                logger.info(f"🔄 Restored bot state: {len(self.pending_stop_losses)} pending stop losses, {len(self.last_known_orders)} tracked orders")
+                
+                # Log details about restored pending stop losses
+                if self.pending_stop_losses:
+                    for order_id, stop_loss_info in self.pending_stop_losses.items():
+                        token = stop_loss_info.get('token', 'Unknown')
+                        stop_price = stop_loss_info.get('stop_price', 0)
+                        order_type = stop_loss_info.get('order_type', 'Unknown')
+                        logger.info(f"📋 Restored pending stop loss: {order_id} -> {token} {order_type} @ ${stop_price}")
+                        
+        except Exception as e:
+            logger.error(f"❌ Error loading bot state: {e}")
+            # Initialize with defaults
+            self.pending_stop_losses = {}
+            self.last_known_orders = set()
+            self.last_known_positions = {}
+            self.last_processed_trade_time = None
+            self.last_deposit_withdrawal_check = None
+    
+    def _save_bot_state(self):
+        """Save bot state to disk."""
+        try:
+            state_data = {
+                '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,
+                'last_processed_trade_time': self.last_processed_trade_time.isoformat() if self.last_processed_trade_time else None,
+                'last_deposit_withdrawal_check': self.last_deposit_withdrawal_check.isoformat() if self.last_deposit_withdrawal_check else None,
+                'last_updated': datetime.now().isoformat(),
+                'version': self.version
+            }
+            
+            with open(self.bot_state_file, 'w') as f:
+                json.dump(state_data, f, indent=2, default=str)
+                
+            logger.debug(f"💾 Saved bot state: {len(self.pending_stop_losses)} pending stop losses")
+            
+        except Exception as e:
+            logger.error(f"❌ Error saving bot state: {e}")
+
     def _initialize_stats(self):
         """Initialize stats with current balance."""
         try:
@@ -105,7 +178,7 @@ class TelegramTradingBot:
             for row in rows:
                 commands = [cmd.strip() for cmd in row.split(',') if cmd.strip()]
                 if commands:
-                    keyboard.append([KeyboardButton(cmd) for cmd in commands])
+                    keyboard.append([KeyboardButton(cmd.lstrip('/').capitalize()) for cmd in commands])
             
             if keyboard:
                 return ReplyKeyboardMarkup(
@@ -960,6 +1033,7 @@ Tap any button below for instant access to bot functions:
                         'original_order_id': order_id,
                         'is_limit': is_limit
                     }
+                    self._save_bot_state()  # Save state after adding pending stop loss
                     logger.info(f"💾 Saved pending stop loss for order {order_id}: sell {token_amount:.6f} {token} @ ${stop_loss_price}")
                 
                 success_message = f"""
@@ -1046,6 +1120,7 @@ Tap any button below for instant access to bot functions:
                         'original_order_id': order_id,
                         'is_limit': is_limit
                     }
+                    self._save_bot_state()  # Save state after adding pending stop loss
                     logger.info(f"💾 Saved pending stop loss for order {order_id}: buy {token_amount:.6f} {token} @ ${stop_loss_price}")
                 
                 success_message = f"""
@@ -1313,6 +1388,31 @@ Tap any button below for instant access to bot functions:
             await query.edit_message_text(error_message)
             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 /)."""
+        if not self.is_authorized(update.effective_chat.id):
+            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
+        }
+        
+        # Execute the corresponding command handler
+        if message_text in command_handlers:
+            await command_handlers[message_text](update, context)
+
     async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
         """Handle unknown commands."""
         if not self.is_authorized(update.effective_chat.id):
@@ -1356,10 +1456,14 @@ Tap any button below for instant access to bot functions:
         self.application.add_handler(CommandHandler("risk", self.risk_command))
         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))
         
         # Callback query handler for inline keyboards
         self.application.add_handler(CallbackQueryHandler(self.button_callback))
         
+        # Handle clean keyboard button messages (without /)
+        self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_keyboard_message))
+        
         # Handle unknown commands
         self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
     
@@ -2240,6 +2344,9 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
             self.last_known_orders = current_order_ids
             await self._update_position_tracking(current_positions)
             
+            # Save state after updating tracking data
+            self._save_bot_state()
+            
             # Check price alarms
             await self._check_price_alarms()
             
@@ -2262,6 +2369,7 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
     async def _process_pending_stop_losses(self, filled_order_ids: set):
         """Process pending stop losses for filled orders."""
         try:
+            processed_any = False
             for order_id in filled_order_ids:
                 if order_id in self.pending_stop_losses:
                     stop_loss_info = self.pending_stop_losses[order_id]
@@ -2271,7 +2379,12 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
                     
                     # Remove from pending after processing
                     del self.pending_stop_losses[order_id]
+                    processed_any = True
                     logger.info(f"🗑️ Removed processed stop loss for order {order_id}")
+            
+            # Save state if any stop losses were processed
+            if processed_any:
+                self._save_bot_state()
                     
         except Exception as e:
             logger.error(f"❌ Error processing pending stop losses: {e}")
@@ -2338,6 +2451,10 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
                 # Remove from pending
                 del self.pending_stop_losses[order_id]
                 logger.info(f"🗑️ Removed pending stop loss for cancelled order {order_id}")
+            
+            # Save state if any stop losses were removed
+            if orders_to_remove:
+                self._save_bot_state()
                 
         except Exception as e:
             logger.error(f"❌ Error cleaning up cancelled stop losses: {e}")
@@ -2527,6 +2644,9 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
             # Update last processed time
             self.last_processed_trade_time = latest_trade_time
             
+            # Save state after updating last processed time
+            self._save_bot_state()
+            
             if new_trades:
                 logger.info(f"📊 Processed {len(new_trades)} external trades")
                 
@@ -2615,6 +2735,9 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
             # Update last check time
             self.last_deposit_withdrawal_check = current_time
             
+            # Save state after updating last check time
+            self._save_bot_state()
+            
             if new_deposits > 0 or new_withdrawals > 0:
                 logger.info(f"💰 Processed {new_deposits} deposits and {new_withdrawals} withdrawals")
                 
@@ -2989,6 +3112,14 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
 • 🛑 Automatic Stop Loss Triggers
 • 🛑 Order-based Stop Loss Placement
 
+💾 <b>Bot State Persistence:</b>
+• Pending Stop Losses: Saved to disk
+• Order Tracking: Saved to disk  
+• External Trade Times: Saved to disk
+• Deposit Check Times: Saved to disk
+• State File: bot_state.json
+• Survives Bot Restarts: ✅ Yes
+
 ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
 
 💡 <b>Monitoring Features:</b>
@@ -4136,6 +4267,72 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
         
         self.last_known_positions = new_position_map
 
+    async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Handle the /keyboard command to enable/show custom keyboard."""
+        if not self.is_authorized(update.effective_chat.id):
+            await update.message.reply_text("❌ Unauthorized access.")
+            return
+        
+        custom_keyboard = self._create_custom_keyboard()
+        
+        if custom_keyboard:
+            await update.message.reply_text(
+                "⌨️ <b>Custom Keyboard Activated!</b>\n\n"
+                "🎯 <b>Your quick buttons are now ready:</b>\n"
+                "• Daily - Daily performance\n"
+                "• Performance - Performance stats\n"
+                "• Balance - Account balance\n"
+                "• Stats - Trading statistics\n"
+                "• Positions - Open positions\n"
+                "• Orders - Active orders\n"
+                "• Price - Quick price check\n"
+                "• Market - Market overview\n"
+                "• Help - Help guide\n"
+                "• Commands - Command menu\n\n"
+                "💡 <b>How to use:</b>\n"
+                "Tap any button below instead of typing the command manually!\n\n"
+                "🔧 These buttons will stay at the bottom of your chat.",
+                parse_mode='HTML',
+                reply_markup=custom_keyboard
+            )
+        else:
+            await update.message.reply_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"
+                f"📋 <b>Current config:</b>\n"
+                f"• Enabled: {Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED}\n"
+                f"• Layout: {Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT}",
+                parse_mode='HTML'
+            )
+
+    async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Handle messages from custom keyboard buttons (without /)."""
+        if not self.is_authorized(update.effective_chat.id):
+            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
+        }
+        
+        # Execute the corresponding command handler
+        if message_text in command_handlers:
+            await command_handlers[message_text](update, context)
+
 
 async def main_async():
     """Async main entry point for the Telegram bot."""