Browse Source

Refactor trading bot components to enhance async functionality and improve logging. Update requirements to include additional libraries for better performance and error handling. Adjust banner display to include network and database status, and refine error messages for database issues. Ensure consistent formatting in notifications and position tracking for improved clarity.

Carles Sentis 3 days ago
parent
commit
b15cb9e236
38 changed files with 591 additions and 3136 deletions
  1. 8 6
      requirements.txt
  2. 2 0
      src/__init__.py
  3. 7 12
      src/bot/core.py
  4. 52 38
      src/config/config.py
  5. 80 60
      src/config/logging_config.py
  6. 22 22
      src/monitoring/external_event_monitor.py
  7. 2 2
      src/monitoring/simple_position_tracker.py
  8. 25 25
      src/notifications/notification_manager.py
  9. 6 6
      src/stats/aggregation_manager.py
  10. 18 29
      src/stats/database_manager.py
  11. 8 8
      src/stats/trade_lifecycle_manager.py
  12. 169 199
      src/stats/trading_stats.py
  13. 27 12
      src/trading/trading_engine.py
  14. 70 68
      src/utils/token_display_formatter.py
  15. 1 1
      tests/__init__.py
  16. 0 104
      tests/run_all_tests.py
  17. 0 257
      tests/test_alarm_system.py
  18. 0 111
      tests/test_balance.py
  19. 0 162
      tests/test_bot_fixes.py
  20. 0 101
      tests/test_config.py
  21. 0 21
      tests/test_db_mark_price.py
  22. 0 130
      tests/test_exit_command.py
  23. 0 58
      tests/test_heartbeat_config.py
  24. 0 110
      tests/test_integrated_tracking.py
  25. 0 104
      tests/test_logging_system.py
  26. 0 147
      tests/test_order_management.py
  27. 0 177
      tests/test_order_monitoring.py
  28. 0 210
      tests/test_performance_command.py
  29. 0 1
      tests/test_period_commands.py
  30. 0 142
      tests/test_period_stats_consistency.py
  31. 0 91
      tests/test_perps_commands.py
  32. 0 114
      tests/test_position_roe.py
  33. 0 111
      tests/test_positions_display.py
  34. 0 150
      tests/test_risk_management.py
  35. 0 111
      tests/test_stats_fix.py
  36. 0 128
      tests/test_stop_loss_config.py
  37. 0 49
      tests/test_update_and_check_mark_price.py
  38. 94 59
      trading_bot.py

+ 8 - 6
requirements.txt

@@ -1,6 +1,8 @@
-hyperliquid==0.4.66
-python-telegram-bot[webhooks]==20.7
-python-dotenv==1.1.0
-pandas==2.2.3
-numpy==2.2.6
-psutil==7.0.0 
+python-telegram-bot
+python-dotenv
+numpy
+pandas
+psutil
+requests
+aiohttp
+hyperliquid 

+ 2 - 0
src/__init__.py

@@ -1 +1,3 @@
+# This file makes the src directory a package.
+
 # Trading Bot Package 

+ 7 - 12
src/bot/core.py

@@ -280,7 +280,7 @@ For support, contact your bot administrator.
             logger.error(f"Error sending welcome message in /start: {e}")
     
     async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /help command."""
+        """Handle the /help command, providing detailed command information."""
         logger.info(f"/help command triggered by chat_id: {update.effective_chat.id}")
         logger.debug(f"Full Update object in help_command: {update}")
 
@@ -331,21 +331,16 @@ For support or issues, check the logs or contact the administrator.
             logger.error(f"Error sending help message in /help: {e}")
     
     async def run(self):
-        """Run the bot."""
-        if not Config.TELEGRAM_BOT_TOKEN:
-            logger.error("❌ TELEGRAM_BOT_TOKEN not configured")
-            return
+        """Run the bot, including all setup and the main polling loop."""
+        logger.info(f"Starting bot version {self.version}...")
         
-        if not Config.TELEGRAM_CHAT_ID:
-            logger.error("❌ TELEGRAM_CHAT_ID not configured")
-            return
+        # Perform async initialization for components that need it
+        await self.trading_engine.async_init()
         
-        logger.info(f"🔧 Using python-telegram-bot version: {telegram.__version__} (Running in v20.x style)")
-
-        # Create application
+        # Initialize Telegram Application
         self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
         
-        # Connect notification manager to the bot application
+        # Pass application to notification manager
         self.notification_manager.set_bot_application(self.application)
         
         # Set up handlers

+ 52 - 38
src/config/config.py

@@ -8,43 +8,47 @@ load_dotenv()
 
 logger = logging.getLogger(__name__)
 
+def get_bool_env(var_name: str, default: str = 'false') -> bool:
+    """Gets a boolean environment variable."""
+    return os.getenv(var_name, default,).lower() in ('true', '1', 't')
+
 class Config:
     """Configuration class for the Hyperliquid trading bot."""
     
     # Hyperliquid API Configuration
     HYPERLIQUID_SECRET_KEY: Optional[str] = os.getenv('HYPERLIQUID_SECRET_KEY')    # API generator key
     HYPERLIQUID_WALLET_ADDRESS: Optional[str] = os.getenv('HYPERLIQUID_WALLET_ADDRESS')  # Wallet address
-    HYPERLIQUID_TESTNET: bool = os.getenv('HYPERLIQUID_TESTNET', 'true').lower() == 'true'
+    HYPERLIQUID_TESTNET: bool = get_bool_env('HYPERLIQUID_TESTNET', 'true')
     
     # Trading Bot Configuration
     DEFAULT_TRADING_TOKEN: str = os.getenv('DEFAULT_TRADING_TOKEN', 'BTC')
-    RISK_MANAGEMENT_ENABLED: bool = os.getenv('RISK_MANAGEMENT_ENABLED', 'true').lower() == 'true'
+    RISK_MANAGEMENT_ENABLED: bool = get_bool_env('RISK_MANAGEMENT_ENABLED', 'true')
     STOP_LOSS_PERCENTAGE: float = float(os.getenv('STOP_LOSS_PERCENTAGE', '10.0'))
     
     # Telegram Bot Configuration
     TELEGRAM_BOT_TOKEN: Optional[str] = os.getenv('TELEGRAM_BOT_TOKEN')
     TELEGRAM_CHAT_ID: Optional[str] = os.getenv('TELEGRAM_CHAT_ID')
-    TELEGRAM_ENABLED: bool = os.getenv('TELEGRAM_ENABLED', 'true').lower() == 'true'
-    TELEGRAM_DROP_PENDING_UPDATES: bool = os.getenv('TELEGRAM_DROP_PENDING_UPDATES', 'true').lower() == 'true'
+    TELEGRAM_ENABLED: bool = get_bool_env('TELEGRAM_ENABLED', 'true')
+    TELEGRAM_DROP_PENDING_UPDATES: bool = get_bool_env('TELEGRAM_DROP_PENDING_UPDATES', 'true')
     
     # Custom Keyboard Configuration
-    TELEGRAM_CUSTOM_KEYBOARD_ENABLED: bool = os.getenv('TELEGRAM_CUSTOM_KEYBOARD_ENABLED', 'true').lower() == 'true'
+    TELEGRAM_CUSTOM_KEYBOARD_ENABLED: bool = get_bool_env('TELEGRAM_CUSTOM_KEYBOARD_ENABLED', 'true')
     TELEGRAM_CUSTOM_KEYBOARD_LAYOUT: str = os.getenv('TELEGRAM_CUSTOM_KEYBOARD_LAYOUT', '/daily,/performance,/balance|/stats,/positions,/orders|/price,/market,/help,/commands')
     
     # Bot settings
-    BOT_HEARTBEAT_SECONDS = int(os.getenv('BOT_HEARTBEAT_SECONDS', '5'))
+    BOT_HEARTBEAT_SECONDS: int = int(os.getenv('BOT_HEARTBEAT_SECONDS', '5'))
     
     # Market Monitor settings
-    MARKET_MONITOR_CLEANUP_INTERVAL_HEARTBEATS = 120 # Approx every 10 minutes if heartbeat is 5s
+    MARKET_MONITOR_CLEANUP_INTERVAL_HEARTBEATS: int = 120 # Approx every 10 minutes if heartbeat is 5s
     
     # Order settings
-    DEFAULT_SLIPPAGE = 0.005  # 0.5%
+    DEFAULT_SLIPPAGE: float = 0.005  # 0.5%
     
     # Logging
-    LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
+    LOG_LEVEL: str = os.getenv('LOG_LEVEL', 'INFO').upper()
     
     # Log file configuration
-    LOG_TO_FILE: bool = os.getenv('LOG_TO_FILE', 'true').lower() == 'true'
+    LOG_TO_FILE: bool = get_bool_env('LOG_TO_FILE', 'true')
     LOG_FILE_PATH: str = os.getenv('LOG_FILE_PATH', 'logs/trading_bot.log')
     LOG_MAX_SIZE_MB: int = int(os.getenv('LOG_MAX_SIZE_MB', '10'))
     LOG_BACKUP_COUNT: int = int(os.getenv('LOG_BACKUP_COUNT', '5'))
@@ -54,53 +58,63 @@ class Config:
     @classmethod
     def validate(cls) -> bool:
         """Validate all configuration settings."""
-        is_valid = True
-        
-        # Validate Hyperliquid settings
+        validators = [
+            cls._validate_hyperliquid,
+            cls._validate_telegram,
+            cls._validate_bot_settings,
+            cls._validate_logging
+        ]
+        return all(validator() for validator in validators)
+
+    @classmethod
+    def _validate_hyperliquid(cls) -> bool:
+        """Validate Hyperliquid settings."""
         if not cls.HYPERLIQUID_WALLET_ADDRESS:
             logger.error("❌ HYPERLIQUID_WALLET_ADDRESS is required")
-            is_valid = False
-        
+            return False
         if not cls.HYPERLIQUID_SECRET_KEY:
             logger.error("❌ HYPERLIQUID_SECRET_KEY (API generator key) is required")
-            is_valid = False
-        
-        # Validate Telegram settings (if enabled)
+            return False
+        return True
+
+    @classmethod
+    def _validate_telegram(cls) -> bool:
+        """Validate Telegram settings."""
         if cls.TELEGRAM_ENABLED:
             if not cls.TELEGRAM_BOT_TOKEN:
                 logger.error("❌ TELEGRAM_BOT_TOKEN is required when Telegram is enabled")
-                is_valid = False
-            
+                return False
             if not cls.TELEGRAM_CHAT_ID:
                 logger.error("❌ TELEGRAM_CHAT_ID is required when Telegram is enabled")
-                is_valid = False
-        
-        # Validate heartbeat interval
+                return False
+        return True
+
+    @classmethod
+    def _validate_bot_settings(cls) -> bool:
+        """Validate general bot settings."""
         if cls.BOT_HEARTBEAT_SECONDS < 5:
             logger.error("❌ BOT_HEARTBEAT_SECONDS must be at least 5 seconds to avoid rate limiting")
-            is_valid = False
-        elif cls.BOT_HEARTBEAT_SECONDS > 600:
+            return False
+        if cls.BOT_HEARTBEAT_SECONDS > 600:
             logger.warning("⚠️ BOT_HEARTBEAT_SECONDS is very high (>10 minutes), monitoring may be slow")
-        
-        # Validate trading settings
-        if cls.DEFAULT_TRADING_TOKEN == '':
+        if not cls.DEFAULT_TRADING_TOKEN:
             logger.error("❌ DEFAULT_TRADING_TOKEN must be set")
-            is_valid = False
-        
-        # Validate logging settings
+            return False
+        return True
+
+    @classmethod
+    def _validate_logging(cls) -> bool:
+        """Validate logging settings."""
         if cls.LOG_MAX_SIZE_MB <= 0:
             logger.error("❌ LOG_MAX_SIZE_MB must be positive")
-            is_valid = False
-        
+            return False
         if cls.LOG_BACKUP_COUNT < 0:
             logger.error("❌ LOG_BACKUP_COUNT must be non-negative")
-            is_valid = False
-        
+            return False
         if cls.LOG_ROTATION_TYPE not in ['size', 'time']:
             logger.error("❌ LOG_ROTATION_TYPE must be 'size' or 'time'")
-            is_valid = False
-        
-        return is_valid
+            return False
+        return True
     
     @classmethod
     def get_hyperliquid_config(cls) -> dict:

+ 80 - 60
src/config/logging_config.py

@@ -11,6 +11,23 @@ from typing import Optional
 from src.config.config import Config
 
 
+class ColorFormatter(logging.Formatter):
+    """A logging formatter that adds color to log levels."""
+
+    COLORS = {
+        'WARNING': '\033[93m',  # Yellow
+        'INFO': '\033[92m',     # Green
+        'DEBUG': '\033[94m',    # Blue
+        'CRITICAL': '\033[91m', # Red
+        'ERROR': '\033[91m',    # Red
+    }
+    RESET = '\033[0m'
+
+    def format(self, record):
+        log_message = super().format(record)
+        return f"{self.COLORS.get(record.levelname, '')}{log_message}{self.RESET}"
+
+
 class LoggingManager:
     """Manages logging configuration with rotation and cleanup."""
     
@@ -45,10 +62,20 @@ class LoggingManager:
             datefmt='%Y-%m-%d %H:%M:%S'
         )
         
-        # Set up console handler
+        # Set up console handler with color
         self.console_handler = logging.StreamHandler()
         self.console_handler.setLevel(getattr(logging, Config.LOG_LEVEL.upper()))
-        self.console_handler.setFormatter(formatter)
+        
+        # Use color formatter for console, and standard for file
+        if self.console_handler.stream.isatty():
+            color_formatter = ColorFormatter(
+                fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+                datefmt='%Y-%m-%d %H:%M:%S'
+            )
+            self.console_handler.setFormatter(color_formatter)
+        else:
+            self.console_handler.setFormatter(formatter)
+        
         logger.addHandler(self.console_handler)
         
         # Set up file handler with rotation (if enabled)
@@ -91,90 +118,83 @@ class LoggingManager:
     
     def cleanup_old_logs(self, days_to_keep: int = 30) -> None:
         """
-        Clean up old log files beyond the backup count.
+        Clean up old log files beyond a specified number of days.
         
         Args:
-            days_to_keep: Number of days worth of logs to keep
+            days_to_keep: Number of days worth of logs to keep.
         """
         if not Config.LOG_TO_FILE:
             return
-        
-        try:
-            log_dir = Path(Config.LOG_FILE_PATH).parent
-            log_base_name = Path(Config.LOG_FILE_PATH).stem
-            
-            # Find all log files
-            log_files = []
-            for file_path in log_dir.glob(f"{log_base_name}*"):
-                if file_path.is_file() and file_path.suffix in ['.log', '.log.1', '.log.2']:
-                    log_files.append(file_path)
-            
-            # Sort by modification time (oldest first)
-            log_files.sort(key=lambda x: x.stat().st_mtime)
-            
-            # Calculate cutoff date
-            cutoff_date = datetime.now() - timedelta(days=days_to_keep)
-            
-            # Remove old files
-            removed_count = 0
-            for log_file in log_files:
-                file_date = datetime.fromtimestamp(log_file.stat().st_mtime)
-                
-                if file_date < cutoff_date:
-                    try:
+
+        log_dir = Path(Config.LOG_FILE_PATH).parent
+        if not log_dir.exists():
+            return
+
+        cutoff_date = datetime.now() - timedelta(days=days_to_keep)
+        removed_count = 0
+
+        for log_file in log_dir.glob(f"{Path(Config.LOG_FILE_PATH).stem}*"):
+            if log_file.is_file():
+                try:
+                    file_mod_time = datetime.fromtimestamp(log_file.stat().st_mtime)
+                    if file_mod_time < cutoff_date:
                         log_file.unlink()
                         removed_count += 1
                         if self.logger:
                             self.logger.info(f"🗑️ Removed old log file: {log_file.name}")
-                    except OSError as e:
-                        if self.logger:
-                            self.logger.warning(f"⚠️ Could not remove {log_file.name}: {e}")
-            
-            if removed_count > 0 and self.logger:
-                self.logger.info(f"🧹 Cleanup complete: removed {removed_count} old log files")
+                except (OSError, FileNotFoundError) as e:
+                    if self.logger:
+                        self.logger.warning(f"⚠️ Could not remove {log_file.name}: {e}")
+        
+        if removed_count > 0 and self.logger:
+            self.logger.info(f"🧹 Cleanup complete: removed {removed_count} old log files.")
             
-        except Exception as e:
-            if self.logger:
-                self.logger.error(f"❌ Error during log cleanup: {e}")
-    
     def get_log_stats(self) -> dict:
         """
         Get statistics about current log files.
         
         Returns:
-            Dictionary with log file statistics
+            Dictionary with log file statistics.
         """
+        if not Config.LOG_TO_FILE:
+            return {
+                'log_to_file': False,
+                'log_file_path': 'N/A',
+                'current_size_mb': 0,
+                'backup_files': 0,
+                'total_size_mb': 0
+            }
+
+        log_file = Path(Config.LOG_FILE_PATH)
+        log_dir = log_file.parent
+        
         stats = {
-            'log_to_file': Config.LOG_TO_FILE,
-            'log_file_path': Config.LOG_FILE_PATH,
+            'log_to_file': True,
+            'log_file_path': str(log_file),
             'current_size_mb': 0,
             'backup_files': 0,
             'total_size_mb': 0
         }
-        
-        if not Config.LOG_TO_FILE:
+
+        if not log_dir.exists():
             return stats
-        
+
         try:
-            log_dir = Path(Config.LOG_FILE_PATH).parent
-            log_base_name = Path(Config.LOG_FILE_PATH).stem
+            current_size = log_file.stat().st_size if log_file.exists() else 0
+            stats['current_size_mb'] = current_size / (1024 * 1024)
             
-            # Main log file
-            main_log = Path(Config.LOG_FILE_PATH)
-            if main_log.exists():
-                stats['current_size_mb'] = main_log.stat().st_size / (1024 * 1024)
-                stats['total_size_mb'] = stats['current_size_mb']
+            total_size = current_size
+            backup_files = 0
             
-            # Backup files
-            backup_files = list(log_dir.glob(f"{log_base_name}*.log.*"))
-            stats['backup_files'] = len(backup_files)
+            for f in log_dir.glob(f"{log_file.stem}*"):
+                if f.is_file() and f != log_file:
+                    total_size += f.stat().st_size
+                    backup_files += 1
             
-            # Total size
-            for backup_file in backup_files:
-                if backup_file.exists():
-                    stats['total_size_mb'] += backup_file.stat().st_size / (1024 * 1024)
-            
-        except Exception as e:
+            stats['total_size_mb'] = total_size / (1024 * 1024)
+            stats['backup_files'] = backup_files
+
+        except (OSError, FileNotFoundError) as e:
             if self.logger:
                 self.logger.error(f"❌ Error getting log stats: {e}")
         

+ 22 - 22
src/monitoring/external_event_monitor.py

@@ -199,7 +199,7 @@ class ExternalEventMonitor:
                 position_side = existing_lc.get('position_side', 'unknown').upper()
                 entry_price = existing_lc.get('entry_price', 0)
                 pnl_emoji = "🟢" if realized_pnl and realized_pnl >= 0 else "🔴"
-                pnl_text = f"{formatter.format_price_with_symbol(realized_pnl)}" if realized_pnl is not None else "N/A"
+                pnl_text = f"{await formatter.format_price_with_symbol(realized_pnl)}" if realized_pnl is not None else "N/A"
                 
                 # Get ROE directly from exchange data
                 info_data = existing_lc.get('info', {})
@@ -225,10 +225,10 @@ class ExternalEventMonitor:
 📊 <b>Trade Details:</b>
 • Token: {token}
 • Direction: {position_side}
-• Size Closed: {formatter.format_amount(amount_from_fill, token)}
-• Entry Price: {formatter.format_price_with_symbol(entry_price, token)}
-• Exit Price: {formatter.format_price_with_symbol(price_from_fill, token)}
-• Exit Value: {formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
+• Size Closed: {await formatter.format_amount(amount_from_fill, token)}
+• Entry Price: {await formatter.format_price_with_symbol(entry_price, token)}
+• Exit Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
+• Exit Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
 
 {pnl_emoji} <b>P&L:</b> {pnl_text}{roe_text}
 ✅ <b>Status:</b> {position_side} position closed externally
@@ -245,9 +245,9 @@ class ExternalEventMonitor:
 📊 <b>Trade Details:</b>
 • Token: {token}
 • Direction: {position_side}
-• Size: {formatter.format_amount(amount_from_fill, token)}
-• Entry Price: {formatter.format_price_with_symbol(price_from_fill, token)}
-• Position Value: {formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
+• Size: {await formatter.format_amount(amount_from_fill, token)}
+• Entry Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
+• Position Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
 
 ✅ <b>Status:</b> New {position_side} position opened externally
 ⏰ <b>Time:</b> {time_str}
@@ -276,11 +276,11 @@ class ExternalEventMonitor:
 📊 <b>Trade Details:</b>
 • Token: {token}
 • Direction: {position_side}
-• Size Added: {formatter.format_amount(amount_from_fill, token)}
-• Add Price: {formatter.format_price_with_symbol(price_from_fill, token)}
-• Previous Size: {formatter.format_amount(previous_size, token)}
-• New Size: {formatter.format_amount(current_size, token)}
-• Add Value: {formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
+• Size Added: {await formatter.format_amount(amount_from_fill, token)}
+• Add Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
+• Previous Size: {await formatter.format_amount(previous_size, token)}
+• New Size: {await formatter.format_amount(current_size, token)}
+• Add Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
 
 📈 <b>Status:</b> {position_side} position size increased externally
 ⏰ <b>Time:</b> {time_str}
@@ -328,13 +328,13 @@ class ExternalEventMonitor:
 📊 <b>Trade Details:</b>
 • Token: {token}
 • Direction: {position_side}
-• Size Reduced: {formatter.format_amount(amount_from_fill, token)}
-• Exit Price: {formatter.format_price_with_symbol(price_from_fill, token)}
-• Previous Size: {formatter.format_amount(previous_size, token)}
-• Remaining Size: {formatter.format_amount(current_size, token)}
-• Exit Value: {formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
+• Size Reduced: {await formatter.format_amount(amount_from_fill, token)}
+• Exit Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
+• Previous Size: {await formatter.format_amount(previous_size, token)}
+• Remaining Size: {await formatter.format_amount(current_size, token)}
+• Exit Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
 
-{pnl_emoji} <b>Partial P&L:</b> {formatter.format_price_with_symbol(partial_pnl)}{roe_text}
+{pnl_emoji} <b>Partial P&L:</b> {await formatter.format_price_with_symbol(partial_pnl)}{roe_text}
 📉 <b>Status:</b> {position_side} position size decreased externally  
 ⏰ <b>Time:</b> {time_str}
 
@@ -395,7 +395,7 @@ class ExternalEventMonitor:
                     logger.error(f"AUTO-SYNC: Could not determine entry price for {symbol}.")
                     return False
             
-            logger.info(f"🔄 AUTO-SYNC: Creating lifecycle for {symbol} {position_side.upper()} {contracts_abs} @ {formatter.format_price_with_symbol(final_entry_price, symbol)}")
+            logger.info(f"🔄 AUTO-SYNC: Creating lifecycle for {symbol} {position_side.upper()} {contracts_abs} @ {await formatter.format_price_with_symbol(final_entry_price, symbol)}")
             
             unique_sync_id = str(uuid.uuid4())[:8]
             lifecycle_id = stats.create_trade_lifecycle(
@@ -406,7 +406,7 @@ class ExternalEventMonitor:
             )
             
             if lifecycle_id:
-                success = stats.update_trade_position_opened(
+                success = await stats.update_trade_position_opened(
                     lifecycle_id, 
                     final_entry_price, 
                     contracts_abs,
@@ -499,7 +499,7 @@ class ExternalEventMonitor:
                     if exchange_order_id_from_fill:
                         pending_lc = stats.get_lifecycle_by_entry_order_id(exchange_order_id_from_fill, status='pending')
                         if pending_lc and pending_lc.get('symbol') == full_symbol:
-                            success = stats.update_trade_position_opened(
+                            success = await stats.update_trade_position_opened(
                                 lifecycle_id=pending_lc['trade_lifecycle_id'],
                                 entry_price=price_from_fill,
                                 entry_amount=amount_from_fill,

+ 2 - 2
src/monitoring/simple_position_tracker.py

@@ -128,7 +128,7 @@ class SimplePositionTracker:
             
             if lifecycle_id:
                 # Update to position_opened using existing manager
-                stats.update_trade_position_opened(
+                await stats.update_trade_position_opened(
                     lifecycle_id=lifecycle_id,
                     entry_price=entry_price,
                     entry_amount=size,
@@ -188,7 +188,7 @@ class SimplePositionTracker:
                 realized_pnl = size * (entry_price - exit_price)
             
             # Update to position_closed using existing manager
-            success = stats.update_trade_position_closed(
+            success = await stats.update_trade_position_closed(
                 lifecycle_id=lifecycle_id,
                 exit_price=exit_price,
                 realized_pnl=realized_pnl,

+ 25 - 25
src/notifications/notification_manager.py

@@ -28,12 +28,12 @@ class NotificationManager:
             # Use TokenDisplayFormatter for consistent formatting
             formatter = get_formatter() # Get formatter
             
-            amount_str = formatter.format_amount(amount, token)
+            amount_str = await formatter.format_amount(amount, token)
             order_id_str = order_details.get('id', 'N/A')
             
             if price is not None:
-                price_str = formatter.format_price_with_symbol(price, token)
-                value_str = formatter.format_price_with_symbol(amount * price, token)
+                price_str = await formatter.format_price_with_symbol(price, token)
+                value_str = await formatter.format_price_with_symbol(amount * price, token)
                 message = (
                     f"✅ Successfully opened <b>LONG</b> position for {amount_str} {token} at ~{price_str}\n\n"
                     f"💰 Value: {value_str}\n"
@@ -51,7 +51,7 @@ class NotificationManager:
                 message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
 
             if stop_loss_price:
-                sl_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
+                sl_price_str = await formatter.format_price_with_symbol(stop_loss_price, token)
                 message += f"\n🛑 Stop Loss pending at {sl_price_str}"
             
             await query.edit_message_text(text=message, parse_mode='HTML')
@@ -63,12 +63,12 @@ class NotificationManager:
         try:
             formatter = get_formatter() # Get formatter
             
-            amount_str = formatter.format_amount(amount, token)
+            amount_str = await formatter.format_amount(amount, token)
             order_id_str = order_details.get('id', 'N/A')
 
             if price is not None:
-                price_str = formatter.format_price_with_symbol(price, token)
-                value_str = formatter.format_price_with_symbol(amount * price, token)
+                price_str = await formatter.format_price_with_symbol(price, token)
+                value_str = await formatter.format_price_with_symbol(amount * price, token)
                 message = (
                     f"✅ Successfully opened <b>SHORT</b> position for {amount_str} {token} at ~{price_str}\n\n"
                     f"💰 Value: {value_str}\n"
@@ -86,7 +86,7 @@ class NotificationManager:
                 message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
 
             if stop_loss_price:
-                sl_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
+                sl_price_str = await formatter.format_price_with_symbol(stop_loss_price, token)
                 message += f"\n🛑 Stop Loss pending at {sl_price_str}"
             
             await query.edit_message_text(text=message, parse_mode='HTML')
@@ -100,9 +100,9 @@ class NotificationManager:
             
             # Price is the execution price, PnL is calculated based on it
             # For market orders, price might be approximate or from fill later
-            price_str = formatter.format_price_with_symbol(price, token) if price > 0 else "Market Price"
-            amount_str = formatter.format_amount(amount, token)
-            pnl_str = formatter.format_price_with_symbol(pnl)
+            price_str = await formatter.format_price_with_symbol(price, token) if price > 0 else "Market Price"
+            amount_str = await formatter.format_amount(amount, token)
+            pnl_str = await formatter.format_price_with_symbol(pnl)
             pnl_emoji = "🟢" if pnl >= 0 else "🔴"
             order_id_str = order_details.get('id', 'N/A')
             cancelled_sl_count = order_details.get('cancelled_stop_losses', 0)
@@ -129,8 +129,8 @@ class NotificationManager:
         try:
             formatter = get_formatter() # Get formatter
             
-            stop_price_str = formatter.format_price_with_symbol(stop_price, token)
-            amount_str = formatter.format_amount(amount, token)
+            stop_price_str = await formatter.format_price_with_symbol(stop_price, token)
+            amount_str = await formatter.format_amount(amount, token)
             order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
             
             message = (
@@ -150,8 +150,8 @@ class NotificationManager:
         try:
             formatter = get_formatter() # Get formatter
             
-            tp_price_str = formatter.format_price_with_symbol(tp_price, token)
-            amount_str = formatter.format_amount(amount, token)
+            tp_price_str = await formatter.format_price_with_symbol(tp_price, token)
+            amount_str = await formatter.format_amount(amount, token)
             order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
 
             message = (
@@ -198,8 +198,8 @@ class NotificationManager:
                 # If individual orders can have different tokens, order dict should contain 'symbol' or 'token' key
                 order_token_symbol = order.get('symbol', token) # Fallback to main token if not in order dict
 
-                amount_str = formatter.format_amount(amount, order_token_symbol)
-                price_str = formatter.format_price_with_symbol(price, order_token_symbol) if price > 0 else "N/A"
+                amount_str = await formatter.format_amount(amount, order_token_symbol)
+                price_str = await formatter.format_price_with_symbol(price, order_token_symbol) if price > 0 else "N/A"
                 
                 side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
                 success_message += f"""
@@ -240,8 +240,8 @@ class NotificationManager:
         
         formatter = get_formatter() # Get formatter
         
-        target_price_str = formatter.format_price_with_symbol(target_price, token)
-        current_price_str = formatter.format_price_with_symbol(current_price, token)
+        target_price_str = await formatter.format_price_with_symbol(target_price, token)
+        current_price_str = await formatter.format_price_with_symbol(current_price, token)
         
         alarm_message = f"""
 🔔 <b>Price Alarm Triggered!</b>
@@ -531,9 +531,9 @@ class NotificationManager:
                 pnl_emoji = "🟢" if pnl >= 0 else "🔴"
                 pnl_info = f"""
 {pnl_emoji} <b>Take Profit P&L:</b>
-• Entry Price: {formatter.format_price_with_symbol(entry_price, token)}
-• Exit Price: {formatter.format_price_with_symbol(price, token)}
-• Realized P&L: {formatter.format_price_with_symbol(pnl)} ({roe:+.2f}% ROE)
+• Entry Price: {await formatter.format_price_with_symbol(entry_price, token)}
+• Exit Price: {await formatter.format_price_with_symbol(price, token)}
+• Realized P&L: {await formatter.format_price_with_symbol(pnl)} ({roe:+.2f}% ROE)
 • Result: {"PROFIT SECURED" if pnl >=0 else "MINIMIZED LOSS"}"""
             
             trade_value = amount * price
@@ -546,9 +546,9 @@ class NotificationManager:
 • Token: {token}
 • Position Type: {position_side.upper()}
 • Take Profit Size: {amount} {token}
-• Target Price: {formatter.format_price_with_symbol(trigger_price, token)}
-• Execution Price: {formatter.format_price_with_symbol(price, token)}
-• Exit Value: {formatter.format_price_with_symbol(trade_value, token)}
+• Target Price: {await formatter.format_price_with_symbol(trigger_price, token)}
+• Execution Price: {await formatter.format_price_with_symbol(price, token)}
+• Exit Value: {await formatter.format_price_with_symbol(trade_value, token)}
 
 ✅ <b>Take Profit Details:</b>
 • Status: EXECUTED

+ 6 - 6
src/stats/aggregation_manager.py

@@ -172,12 +172,12 @@ class AggregationManager:
         self.db._execute_query(cancelled_upsert_query, (token, now_iso))
         logger.info(f"Incremented cancelled_cycles for {token}.")
 
-    def record_deposit(self, amount: float, timestamp: Optional[str] = None, 
+    async def record_deposit(self, amount: float, timestamp: Optional[str] = None, 
                        deposit_id: Optional[str] = None, description: Optional[str] = None):
         """Record a deposit."""
         ts = timestamp if timestamp else datetime.now(timezone.utc).isoformat()
         formatter = get_formatter()
-        formatted_amount_str = formatter.format_price_with_symbol(amount)
+        formatted_amount_str = await formatter.format_price_with_symbol(amount)
         desc = description if description else f'Deposit of {formatted_amount_str}'
         
         self.db._execute_query(
@@ -187,14 +187,14 @@ class AggregationManager:
         # Adjust initial_balance in metadata to reflect capital changes
         current_initial = float(self.db._get_metadata('initial_balance') or '0.0')
         self.db._set_metadata('initial_balance', str(current_initial + amount))
-        logger.info(f"💰 Recorded deposit: {formatted_amount_str}. New effective initial balance: {formatter.format_price_with_symbol(current_initial + amount)}")
+        logger.info(f"💰 Recorded deposit: {formatted_amount_str}. New effective initial balance: {await formatter.format_price_with_symbol(current_initial + amount)}")
 
-    def record_withdrawal(self, amount: float, timestamp: Optional[str] = None, 
+    async def record_withdrawal(self, amount: float, timestamp: Optional[str] = None, 
                           withdrawal_id: Optional[str] = None, description: Optional[str] = None):
         """Record a withdrawal."""
         ts = timestamp if timestamp else datetime.now(timezone.utc).isoformat()
         formatter = get_formatter()
-        formatted_amount_str = formatter.format_price_with_symbol(amount)
+        formatted_amount_str = await formatter.format_price_with_symbol(amount)
         desc = description if description else f'Withdrawal of {formatted_amount_str}'
         
         self.db._execute_query(
@@ -203,7 +203,7 @@ class AggregationManager:
         )
         current_initial = float(self.db._get_metadata('initial_balance') or '0.0')
         self.db._set_metadata('initial_balance', str(current_initial - amount))
-        logger.info(f"💸 Recorded withdrawal: {formatted_amount_str}. New effective initial balance: {formatter.format_price_with_symbol(current_initial - amount)}")
+        logger.info(f"💸 Recorded withdrawal: {formatted_amount_str}. New effective initial balance: {await formatter.format_price_with_symbol(current_initial - amount)}")
 
     def get_balance_adjustments_summary(self) -> Dict[str, Any]:
         """Get summary of all balance adjustments from DB."""

+ 18 - 29
src/stats/database_manager.py

@@ -256,44 +256,33 @@ class DatabaseManager:
         self._execute_query("INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)", (key, value))
 
     def set_initial_balance(self, balance: float):
-        """Set the initial balance if not already set or zero."""
-        current_initial_balance_str = self._get_metadata('initial_balance')
-        current_initial_balance = float(current_initial_balance_str) if current_initial_balance_str else 0.0
-        
-        if current_initial_balance == 0.0:  # Only set if it's effectively unset
+        """Set the initial balance if it hasn't been set yet."""
+        # Use a small tolerance for floating point comparison
+        if self.get_initial_balance() < 1e-9: # Check if it's effectively zero
             self._set_metadata('initial_balance', str(balance))
-            # Also set start_date if it's the first time setting balance
-            if self._get_metadata('start_date') is None or float(current_initial_balance_str if current_initial_balance_str else '0.0') == 0.0:
-                self._set_metadata('start_date', datetime.now(timezone.utc).isoformat())
-            formatter = get_formatter()
-            logger.info(f"Initial balance set to: {formatter.format_price_with_symbol(balance)}")
+            logger.info(f"Initial balance set to: {balance}")
         else:
-            formatter = get_formatter()
-            logger.info(f"Initial balance already set to {formatter.format_price_with_symbol(current_initial_balance)}. Not changing.")
+            logger.info(f"Initial balance is already set. Current value: {self.get_initial_balance()}")
 
     def get_initial_balance(self) -> float:
-        """Get the initial balance."""
-        initial_balance_str = self._get_metadata('initial_balance')
-        return float(initial_balance_str) if initial_balance_str else 0.0
+        """Retrieve the initial balance from the metadata table."""
+        balance_str = self._get_metadata('initial_balance')
+        return float(balance_str) if balance_str is not None else 0.0
 
     def record_balance_snapshot(self, balance: float, unrealized_pnl: float = 0.0, 
                                timestamp: Optional[str] = None, notes: Optional[str] = None):
-        """Record a balance snapshot."""
-        if not timestamp:
-            timestamp = datetime.now(timezone.utc).isoformat()
+        """Records a balance snapshot to both balance_history and daily_balances."""
         
-        query = """
-            INSERT INTO balance_history (balance, unrealized_pnl, timestamp, notes)
-            VALUES (?, ?, ?, ?)
-        """
+        now_utc = datetime.now(timezone.utc)
         
-        try:
-            self._execute_query(query, (balance, unrealized_pnl, timestamp, notes))
-            from src.utils.token_display_formatter import get_formatter
-            formatter = get_formatter()
-            logger.info(f"Recorded balance snapshot: {formatter.format_price_with_symbol(balance)} (unrealized: {formatter.format_price_with_symbol(unrealized_pnl)})")
-        except Exception as e:
-            logger.error(f"Failed to record balance snapshot: {e}")
+        # For logging, show the formatted values
+        logger.info(f"Recorded balance snapshot: {balance} (unrealized: {unrealized_pnl})")
+        
+        # Store raw float values in the database
+        self._execute_query(
+            "INSERT INTO balance_history (balance, unrealized_pnl, timestamp, notes) VALUES (?, ?, ?, ?)",
+            (balance, unrealized_pnl, now_utc.isoformat(), notes)
+        )
 
     def purge_old_daily_aggregated_stats(self, months_to_keep: int = 10):
         """Purge records from daily_aggregated_stats older than specified months."""

+ 8 - 8
src/stats/trade_lifecycle_manager.py

@@ -59,7 +59,7 @@ class TradeLifecycleManager:
             logger.error(f"❌ Error creating trade lifecycle: {e}")
             return None
     
-    def update_trade_position_opened(self, lifecycle_id: str, entry_price: float, 
+    async def update_trade_position_opened(self, lifecycle_id: str, entry_price: float, 
                                    entry_amount: float, exchange_fill_id: str) -> bool:
         """Update trade when position is opened (entry order filled)."""
         try:
@@ -93,14 +93,14 @@ class TradeLifecycleManager:
             symbol_for_formatting = trade_info.get('symbol', 'UNKNOWN_SYMBOL') if trade_info else 'UNKNOWN_SYMBOL'
             base_asset_for_amount = symbol_for_formatting.split('/')[0] if '/' in symbol_for_formatting else symbol_for_formatting
 
-            logger.info(f"📈 Trade lifecycle {lifecycle_id} position opened: {formatter.format_amount(entry_amount, base_asset_for_amount)} {symbol_for_formatting} @ {formatter.format_price(entry_price, symbol_for_formatting)}")
+            logger.info(f"📈 Trade lifecycle {lifecycle_id} position opened: {await formatter.format_amount(entry_amount, base_asset_for_amount)} {symbol_for_formatting} @ {await formatter.format_price(entry_price, symbol_for_formatting)}")
             return True
             
         except Exception as e:
             logger.error(f"❌ Error updating trade position opened: {e}")
             return False
     
-    def update_trade_position_closed(self, lifecycle_id: str, exit_price: float,
+    async def update_trade_position_closed(self, lifecycle_id: str, exit_price: float,
                                    realized_pnl: float, exchange_fill_id: str) -> bool:
         """Update trade when position is fully closed."""
         try:
@@ -121,7 +121,7 @@ class TradeLifecycleManager:
             
             formatter = get_formatter()
             pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
-            logger.info(f"{pnl_emoji} Trade lifecycle {lifecycle_id} position closed: P&L {formatter.format_price_with_symbol(realized_pnl)}")
+            logger.info(f"{pnl_emoji} Trade lifecycle {lifecycle_id} position closed: P&L {await formatter.format_price_with_symbol(realized_pnl)}")
             return True
             
         except Exception as e:
@@ -150,7 +150,7 @@ class TradeLifecycleManager:
             logger.error(f"❌ Error updating trade cancelled: {e}")
             return False
     
-    def link_stop_loss_to_trade(self, lifecycle_id: str, stop_loss_order_id: str,
+    async def link_stop_loss_to_trade(self, lifecycle_id: str, stop_loss_order_id: str,
                                stop_loss_price: float) -> bool:
         """Link a stop loss order to a trade lifecycle."""
         try:
@@ -169,14 +169,14 @@ class TradeLifecycleManager:
             formatter = get_formatter()
             trade_info = self.get_trade_by_lifecycle_id(lifecycle_id)
             symbol_for_formatting = trade_info.get('symbol', 'UNKNOWN_SYMBOL') if trade_info else 'UNKNOWN_SYMBOL'
-            logger.info(f"🛑 Linked stop loss order {stop_loss_order_id} ({formatter.format_price(stop_loss_price, symbol_for_formatting)}) to trade {lifecycle_id}")
+            logger.info(f"🛑 Linked stop loss order {stop_loss_order_id} ({await formatter.format_price(stop_loss_price, symbol_for_formatting)}) to trade {lifecycle_id}")
             return True
             
         except Exception as e:
             logger.error(f"❌ Error linking stop loss to trade: {e}")
             return False
     
-    def link_take_profit_to_trade(self, lifecycle_id: str, take_profit_order_id: str,
+    async def link_take_profit_to_trade(self, lifecycle_id: str, take_profit_order_id: str,
                                  take_profit_price: float) -> bool:
         """Link a take profit order to a trade lifecycle."""
         try:
@@ -195,7 +195,7 @@ class TradeLifecycleManager:
             formatter = get_formatter()
             trade_info = self.get_trade_by_lifecycle_id(lifecycle_id)
             symbol_for_formatting = trade_info.get('symbol', 'UNKNOWN_SYMBOL') if trade_info else 'UNKNOWN_SYMBOL'
-            logger.info(f"🎯 Linked take profit order {take_profit_order_id} ({formatter.format_price(take_profit_price, symbol_for_formatting)}) to trade {lifecycle_id}")
+            logger.info(f"🎯 Linked take profit order {take_profit_order_id} ({await formatter.format_price(take_profit_price, symbol_for_formatting)}) to trade {lifecycle_id}")
             return True
             
         except Exception as e:

+ 169 - 199
src/stats/trading_stats.py

@@ -64,18 +64,18 @@ class TradingStats:
     # DATABASE MANAGEMENT DELEGATION
     # =============================================================================
     
-    def set_initial_balance(self, balance: float):
+    async def set_initial_balance(self, balance: float):
         """Set initial balance."""
-        return self.db_manager.set_initial_balance(balance)
+        return await self.db_manager.set_initial_balance(balance)
     
     def get_initial_balance(self) -> float:
         """Get initial balance."""
         return self.db_manager.get_initial_balance()
     
-    def record_balance_snapshot(self, balance: float, unrealized_pnl: float = 0.0, 
+    async def record_balance_snapshot(self, balance: float, unrealized_pnl: float = 0.0, 
                                timestamp: Optional[str] = None, notes: Optional[str] = None):
         """Record balance snapshot."""
-        return self.db_manager.record_balance_snapshot(balance, unrealized_pnl, timestamp, notes)
+        return await self.db_manager.record_balance_snapshot(balance, unrealized_pnl, timestamp, notes)
     
     def purge_old_balance_history(self, days_to_keep: int = 30) -> int:
         """Purge old balance history."""
@@ -194,17 +194,17 @@ class TradingStats:
             stop_loss_price, take_profit_price, trade_type
         )
     
-    def update_trade_position_opened(self, lifecycle_id: str, entry_price: float, 
+    async def update_trade_position_opened(self, lifecycle_id: str, entry_price: float, 
                                    entry_amount: float, exchange_fill_id: str) -> bool:
         """Update trade position opened."""
-        return self.trade_manager.update_trade_position_opened(
+        return await self.trade_manager.update_trade_position_opened(
             lifecycle_id, entry_price, entry_amount, exchange_fill_id
         )
     
-    def update_trade_position_closed(self, lifecycle_id: str, exit_price: float,
+    async def update_trade_position_closed(self, lifecycle_id: str, exit_price: float,
                                    realized_pnl: float, exchange_fill_id: str) -> bool:
         """Update trade position closed."""
-        return self.trade_manager.update_trade_position_closed(
+        return await self.trade_manager.update_trade_position_closed(
             lifecycle_id, exit_price, realized_pnl, exchange_fill_id
         )
     
@@ -212,17 +212,17 @@ class TradingStats:
         """Update trade cancelled."""
         return self.trade_manager.update_trade_cancelled(lifecycle_id, reason)
     
-    def link_stop_loss_to_trade(self, lifecycle_id: str, stop_loss_order_id: str,
+    async def link_stop_loss_to_trade(self, lifecycle_id: str, stop_loss_order_id: str,
                                stop_loss_price: float) -> bool:
         """Link stop loss to trade."""
-        return self.trade_manager.link_stop_loss_to_trade(
+        return await self.trade_manager.link_stop_loss_to_trade(
             lifecycle_id, stop_loss_order_id, stop_loss_price
         )
     
-    def link_take_profit_to_trade(self, lifecycle_id: str, take_profit_order_id: str,
+    async def link_take_profit_to_trade(self, lifecycle_id: str, take_profit_order_id: str,
                                  take_profit_price: float) -> bool:
         """Link take profit to trade."""
-        return self.trade_manager.link_take_profit_to_trade(
+        return await self.trade_manager.link_take_profit_to_trade(
             lifecycle_id, take_profit_order_id, take_profit_price
         )
     
@@ -282,29 +282,29 @@ class TradingStats:
         return self.trade_manager.get_all_trades()
 
     def cancel_linked_orders(self, parent_bot_order_ref_id: str, new_status: str = 'cancelled_parent_filled') -> int:
-        """Cancel all orders linked to a parent order. Returns count of cancelled orders."""
-        return self.order_manager.cancel_linked_orders(parent_bot_order_ref_id, new_status)
+        """Cancel linked SL/TP orders when a parent order is filled or cancelled."""
+        return self.trade_manager.cancel_linked_orders(parent_bot_order_ref_id, new_status)
 
     # =============================================================================
     # AGGREGATION MANAGEMENT DELEGATION
     # =============================================================================
     
     def migrate_trade_to_aggregated_stats(self, trade_lifecycle_id: str):
-        """Migrate trade to aggregated stats."""
+        """Migrate completed trade to aggregated stats."""
         return self.aggregation_manager.migrate_trade_to_aggregated_stats(trade_lifecycle_id)
     
-    def record_deposit(self, amount: float, timestamp: Optional[str] = None, 
+    async def record_deposit(self, amount: float, timestamp: Optional[str] = None, 
                        deposit_id: Optional[str] = None, description: Optional[str] = None):
-        """Record deposit."""
-        return self.aggregation_manager.record_deposit(amount, timestamp, deposit_id, description)
+        """Record a deposit."""
+        return await self.aggregation_manager.record_deposit(amount, timestamp, deposit_id, description)
     
-    def record_withdrawal(self, amount: float, timestamp: Optional[str] = None, 
+    async def record_withdrawal(self, amount: float, timestamp: Optional[str] = None, 
                           withdrawal_id: Optional[str] = None, description: Optional[str] = None):
-        """Record withdrawal."""
-        return self.aggregation_manager.record_withdrawal(amount, timestamp, withdrawal_id, description)
+        """Record a withdrawal."""
+        return await self.aggregation_manager.record_withdrawal(amount, timestamp, withdrawal_id, description)
     
     def get_balance_adjustments_summary(self) -> Dict[str, Any]:
-        """Get balance adjustments summary."""
+        """Get summary of balance adjustments."""
         return self.aggregation_manager.get_balance_adjustments_summary()
     
     def get_daily_stats(self, limit: int = 10) -> List[Dict[str, Any]]:
@@ -702,171 +702,144 @@ class TradingStats:
         
         return " ".join(parts)
 
-    def format_stats_message(self, current_balance: Optional[float] = None) -> str:
-        """Format stats for Telegram display using data from DB."""
-        try:
-            basic = self.get_basic_stats(current_balance)
-            perf = self.get_performance_stats()
-            risk = self.get_risk_metrics()
-            
-            formatter = get_formatter()
-            
-            effective_current_balance = current_balance if current_balance is not None else (basic['initial_balance'] + basic['total_pnl'])
-            initial_bal = basic['initial_balance']
-
-            total_pnl_val = effective_current_balance - initial_bal if initial_bal > 0 and current_balance is not None else basic['total_pnl']
-            total_return_pct = (total_pnl_val / initial_bal * 100) if initial_bal > 0 else 0.0
-            pnl_emoji = "🟢" if total_pnl_val >= 0 else "🔴"
-            open_positions_count = basic['open_positions_count']
-
-            stats_text_parts = []
-            stats_text_parts.append(f"📊 <b>Trading Statistics</b>\n")
-            
-            # Account Overview
-            stats_text_parts.append(f"\n💰 <b>Account Overview:</b>")
-            stats_text_parts.append(f"• Current Balance: {formatter.format_price_with_symbol(effective_current_balance)}")
-            stats_text_parts.append(f"• Initial Balance: {formatter.format_price_with_symbol(initial_bal)}")
-            stats_text_parts.append(f"• Open Positions: {open_positions_count}")
-            stats_text_parts.append(f"• {pnl_emoji} Total P&L: {formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)")
-            stats_text_parts.append(f"• Days Active: {basic['days_active']}\n")
-            
-            # Performance Metrics
-            stats_text_parts.append(f"\n🏆 <b>Performance Metrics:</b>")
-            stats_text_parts.append(f"• Total Completed Trades: {basic['completed_trades']}")
-            stats_text_parts.append(f"• Win Rate: {perf['win_rate']:.1f}% ({perf['total_wins']}/{basic['completed_trades']})")
-            stats_text_parts.append(f"• Trading Volume (Entry Vol.): {formatter.format_price_with_symbol(perf.get('total_trading_volume', 0.0))}")
-            stats_text_parts.append(f"• Profit Factor: {perf['profit_factor']:.2f}")
-            stats_text_parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf['expectancy'])}")
-            
-            largest_win_pct_str = f" ({perf.get('largest_winning_percentage', 0):+.2f}%)" if perf.get('largest_winning_percentage', 0) != 0 else ""
-            largest_loss_pct_str = f" ({perf.get('largest_losing_percentage', 0):+.2f}%)" if perf.get('largest_losing_percentage', 0) != 0 else ""
-            
-            # Show token names for largest trades
-            largest_win_token = perf.get('largest_winning_token', 'N/A')
-            largest_loss_token = perf.get('largest_losing_token', 'N/A')
-            
-            stats_text_parts.append(f"• Largest Winning Trade: {formatter.format_price_with_symbol(perf['largest_win'])}{largest_win_pct_str} ({largest_win_token})")
-            stats_text_parts.append(f"• Largest Losing Trade: {formatter.format_price_with_symbol(-perf['largest_loss'])}{largest_loss_pct_str} ({largest_loss_token})")
-            
-            # Add ROE-based largest trades if different from dollar-based
-            largest_win_roe_token = perf.get('largest_winning_roe_token', 'N/A')
-            largest_loss_roe_token = perf.get('largest_losing_roe_token', 'N/A')
-            largest_win_roe = perf.get('largest_winning_roe', 0)
-            largest_loss_roe = perf.get('largest_losing_roe', 0)
-            
-            if largest_win_roe_token != largest_win_token and largest_win_roe > 0:
-                largest_win_roe_pnl = perf.get('largest_winning_roe_pnl', 0)
-                stats_text_parts.append(f"• Best ROE Trade: {formatter.format_price_with_symbol(largest_win_roe_pnl)} (+{largest_win_roe:.2f}%) ({largest_win_roe_token})")
-            
-            if largest_loss_roe_token != largest_loss_token and largest_loss_roe > 0:
-                largest_loss_roe_pnl = perf.get('largest_losing_roe_pnl', 0)
-                stats_text_parts.append(f"• Worst ROE Trade: {formatter.format_price_with_symbol(-largest_loss_roe_pnl)} (-{largest_loss_roe:.2f}%) ({largest_loss_roe_token})")
-
-            best_token_stats = perf.get('best_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0, 'volume': 0.0, 'pnl_value': 0.0})
-            worst_token_stats = perf.get('worst_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0, 'volume': 0.0, 'pnl_value': 0.0})
-            
-            stats_text_parts.append(f"• Best Token: {best_token_stats['name']} {formatter.format_price_with_symbol(best_token_stats['pnl_value'])} ({best_token_stats['pnl_percentage']:+.2f}%)")
-            stats_text_parts.append(f"• Worst Token: {worst_token_stats['name']} {formatter.format_price_with_symbol(worst_token_stats['pnl_value'])} ({worst_token_stats['pnl_percentage']:+.2f}%)")
-            
-            stats_text_parts.append(f"• Average Trade Duration: {perf.get('avg_trade_duration', 'N/A')}")
-            stats_text_parts.append(f"• Portfolio Max Drawdown: {risk.get('max_drawdown_live_percentage', 0.0):.2f}% <i>(Live)</i>")
-            
-            # Session Info
-            stats_text_parts.append(f"\n\n⏰ <b>Session Info:</b>")
-            stats_text_parts.append(f"• Bot Started: {basic['start_date']}")
-            stats_text_parts.append(f"• Stats Last Updated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
-            
-            return "\n".join(stats_text_parts).strip()
-                
-        except Exception as e:
-            logger.error(f"❌ Error formatting stats message: {e}", exc_info=True)
-            return f"""📊 <b>Trading Statistics</b>\n\n❌ <b>Error loading statistics</b>\n\n🔧 <b>Debug info:</b> {str(e)[:100]}"""
+    async def format_stats_message(self, current_balance: Optional[float] = None) -> str:
+        """Formats a comprehensive statistics message."""
+        formatter = get_formatter()
+        basic_stats = self.get_basic_stats(current_balance)
+        initial_bal = basic_stats.get('initial_balance', 0.0)
+        total_pnl_val = basic_stats.get('total_pnl', 0.0)
+        total_return_pct = basic_stats.get('total_return_pct', 0.0)
+        pnl_emoji = "✅" if total_pnl_val >= 0 else "🔻"
+
+        stats_text_parts = [
+            f"📊 <b>Trading Performance Summary</b>",
+            f"• Current Balance: {await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val))} ({await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val) - initial_bal) if initial_bal > 0 else 'N/A'})",
+            f"• Initial Balance: {await formatter.format_price_with_symbol(initial_bal)}",
+            f"• Balance Change: {await formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)",
+            f"• {pnl_emoji} Total P&L: {await formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)"
+        ]
+        
+        # Performance Metrics
+        perf = basic_stats.get('performance_metrics', {})
+        if perf:
+            stats_text_parts.append("\n<b>Key Metrics:</b>")
+            stats_text_parts.append(f"• Trading Volume (Entry Vol.): {await formatter.format_price_with_symbol(perf.get('total_trading_volume', 0.0))}")
+            if perf.get('expectancy') is not None:
+                stats_text_parts.append(f"• Expectancy: {await formatter.format_price_with_symbol(perf['expectancy'])}")
+            stats_text_parts.append(f"• Win Rate: {perf.get('win_rate', 0.0):.2f}% ({perf.get('num_wins', 0)} wins)")
+            stats_text_parts.append(f"• Profit Factor: {perf.get('profit_factor', 0.0):.2f}")
+            
+            # Largest Trades
+            if perf.get('largest_win') is not None:
+                largest_win_pct_str = f" ({perf.get('largest_win_entry_pct', 0):.2f}%)" if perf.get('largest_win_entry_pct') is not None else ""
+                largest_win_token = perf.get('largest_win_token', 'N/A')
+                stats_text_parts.append(f"• Largest Winning Trade: {await formatter.format_price_with_symbol(perf['largest_win'])}{largest_win_pct_str} ({largest_win_token})")
+            if perf.get('largest_loss') is not None:
+                largest_loss_pct_str = f" ({perf.get('largest_loss_entry_pct', 0):.2f}%)" if perf.get('largest_loss_entry_pct') is not None else ""
+                largest_loss_token = perf.get('largest_loss_token', 'N/A')
+                stats_text_parts.append(f"• Largest Losing Trade: {await formatter.format_price_with_symbol(-perf['largest_loss'])}{largest_loss_pct_str} ({largest_loss_token})")
+
+            # ROE-based metrics if available
+            largest_win_roe = perf.get('largest_win_roe')
+            largest_loss_roe = perf.get('largest_loss_roe')
+            if largest_win_roe is not None:
+                largest_win_roe_pnl = perf.get('largest_win_roe_pnl', 0.0)
+                largest_win_roe_token = perf.get('largest_win_roe_token', 'N/A')
+                stats_text_parts.append(f"• Best ROE Trade: {await formatter.format_price_with_symbol(largest_win_roe_pnl)} (+{largest_win_roe:.2f}%) ({largest_win_roe_token})")
+            if largest_loss_roe is not None:
+                largest_loss_roe_pnl = perf.get('largest_loss_roe_pnl', 0.0)
+                largest_loss_roe_token = perf.get('largest_loss_roe_token', 'N/A')
+                stats_text_parts.append(f"• Worst ROE Trade: {await formatter.format_price_with_symbol(-largest_loss_roe_pnl)} (-{largest_loss_roe:.2f}%) ({largest_loss_roe_token})")
+
+        # Best/Worst Tokens
+        best_token_stats = basic_stats.get('best_token')
+        worst_token_stats = basic_stats.get('worst_token')
+        if best_token_stats:
+            stats_text_parts.append(f"• Best Token: {best_token_stats['name']} {await formatter.format_price_with_symbol(best_token_stats['pnl_value'])} ({best_token_stats['pnl_percentage']:+.2f}%)")
+        if worst_token_stats:
+            stats_text_parts.append(f"• Worst Token: {worst_token_stats['name']} {await formatter.format_price_with_symbol(worst_token_stats['pnl_value'])} ({worst_token_stats['pnl_percentage']:+.2f}%)")
+
+        return "\n".join(stats_text_parts)
+
+    async def format_token_stats_message(self, token: str) -> str:
+        """Formats a statistics message for a specific token."""
+        formatter = get_formatter()
+        token_stats = self.get_token_detailed_stats(token)
+        normalized_token = _normalize_token_case(token)
+        token_name = token_stats.get('token', normalized_token.upper())
+        
+        if not token_stats or token_stats.get('summary_total_trades', 0) == 0:
+            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>"
+            )
 
-    def format_token_stats_message(self, token: str) -> str:
-        """Format detailed statistics for a specific token."""
-        try:
-            from src.utils.token_display_formatter import get_formatter
-            formatter = get_formatter()
-            
-            token_stats_data = self.get_token_detailed_stats(token)
-            token_name = token_stats_data.get('token', token.upper())
-            
-            if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
-                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
-            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 "🔴"
-                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))}")
-                
-                # Format largest trades with percentages
-                largest_win_pct_str = f" ({perf_summary.get('largest_win_percentage', 0):+.2f}%)" if perf_summary.get('largest_win_percentage', 0) != 0 else ""
-                largest_loss_pct_str = f" ({perf_summary.get('largest_loss_percentage', 0):+.2f}%)" if perf_summary.get('largest_loss_percentage', 0) != 0 else ""
-                
-                parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}{largest_loss_pct_str}")
-                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("")
-
-            # Open Positions
-            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 "🔴"
+        perf_summary = token_stats.get('performance_summary', {})
+        open_positions = token_stats.get('open_positions', [])
+        
+        parts = [f"📊 <b>{token_name.upper()} Detailed Statistics</b>\n"]
+
+        # Completed Trades Summary
+        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 "🔻"
+            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: {await 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: {await formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
+            parts.append(f"• Avg Win: {await formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {await formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
+            
+            # Format largest trades with percentages
+            largest_win_pct_str = f" ({perf_summary.get('largest_win_entry_pct', 0):.2f}%)" if perf_summary.get('largest_win_entry_pct') is not None else ""
+            largest_loss_pct_str = f" ({perf_summary.get('largest_loss_entry_pct', 0):.2f}%)" if perf_summary.get('largest_loss_entry_pct') is not None else ""
+            
+            parts.append(f"• Largest Win: {await formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {await formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}{largest_loss_pct_str}")
+            parts.append(f"• Entry Volume: {await formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
+            parts.append(f"• Exit Volume: {await 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("")
+
+        # Open Positions
+        parts.append("📉 <b>Current Open Positions:</b>")
+        if open_positions:
+            total_open_unrealized_pnl = token_stats.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', 'buy').lower() == 'buy' else "🔽"
+                pos_pnl_emoji = "✅" if pos.get('unrealized_pnl', 0) >= 0 else "🔻"
+                opened_at_str = "N/A"
+                if pos.get('opened_at'):
+                    try:
+                        from datetime import datetime
+                        opened_at_dt = datetime.fromisoformat(pos['opened_at'])
+                        opened_at_str = opened_at_dt.strftime('%Y-%m-%d %H:%M')
+                    except:
+                        pass
                 
-                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:
-                            from datetime import datetime
-                            opened_at_dt = datetime.fromisoformat(pos['opened_at'])
-                            opened_at_str = opened_at_dt.strftime('%Y-%m-%d %H:%M')
-                        except:
-                            pass
-                    
-                    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("")
-
-            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)
-            
-        except Exception as e:
-            logger.error(f"❌ Error formatting token stats message for {token}: {e}", exc_info=True)
-            return f"❌ Error generating statistics for {token}: {str(e)[:100]}"
+                parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {await formatter.format_amount(abs(pos.get('amount',0)), token_name)} {token_name}")
+                parts.append(f"    Entry: {await formatter.format_price_with_symbol(pos.get('entry_price',0), token_name)} | Mark: {await formatter.format_price_with_symbol(pos.get('mark_price',0), token_name)}")
+                parts.append(f"    {pos_pnl_emoji} Unrealized P&L: {await 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: {await formatter.format_price_with_symbol(total_open_unrealized_pnl)}</b>")
+        else:
+            parts.append("• No open positions for this token.")
+        parts.append("")
+
+        parts.append(f"📋 Open Orders (Exchange): {token_stats.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)
 
     # =============================================================================
     # CONVENIENCE METHODS & HIGH-LEVEL OPERATIONS
@@ -930,28 +903,25 @@ class TradingStats:
             logger.error(f"❌ Error generating summary report: {e}")
             return {'error': str(e)}
 
-    def record_trade(self, symbol: str, side: str, amount: float, price: float,
+    async def record_trade(self, symbol: str, side: str, amount: float, price: float,
                      exchange_fill_id: Optional[str] = None, trade_type: str = "manual",
                      pnl: Optional[float] = None, timestamp: Optional[str] = None,
                      linked_order_table_id_to_link: Optional[int] = None):
-        """Record a trade directly in the database (used for unmatched external fills)."""
+        """DEPRECATED - use trade lifecycle methods instead."""
         if timestamp is None:
             timestamp = datetime.now(timezone.utc).isoformat()
         
         value = amount * price
+        formatter = get_formatter()
+        ts = timestamp or datetime.now(timezone.utc).isoformat()
         
-        try:
-            self.db_manager._execute_query(
-                "INSERT OR IGNORE INTO trades (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl, linked_order_table_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
-                (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl or 0.0, linked_order_table_id_to_link)
-            )
-            
-            formatter = get_formatter()
-            base_asset_for_amount = symbol.split('/')[0] if '/' in symbol else symbol 
-            logger.info(f"📈 Trade recorded: {side.upper()} {formatter.format_amount(amount, base_asset_for_amount)} {symbol} @ {formatter.format_price(price, symbol)} ({formatter.format_price(value, symbol)}) [{trade_type}]")
-            
-        except Exception as e:
-            logger.error(f"Failed to record trade: {e}")
+        base_asset_for_amount = symbol.split('/')[0]
+        logger.info(f"📈 Trade recorded: {side.upper()} {await formatter.format_amount(amount, base_asset_for_amount)} {symbol} @ {await formatter.format_price(price, symbol)} ({await formatter.format_price(value, symbol)}) [{trade_type}]")
+
+        self.db_manager._execute_query(
+            "INSERT OR IGNORE INTO trades (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl, linked_order_table_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+            (symbol, side, amount, price, value, trade_type, ts, exchange_fill_id, pnl or 0.0, linked_order_table_id_to_link)
+        )
 
     def health_check(self) -> Dict[str, Any]:
         """Perform health check on all components."""

+ 27 - 12
src/trading/trading_engine.py

@@ -24,7 +24,7 @@ class TradingEngine:
     def __init__(self):
         """Initialize the trading engine."""
         self.client = HyperliquidClient()
-        self.stats = None
+        self.stats: Optional[TradingStats] = None
         self.market_monitor = None  # Will be set by the main bot
         
         # State persistence (Removed - state is now in DB)
@@ -32,24 +32,39 @@ class TradingEngine:
         
         # Position and order tracking (All main state moved to DB via TradingStats)
         
-        # Initialize stats (this will connect to/create the DB)
-        self._initialize_stats()
+        # Initialize stats object, but don't do async setup here
+        self._initialize_stats_sync()
         
         # Initialize price formatter with this trading engine
         set_global_trading_engine(self)
         
-    def _initialize_stats(self):
-        """Initialize trading statistics."""
+    def _initialize_stats_sync(self):
+        """Initialize trading statistics synchronously."""
         try:
             self.stats = TradingStats()
+        except Exception as e:
+            logger.error(f"Could not initialize TradingStats object: {e}")
+            # The application can't run without stats, so we might want to raise
+            raise
             
+    async def async_init(self):
+        """Asynchronously initialize the trading engine components."""
+        await self._initialize_stats_async()
+
+    async def _initialize_stats_async(self):
+        """Fetch initial data asynchronously after the loop has started."""
+        if not self.stats:
+            logger.error("TradingStats not initialized, cannot perform async init.")
+            return
+        try:
             # Set initial balance
+            # NOTE: client.get_balance() is synchronous.
             balance = self.client.get_balance()
             if balance and balance.get('total'):
                 usdc_balance = float(balance['total'].get('USDC', 0))
-                self.stats.set_initial_balance(usdc_balance)
+                await self.stats.set_initial_balance(usdc_balance)
         except Exception as e:
-            logger.error(f"Could not initialize trading stats: {e}")
+            logger.error(f"Could not set initial balance during async init: {e}")
     
     def set_market_monitor(self, market_monitor):
         """Set the market monitor reference for accessing cached data."""
@@ -652,7 +667,7 @@ class TradingEngine:
         """Execute a stop loss order."""
         position = await self.find_position(token)
         if not position:
-            return {"success": False, "error": f"No open position found for {token}"}
+            return {"success": False, "error": f"No open position found for {token} to set stop loss."}
         
         formatter = get_formatter() # Get formatter
         try:
@@ -720,7 +735,7 @@ class TradingEngine:
                         # Ensure that if an old SL (e.g. stop-market) existed and is being replaced,
                         # it might need to be cancelled first. However, current flow assumes this /sl places a new/updated one.
                         # For simplicity, link_stop_loss_to_trade will update if one already exists or insert.
-                        self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, stop_price)
+                        await self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, stop_price)
                         logger.info(f"🛡️ Linked SL limit order {exchange_oid} to lifecycle {lifecycle_id} for {symbol} (from /sl command)")
             
             return {
@@ -748,7 +763,7 @@ class TradingEngine:
         """Execute a take profit order."""
         position = await self.find_position(token)
         if not position:
-            return {"success": False, "error": f"No open position found for {token}"}
+            return {"success": False, "error": f"No open position found for {token} to set take profit."}
         
         formatter = get_formatter() # Get formatter
         try:
@@ -811,7 +826,7 @@ class TradingEngine:
                 if active_trade_lc:
                     lifecycle_id = active_trade_lc.get('trade_lifecycle_id')
                     if exchange_oid: # If TP order placed successfully on exchange
-                        self.stats.link_take_profit_to_trade(lifecycle_id, exchange_oid, profit_price)
+                        await self.stats.link_take_profit_to_trade(lifecycle_id, exchange_oid, profit_price)
                         logger.info(f"🎯 Linked TP order {exchange_oid} to lifecycle {lifecycle_id} for {symbol}")
             
             return {
@@ -1016,7 +1031,7 @@ class TradingEngine:
                 set_exchange_order_id=exchange_oid
             )
             # 4. Link this exchange SL order to the trade lifecycle
-            self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, sl_price)
+            await self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, sl_price)
             logger.info(f"🛡️ Successfully placed and linked SL limit order {exchange_oid} to lifecycle {lifecycle_id} for {symbol}")
         else:
             logger.warning(f"No exchange_order_id received for SL limit order {order_db_id} ({bot_order_ref_id}, LC: {lifecycle_id[:8]}). Status remains pending_submission.")

+ 70 - 68
src/utils/token_display_formatter.py

@@ -34,11 +34,11 @@ class TokenDisplayFormatter:
         self._precision_cache: Dict[str, Dict[str, int]] = {}
         self._markets_cache: Optional[Dict[str, Any]] = None
 
-    def _load_markets_data(self) -> Dict[str, Any]:
+    async def _load_markets_data(self) -> Dict[str, Any]:
         """Load markets data with caching."""
         if self._markets_cache is None and self.trading_engine:
             try:
-                markets = self.trading_engine.client.get_markets()
+                markets = await self.trading_engine.client.get_markets()
                 if markets:
                     self._markets_cache = markets
                     logger.info(f"📊 Loaded {len(markets)} markets for precision data")
@@ -50,7 +50,7 @@ class TokenDisplayFormatter:
                 self._markets_cache = {}
         return self._markets_cache or {}
 
-    def _fetch_and_cache_precisions(self, token: str) -> Optional[Dict[str, int]]:
+    async def _fetch_and_cache_precisions(self, token: str) -> Optional[Dict[str, int]]:
         """
         Fetches price and amount precisions for a token from market data and caches them.
         Returns the cached dict {'price_decimals': X, 'amount_decimals': Y} or None if not found.
@@ -59,48 +59,39 @@ class TokenDisplayFormatter:
         if normalized_token in self._precision_cache:
             return self._precision_cache[normalized_token]
 
-        markets = self._load_markets_data()
+        markets = await self._load_markets_data()
         if not markets:
             logger.debug(f"No markets data available for {normalized_token}, cannot fetch precisions.")
             return None
 
-        possible_symbols = [
+        # Search for the market symbol in a more streamlined way
+        symbol_variants = [
             f"{normalized_token}/USDC:USDC",
             f"{normalized_token}/USDC",
-            # Add other common quote currencies or symbol formats if necessary
+            token  # Direct match for fully qualified symbols
         ]
-        # Handle k-tokens or other prefixed tokens if that's a pattern
-        if normalized_token.startswith('K') and len(normalized_token) > 1 and not normalized_token[1].isnumeric(): # e.g. kPEPE but not KSM
-             possible_symbols.append(f"{normalized_token}/USDC:USDC") # Already covered if normalized_token includes 'k'
-             possible_symbols.append(f"{normalized_token.upper()}/USDC:USDC") # Try uppercase original if 'k' was part of normalization strategy
-
+        
         market_info = None
-        for symbol_variant in possible_symbols:
-            if symbol_variant in markets:
-                market_info = markets[symbol_variant]
+        for symbol in symbol_variants:
+            if symbol in markets:
+                market_info = markets[symbol]
                 break
         
         if not market_info:
-            # Try direct lookup for already fully qualified symbols (e.g. "PEPE/USDC:USDC")
-            if token in markets:
-                 market_info = markets[token]
-            else:
-                logger.warning(f"Market info not found for {normalized_token} or its variants.")
-                return None
+            logger.warning(f"Market info not found for {normalized_token} or its variants.")
+            return None
 
         precision_info = market_info.get('precision', {})
-        price_precision_val = precision_info.get('price')
-        amount_precision_val = precision_info.get('amount')
-
-        price_decimals = self._get_default_price_decimals_for_token(normalized_token) # Default
-        if price_precision_val and price_precision_val > 0:
-            price_decimals = int(-math.log10(price_precision_val))
-
-        # For amount, we typically need it from the exchange; default might be less useful
-        # Defaulting to a common high precision if not found, but ideally exchange provides this.
-        amount_decimals = 6 
-        if amount_precision_val and amount_precision_val > 0:
-            amount_decimals = int(-math.log10(amount_precision_val))
+        price_precision = precision_info.get('price')
+        amount_precision = precision_info.get('amount')
+
+        price_decimals = self._get_default_price_decimals_for_token(normalized_token)
+        if price_precision and price_precision > 0:
+            price_decimals = int(-math.log10(price_precision))
+
+        amount_decimals = 6  # Default amount precision
+        if amount_precision and amount_precision > 0:
+            amount_decimals = int(-math.log10(amount_precision))
         
         self._precision_cache[normalized_token] = {
             'price_decimals': price_decimals,
@@ -109,14 +100,14 @@ class TokenDisplayFormatter:
         logger.debug(f"📊 Cached precisions for {normalized_token}: price {price_decimals}, amount {amount_decimals}")
         return self._precision_cache[normalized_token]
 
-    def get_token_price_decimal_places(self, token: str) -> int:
+    async def get_token_price_decimal_places(self, token: str) -> int:
         """
         Get the number of decimal places for a token's price.
         """
         normalized_token = _normalize_token_case(token)
         precisions = self._precision_cache.get(normalized_token)
         if not precisions:
-            precisions = self._fetch_and_cache_precisions(normalized_token)
+            precisions = await self._fetch_and_cache_precisions(normalized_token)
 
         if precisions:
             return precisions['price_decimals']
@@ -124,14 +115,14 @@ class TokenDisplayFormatter:
         # Fallback to smart default if fetching failed completely
         return self._get_default_price_decimals_for_token(normalized_token)
 
-    def get_token_amount_decimal_places(self, token: str) -> int:
+    async def get_token_amount_decimal_places(self, token: str) -> int:
         """
         Get the number of decimal places for a token's amount (quantity).
         """
         normalized_token = _normalize_token_case(token)
         precisions = self._precision_cache.get(normalized_token)
         if not precisions:
-            precisions = self._fetch_and_cache_precisions(normalized_token)
+            precisions = await self._fetch_and_cache_precisions(normalized_token)
             
         if precisions:
             return precisions['amount_decimals']
@@ -140,19 +131,29 @@ class TokenDisplayFormatter:
         logger.warning(f"Amount precision not found for {normalized_token}, defaulting to 6.")
         return 6 # Default amount precision
 
-    def _get_default_price_decimals_for_token(self, token: str) -> int: # Renamed from _get_default_decimals_for_token
+    def _get_default_price_decimals_for_token(self, token: str) -> int:
         """Get smart default price decimal places based on token characteristics."""
-        token_upper = token.upper() # Ensure consistent casing for checks
+        token_upper = token.upper()
 
-        if token_upper in ['BTC', 'ETH', 'BNB', 'SOL', 'ADA', 'DOT', 'AVAX', 'MATIC', 'LINK']:
+        # Define decimal places for different token categories
+        token_decimals = {
+            2: ['BTC', 'ETH', 'BNB', 'SOL', 'ADA', 'DOT', 'AVAX', 'MATIC', 'LINK'],
+            4: ['DOGE', 'XRP', 'LTC', 'BCH', 'ETC', 'FIL', 'AAVE', 'UNI'],
+            6: ['PEPE', 'SHIB', 'FLOKI', 'BONK', 'WIF']
+        }
+
+        # Check for meme tokens first, as they might be substrings
+        for decimals, tokens in sorted(token_decimals.items(), reverse=True):
+            if any(t in token_upper for t in tokens):
+                return decimals
+        
+        # Check for major tokens
+        if token_upper in token_decimals[2]:
             return 2
-        if token_upper in ['DOGE', 'XRP', 'LTC', 'BCH', 'ETC', 'FIL', 'AAVE', 'UNI']:
-            return 4
-        if any(meme in token_upper for meme in ['PEPE', 'SHIB', 'FLOKI', 'BONK', 'WIF']): # DOGE already covered
-            return 6
-        return 4
+        
+        return 4 # Default for other tokens
 
-    def format_price(self, price: float, token: str = None) -> str:
+    async def format_price(self, price: float, token: str = None) -> str:
         """
         Format a price with appropriate decimal places.
         """
@@ -160,41 +161,42 @@ class TokenDisplayFormatter:
             return "N/A"  # Handle None price gracefully
 
         try:
-            decimal_places = 2 # Default if no token
             if token:
-                decimal_places = self.get_token_price_decimal_places(token)
+                decimal_places = await self.get_token_price_decimal_places(token)
             else:
-                # Smart default based on price magnitude if no token provided
-                if price == 0: decimal_places = 2
-                elif abs(price) >= 1000: decimal_places = 2
-                elif abs(price) >= 1: decimal_places = 3
-                elif abs(price) >= 0.01: decimal_places = 4
-                elif abs(price) >= 0.0001: decimal_places = 6
-                else: decimal_places = 8
+                # Smart default based on price magnitude
+                if price == 0:
+                    decimal_places = 2
+                elif abs(price) >= 1000:
+                    decimal_places = 2
+                elif abs(price) >= 1:
+                    decimal_places = 3
+                elif abs(price) >= 0.01:
+                    decimal_places = 4
+                else:
+                    decimal_places = 6
             
             return f"{price:,.{decimal_places}f}"
         except Exception as e:
             logger.error(f"❌ Error formatting price {price} for {token}: {e}")
-            # Fallback for other errors, assuming price is not None here due to the check above
-            # If it could still be an issue, provide a very basic fallback.
             try:
                 return f"{price:,.2f}"
-            except: # Final fallback if even basic formatting fails
-                return str(price) # Convert to string as last resort
+            except Exception:
+                return str(price)
 
-    def format_price_with_symbol(self, price: float, token: str = None) -> str:
+    async def format_price_with_symbol(self, price: float, token: str = None) -> str:
         """
         Format a price with currency symbol and appropriate decimal places.
         """
-        formatted_price = self.format_price(price, token)
+        formatted_price = await self.format_price(price, token)
         return f"${formatted_price}"
 
-    def format_amount(self, amount: float, token: str) -> str:
+    async def format_amount(self, amount: float, token: str) -> str:
         """
         Format an amount (quantity) with appropriate decimal places for the given token.
         """
         try:
-            decimal_places = self.get_token_amount_decimal_places(token)
+            decimal_places = await self.get_token_amount_decimal_places(token)
             return f"{amount:,.{decimal_places}f}"
         except Exception as e:
             logger.error(f"❌ Error formatting amount {amount} for {token}: {e}")
@@ -232,14 +234,14 @@ def get_formatter() -> TokenDisplayFormatter:
         _global_formatter = TokenDisplayFormatter()
     return _global_formatter
 
-def format_price(price: float, token: str = None) -> str:
+async def format_price(price: float, token: str = None) -> str:
     """Convenience function to format price using global formatter."""
-    return get_formatter().format_price(price, token)
+    return await get_formatter().format_price(price, token)
 
-def format_price_with_symbol(price: float, token: str = None) -> str:
+async def format_price_with_symbol(price: float, token: str = None) -> str:
     """Convenience function to format price with $ symbol using global formatter."""
-    return get_formatter().format_price_with_symbol(price, token)
+    return await get_formatter().format_price_with_symbol(price, token)
 
-def format_amount(amount: float, token: str) -> str:
+async def format_amount(amount: float, token: str) -> str:
     """Convenience function to format amount using global formatter."""
-    return get_formatter().format_amount(amount, token) 
+    return await get_formatter().format_amount(amount, token) 

+ 1 - 1
tests/__init__.py

@@ -1 +1 @@
- 
+# This file makes the tests directory a package. 

+ 0 - 104
tests/run_all_tests.py

@@ -1,104 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test Runner for Hyperliquid Trading Bot
-
-Runs all test modules and provides a summary of results.
-"""
-
-import sys
-import os
-import importlib.util
-from pathlib import Path
-
-# Add the project root to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-def run_test_module(test_file_path):
-    """Run a single test module and return results."""
-    test_name = test_file_path.stem
-    
-    try:
-        print(f"\n{'='*60}")
-        print(f"🧪 Running {test_name}")
-        print(f"{'='*60}")
-        
-        # Load and execute the test module
-        spec = importlib.util.spec_from_file_location(test_name, test_file_path)
-        module = importlib.util.module_from_spec(spec)
-        
-        # Execute the module
-        spec.loader.exec_module(module)
-        
-        # Try to find and run a main test function
-        if hasattr(module, 'main') and callable(module.main):
-            result = module.main()
-            return test_name, result if isinstance(result, bool) else True
-        elif hasattr(module, f'test_{test_name.replace("test_", "")}') and callable(getattr(module, f'test_{test_name.replace("test_", "")}')):
-            func_name = f'test_{test_name.replace("test_", "")}'
-            result = getattr(module, func_name)()
-            return test_name, result if isinstance(result, bool) else True
-        else:
-            # If no specific test function, assume module execution was the test
-            print(f"✅ {test_name} completed (no return value)")
-            return test_name, True
-            
-    except Exception as e:
-        print(f"❌ {test_name} failed: {e}")
-        import traceback
-        traceback.print_exc()
-        return test_name, False
-
-def main():
-    """Run all tests and provide summary."""
-    print("🚀 Hyperliquid Trading Bot - Test Suite")
-    print("="*60)
-    
-    # Find all test files
-    tests_dir = Path(__file__).parent
-    test_files = list(tests_dir.glob("test_*.py"))
-    
-    if not test_files:
-        print("❌ No test files found!")
-        return False
-    
-    print(f"📋 Found {len(test_files)} test files:")
-    for test_file in test_files:
-        print(f"  • {test_file.name}")
-    
-    # Run all tests
-    results = []
-    for test_file in test_files:
-        test_name, success = run_test_module(test_file)
-        results.append((test_name, success))
-    
-    # Print summary
-    print(f"\n{'='*60}")
-    print(f"📊 TEST SUMMARY")
-    print(f"{'='*60}")
-    
-    passed = sum(1 for _, success in results if success)
-    failed = len(results) - passed
-    
-    print(f"✅ Passed: {passed}")
-    print(f"❌ Failed: {failed}")
-    print(f"📊 Total:  {len(results)}")
-    
-    print(f"\n📋 Individual Results:")
-    for test_name, success in results:
-        status = "✅ PASS" if success else "❌ FAIL"
-        print(f"  {status} {test_name}")
-    
-    if failed == 0:
-        print(f"\n🎉 ALL TESTS PASSED!")
-        print(f"🚀 Bot is ready for deployment!")
-        return True
-    else:
-        print(f"\n💥 {failed} TEST(S) FAILED!")
-        print(f"🔧 Please fix failing tests before deployment.")
-        return False
-
-if __name__ == "__main__":
-    success = main()
-    sys.exit(0 if success else 1) 

+ 0 - 257
tests/test_alarm_system.py

@@ -1,257 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test suite for the Price Alarm System
-
-Tests alarm creation, management, triggering, and integration.
-"""
-
-import unittest
-import tempfile
-import os
-from datetime import datetime
-from alarm_manager import AlarmManager
-
-
-class TestAlarmManager(unittest.TestCase):
-    """Test the AlarmManager class."""
-    
-    def setUp(self):
-        """Set up test fixtures."""
-        # Use temporary file for testing
-        self.temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.json')
-        self.temp_file.close()
-        self.alarm_manager = AlarmManager(self.temp_file.name)
-    
-    def tearDown(self):
-        """Clean up after tests."""
-        # Remove temporary file
-        if os.path.exists(self.temp_file.name):
-            os.unlink(self.temp_file.name)
-    
-    def test_create_alarm_above(self):
-        """Test creating an alarm for price going above current price."""
-        # Current price $50, target $55 (above)
-        alarm = self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        
-        self.assertEqual(alarm['id'], 1)
-        self.assertEqual(alarm['token'], 'BTC')
-        self.assertEqual(alarm['target_price'], 55.0)
-        self.assertEqual(alarm['current_price_when_set'], 50.0)
-        self.assertEqual(alarm['direction'], 'above')
-        self.assertEqual(alarm['status'], 'active')
-    
-    def test_create_alarm_below(self):
-        """Test creating an alarm for price going below current price."""
-        # Current price $50, target $45 (below)
-        alarm = self.alarm_manager.create_alarm("ETH", 45.0, 50.0)
-        
-        self.assertEqual(alarm['id'], 1)
-        self.assertEqual(alarm['token'], 'ETH')
-        self.assertEqual(alarm['target_price'], 45.0)
-        self.assertEqual(alarm['current_price_when_set'], 50.0)
-        self.assertEqual(alarm['direction'], 'below')
-        self.assertEqual(alarm['status'], 'active')
-    
-    def test_multiple_alarms_increment_id(self):
-        """Test that multiple alarms get incrementing IDs."""
-        alarm1 = self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        alarm2 = self.alarm_manager.create_alarm("ETH", 3500.0, 3000.0)
-        alarm3 = self.alarm_manager.create_alarm("BTC", 45.0, 50.0)
-        
-        self.assertEqual(alarm1['id'], 1)
-        self.assertEqual(alarm2['id'], 2)
-        self.assertEqual(alarm3['id'], 3)
-    
-    def test_get_alarms_by_token(self):
-        """Test filtering alarms by token."""
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        self.alarm_manager.create_alarm("ETH", 3500.0, 3000.0)
-        self.alarm_manager.create_alarm("BTC", 45.0, 50.0)
-        
-        btc_alarms = self.alarm_manager.get_alarms_by_token("BTC")
-        eth_alarms = self.alarm_manager.get_alarms_by_token("ETH")
-        
-        self.assertEqual(len(btc_alarms), 2)
-        self.assertEqual(len(eth_alarms), 1)
-        self.assertEqual(btc_alarms[0]['token'], 'BTC')
-        self.assertEqual(eth_alarms[0]['token'], 'ETH')
-    
-    def test_remove_alarm(self):
-        """Test removing an alarm by ID."""
-        alarm = self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        alarm_id = alarm['id']
-        
-        # Verify alarm exists
-        self.assertIsNotNone(self.alarm_manager.get_alarm_by_id(alarm_id))
-        
-        # Remove alarm
-        result = self.alarm_manager.remove_alarm(alarm_id)
-        self.assertTrue(result)
-        
-        # Verify alarm is gone
-        self.assertIsNone(self.alarm_manager.get_alarm_by_id(alarm_id))
-    
-    def test_remove_nonexistent_alarm(self):
-        """Test removing an alarm that doesn't exist."""
-        result = self.alarm_manager.remove_alarm(999)
-        self.assertFalse(result)
-    
-    def test_check_alarms_trigger_above(self):
-        """Test triggering an alarm when price goes above target."""
-        # Create alarm: trigger when BTC goes above $55
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        
-        # Price data: BTC is now $56 (above $55)
-        price_data = {"BTC": 56.0}
-        triggered = self.alarm_manager.check_alarms(price_data)
-        
-        self.assertEqual(len(triggered), 1)
-        self.assertEqual(triggered[0]['token'], 'BTC')
-        self.assertEqual(triggered[0]['triggered_price'], 56.0)
-        self.assertEqual(triggered[0]['status'], 'triggered')
-    
-    def test_check_alarms_trigger_below(self):
-        """Test triggering an alarm when price goes below target."""
-        # Create alarm: trigger when ETH goes below $45
-        self.alarm_manager.create_alarm("ETH", 45.0, 50.0)
-        
-        # Price data: ETH is now $44 (below $45)
-        price_data = {"ETH": 44.0}
-        triggered = self.alarm_manager.check_alarms(price_data)
-        
-        self.assertEqual(len(triggered), 1)
-        self.assertEqual(triggered[0]['token'], 'ETH')
-        self.assertEqual(triggered[0]['triggered_price'], 44.0)
-        self.assertEqual(triggered[0]['status'], 'triggered')
-    
-    def test_check_alarms_no_trigger(self):
-        """Test that alarms don't trigger when conditions aren't met."""
-        # Create alarm: trigger when BTC goes above $55
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        
-        # Price data: BTC is now $54 (still below $55)
-        price_data = {"BTC": 54.0}
-        triggered = self.alarm_manager.check_alarms(price_data)
-        
-        self.assertEqual(len(triggered), 0)
-        
-        # Verify alarm is still active
-        active_alarms = self.alarm_manager.get_all_active_alarms()
-        self.assertEqual(len(active_alarms), 1)
-        self.assertEqual(active_alarms[0]['status'], 'active')
-    
-    def test_check_alarms_exact_price(self):
-        """Test that alarms trigger at exact target price."""
-        # Create alarm: trigger when BTC goes above $55
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        
-        # Price data: BTC is exactly $55
-        price_data = {"BTC": 55.0}
-        triggered = self.alarm_manager.check_alarms(price_data)
-        
-        self.assertEqual(len(triggered), 1)
-        self.assertEqual(triggered[0]['triggered_price'], 55.0)
-    
-    def test_alarm_only_triggers_once(self):
-        """Test that triggered alarms don't trigger again."""
-        # Create alarm
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        
-        # Trigger alarm
-        price_data = {"BTC": 56.0}
-        triggered1 = self.alarm_manager.check_alarms(price_data)
-        self.assertEqual(len(triggered1), 1)
-        
-        # Try to trigger again with even higher price
-        price_data = {"BTC": 60.0}
-        triggered2 = self.alarm_manager.check_alarms(price_data)
-        self.assertEqual(len(triggered2), 0)  # Should not trigger again
-    
-    def test_multiple_alarms_selective_trigger(self):
-        """Test that only applicable alarms trigger."""
-        # Create multiple alarms
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)  # Above $55
-        self.alarm_manager.create_alarm("ETH", 45.0, 50.0)  # Below $45
-        self.alarm_manager.create_alarm("BTC", 40.0, 50.0)  # Below $40
-        
-        # Price data: BTC $56 (triggers first), ETH $46 (doesn't trigger)
-        price_data = {"BTC": 56.0, "ETH": 46.0}
-        triggered = self.alarm_manager.check_alarms(price_data)
-        
-        self.assertEqual(len(triggered), 1)
-        self.assertEqual(triggered[0]['token'], 'BTC')
-        self.assertEqual(triggered[0]['target_price'], 55.0)
-    
-    def test_persistence(self):
-        """Test that alarms persist across manager instances."""
-        # Create alarm
-        alarm = self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        alarm_id = alarm['id']
-        
-        # Create new manager instance with same file
-        new_manager = AlarmManager(self.temp_file.name)
-        
-        # Check that alarm persists
-        restored_alarm = new_manager.get_alarm_by_id(alarm_id)
-        self.assertIsNotNone(restored_alarm)
-        self.assertEqual(restored_alarm['token'], 'BTC')
-        self.assertEqual(restored_alarm['target_price'], 55.0)
-        self.assertEqual(restored_alarm['status'], 'active')
-    
-    def test_format_alarm_list_empty(self):
-        """Test formatting empty alarm list."""
-        message = self.alarm_manager.format_alarm_list([])
-        self.assertIn("No alarms found", message)
-    
-    def test_format_alarm_list_with_alarms(self):
-        """Test formatting alarm list with alarms."""
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        self.alarm_manager.create_alarm("ETH", 45.0, 50.0)
-        
-        alarms = self.alarm_manager.get_all_active_alarms()
-        message = self.alarm_manager.format_alarm_list(alarms)
-        
-        self.assertIn("BTC", message)
-        self.assertIn("ETH", message)
-        self.assertIn("ID 1", message)
-        self.assertIn("ID 2", message)
-        self.assertIn("55.00", message)
-        self.assertIn("45.00", message)
-    
-    def test_format_triggered_alarm(self):
-        """Test formatting triggered alarm notification."""
-        # Create and trigger alarm
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        price_data = {"BTC": 56.0}
-        triggered = self.alarm_manager.check_alarms(price_data)
-        
-        message = self.alarm_manager.format_triggered_alarm(triggered[0])
-        
-        self.assertIn("Price Alert Triggered", message)
-        self.assertIn("BTC", message)
-        self.assertIn("56.00", message)
-        self.assertIn("55.00", message)
-        self.assertIn("50.00", message)
-    
-    def test_statistics(self):
-        """Test alarm statistics."""
-        # Create some alarms
-        self.alarm_manager.create_alarm("BTC", 55.0, 50.0)
-        self.alarm_manager.create_alarm("BTC", 45.0, 50.0)
-        self.alarm_manager.create_alarm("ETH", 3500.0, 3000.0)
-        
-        # Trigger one alarm
-        price_data = {"BTC": 56.0}
-        self.alarm_manager.check_alarms(price_data)
-        
-        stats = self.alarm_manager.get_statistics()
-        
-        self.assertEqual(stats['total_active'], 2)  # 2 still active
-        self.assertEqual(stats['total_triggered'], 1)  # 1 triggered
-        self.assertEqual(stats['tokens_tracked'], 2)  # BTC and ETH
-        self.assertEqual(stats['token_breakdown']['BTC'], 1)  # 1 BTC alarm left
-        self.assertEqual(stats['token_breakdown']['ETH'], 1)  # 1 ETH alarm
-
-
-if __name__ == '__main__':
-    unittest.main() 

+ 0 - 111
tests/test_balance.py

@@ -1,111 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to verify Hyperliquid balance fetching with CCXT
-"""
-
-import sys
-import os
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from hyperliquid_client import HyperliquidClient
-from config import Config
-
-def test_balance():
-    """Test balance fetching functionality."""
-    print("🧪 Testing Hyperliquid Balance Fetching")
-    print("=" * 50)
-    
-    try:
-        # Validate configuration first
-        print("🔍 Validating configuration...")
-        if not Config.validate():
-            print("❌ Configuration validation failed!")
-            return False
-        
-        print(f"✅ Configuration valid")
-        print(f"🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}")
-        print(f"🔑 Private Key: {Config.HYPERLIQUID_PRIVATE_KEY[:10]}..." if Config.HYPERLIQUID_PRIVATE_KEY else "❌ No private key")
-        print()
-        
-        # Initialize client
-        print("🔧 Initializing Hyperliquid client...")
-        client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
-        
-        if not client.sync_client:
-            print("❌ Failed to initialize client!")
-            return False
-        
-        print("✅ Client initialized successfully")
-        print()
-        
-        # Test balance fetching
-        print("💰 Fetching account balance...")
-        balance = client.get_balance()
-        
-        if balance:
-            print("✅ Balance fetch successful!")
-            print("📊 Balance data:")
-            
-            # Pretty print the balance
-            if isinstance(balance, dict):
-                for key, value in balance.items():
-                    if isinstance(value, dict):
-                        print(f"  {key}:")
-                        for sub_key, sub_value in value.items():
-                            print(f"    {sub_key}: {sub_value}")
-                    else:
-                        print(f"  {key}: {value}")
-            else:
-                print(f"  Raw balance: {balance}")
-                
-            return True
-        else:
-            print("❌ Balance fetch failed! Trying alternative method...")
-            print()
-            
-            # Try alternative method
-            print("🔄 Testing alternative balance fetching approaches...")
-            balance_alt = client.get_balance_alternative()
-            
-            if balance_alt:
-                print("✅ Alternative balance fetch successful!")
-                print("📊 Balance data:")
-                
-                # Pretty print the balance
-                if isinstance(balance_alt, dict):
-                    for key, value in balance_alt.items():
-                        if isinstance(value, dict):
-                            print(f"  {key}:")
-                            for sub_key, sub_value in value.items():
-                                print(f"    {sub_key}: {sub_value}")
-                        else:
-                            print(f"  {key}: {value}")
-                else:
-                    print(f"  Raw balance: {balance_alt}")
-                    
-                return True
-            else:
-                print("❌ Alternative balance fetch also failed!")
-                return False
-            
-    except Exception as e:
-        print(f"💥 Test failed with error: {e}")
-        import traceback
-        print("📜 Full traceback:")
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_balance()
-    
-    if success:
-        print("\n🎉 Balance test PASSED!")
-        sys.exit(0)
-    else:
-        print("\n💥 Balance test FAILED!")
-        sys.exit(1) 

+ 0 - 162
tests/test_bot_fixes.py

@@ -1,162 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to verify that the bot configuration and format string fixes work correctly.
-"""
-
-import sys
-import os
-sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
-
-from config import Config
-
-def test_config_fixes():
-    """Test that the configuration changes work correctly."""
-    print("🔧 Configuration Fixes Test")
-    print("=" * 50)
-    
-    # Test that the new configuration variables exist
-    try:
-        token = Config.DEFAULT_TRADING_TOKEN
-        risk_enabled = Config.RISK_MANAGEMENT_ENABLED
-        stop_loss = Config.STOP_LOSS_PERCENTAGE
-        heartbeat = Config.BOT_HEARTBEAT_SECONDS
-        
-        print(f"✅ DEFAULT_TRADING_TOKEN: {token}")
-        print(f"✅ RISK_MANAGEMENT_ENABLED: {risk_enabled}")
-        print(f"✅ STOP_LOSS_PERCENTAGE: {stop_loss}%")
-        print(f"✅ BOT_HEARTBEAT_SECONDS: {heartbeat}")
-        
-        # Test that old variables are gone
-        try:
-            amount = Config.DEFAULT_TRADE_AMOUNT
-            print(f"❌ DEFAULT_TRADE_AMOUNT still exists: {amount}")
-        except AttributeError:
-            print("✅ DEFAULT_TRADE_AMOUNT properly removed")
-        
-        try:
-            symbol = Config.DEFAULT_TRADING_SYMBOL
-            print(f"❌ DEFAULT_TRADING_SYMBOL still exists: {symbol}")
-        except AttributeError:
-            print("✅ DEFAULT_TRADING_SYMBOL properly removed")
-            
-    except AttributeError as e:
-        print(f"❌ Configuration error: {e}")
-        return False
-    
-    return True
-
-def test_format_strings():
-    """Test that format strings work correctly."""
-    print("\n📝 Format String Test")
-    print("=" * 50)
-    
-    try:
-        # Test the format parameters that would be used in telegram bot
-        symbol = Config.DEFAULT_TRADING_TOKEN
-        network = "Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet"
-        risk_enabled = Config.RISK_MANAGEMENT_ENABLED
-        stop_loss = Config.STOP_LOSS_PERCENTAGE
-        heartbeat = Config.BOT_HEARTBEAT_SECONDS
-        
-        # Test format string similar to what's used in the bot
-        test_format = """
-⚙️ Configuration:
-• Default Token: {symbol}
-• Network: {network}
-• Risk Management: {risk_enabled}
-• Stop Loss: {stop_loss}%
-• Monitoring: Every {heartbeat} seconds
-        """.format(
-            symbol=symbol,
-            network=network,
-            risk_enabled=risk_enabled,
-            stop_loss=stop_loss,
-            heartbeat=heartbeat
-        )
-        
-        print("✅ Format string test successful:")
-        print(test_format.strip())
-        
-    except KeyError as e:
-        print(f"❌ Format string error: {e}")
-        return False
-    except Exception as e:
-        print(f"❌ Unexpected error: {e}")
-        return False
-    
-    return True
-
-def test_timestamp_handling():
-    """Test timestamp handling for external trades."""
-    print("\n⏰ Timestamp Handling Test")
-    print("=" * 50)
-    
-    from datetime import datetime, timedelta
-    
-    try:
-        # Test different timestamp formats
-        test_timestamps = [
-            1733155660,  # Unix timestamp (seconds)
-            1733155660000,  # Unix timestamp (milliseconds)
-            "2024-12-02T15:47:40",  # ISO format
-            "2024-12-02T15:47:40.123Z",  # ISO with Z
-        ]
-        
-        base_time = (datetime.now() - timedelta(hours=1)).isoformat()
-        
-        for ts in test_timestamps:
-            try:
-                # Test the conversion logic from the bot
-                if isinstance(ts, (int, float)):
-                    # Assume it's a unix timestamp
-                    ts_str = datetime.fromtimestamp(ts / 1000 if ts > 1e10 else ts).isoformat()
-                else:
-                    ts_str = str(ts)
-                
-                # Test comparison
-                comparison_result = ts_str > base_time
-                print(f"✅ Timestamp {ts} -> {ts_str} (comparison: {comparison_result})")
-                
-            except Exception as e:
-                print(f"❌ Error processing timestamp {ts}: {e}")
-                return False
-                
-        print("✅ All timestamp formats handled correctly")
-        
-    except Exception as e:
-        print(f"❌ Timestamp handling error: {e}")
-        return False
-    
-    return True
-
-if __name__ == "__main__":
-    print("🚀 Bot Fixes Verification Test")
-    print("=" * 60)
-    
-    # Run all tests
-    tests = [
-        test_config_fixes,
-        test_format_strings,
-        test_timestamp_handling
-    ]
-    
-    results = []
-    for test in tests:
-        try:
-            result = test()
-            results.append(result)
-        except Exception as e:
-            print(f"❌ Test failed with exception: {e}")
-            results.append(False)
-    
-    print("\n" + "=" * 60)
-    
-    if all(results):
-        print("🎉 All tests passed! Bot fixes are working correctly.")
-        print("✅ Configuration cleanup successful")
-        print("✅ Format string errors fixed")
-        print("✅ Timestamp comparison issues resolved")
-    else:
-        print("⚠️ Some tests failed. Please check the issues above.")
-        failed_count = len([r for r in results if not r])
-        print(f"❌ {failed_count}/{len(results)} tests failed") 

+ 0 - 101
tests/test_config.py

@@ -1,101 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to verify configuration and CCXT format
-"""
-
-import sys
-import os
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from config import Config
-
-def test_config():
-    """Test configuration setup."""
-    print("🧪 Testing Configuration Setup")
-    print("=" * 50)
-    
-    try:
-        # Test basic configuration
-        print("🔍 Checking environment variables...")
-        print(f"📂 Current directory: {os.getcwd()}")
-        print(f"📄 .env file exists: {os.path.exists('.env')}")
-        print()
-        
-        # Show raw environment variables
-        print("🔑 Raw Environment Variables:")
-        print(f"  HYPERLIQUID_PRIVATE_KEY: {'✅ Set' if os.getenv('HYPERLIQUID_PRIVATE_KEY') else '❌ Not Set'}")
-        print(f"  HYPERLIQUID_SECRET_KEY: {'✅ Set' if os.getenv('HYPERLIQUID_SECRET_KEY') else '❌ Not Set'}")
-        print(f"  HYPERLIQUID_TESTNET: {os.getenv('HYPERLIQUID_TESTNET', 'NOT SET')}")
-        print(f"  TELEGRAM_BOT_TOKEN: {'✅ Set' if os.getenv('TELEGRAM_BOT_TOKEN') else '❌ Not Set'}")
-        print(f"  TELEGRAM_CHAT_ID: {'✅ Set' if os.getenv('TELEGRAM_CHAT_ID') else '❌ Not Set'}")
-        print(f"  TELEGRAM_ENABLED: {os.getenv('TELEGRAM_ENABLED', 'NOT SET')}")
-        print()
-        
-        # Test Config class
-        print("⚙️ Config Class Values:")
-        print(f"  HYPERLIQUID_PRIVATE_KEY: {'✅ Set (' + Config.HYPERLIQUID_PRIVATE_KEY[:10] + '...)' if Config.HYPERLIQUID_PRIVATE_KEY else '❌ Not Set'}")
-        print(f"  HYPERLIQUID_SECRET_KEY: {'✅ Set (' + Config.HYPERLIQUID_SECRET_KEY[:10] + '...)' if Config.HYPERLIQUID_SECRET_KEY else '❌ Not Set'}")
-        print(f"  HYPERLIQUID_TESTNET: {Config.HYPERLIQUID_TESTNET}")
-        print(f"  TELEGRAM_ENABLED: {Config.TELEGRAM_ENABLED}")
-        print()
-        
-        # Test CCXT configuration format
-        print("🔧 CCXT Configuration Format:")
-        ccxt_config = Config.get_hyperliquid_config()
-        
-        for key, value in ccxt_config.items():
-            if key in ['apiKey', 'secret', 'private_key'] and value:
-                print(f"  {key}: {value[:10]}...")
-            else:
-                print(f"  {key}: {value}")
-        print()
-        
-        # Test validation
-        print("✅ Configuration Validation:")
-        is_valid = Config.validate()
-        
-        if is_valid:
-            print("🎉 Configuration is VALID!")
-            
-            # Show the exact format that will be sent to HyperliquidSync
-            final_config = {
-                'apiKey': Config.HYPERLIQUID_PRIVATE_KEY,
-                'testnet': Config.HYPERLIQUID_TESTNET,
-                'sandbox': Config.HYPERLIQUID_TESTNET,
-            }
-            
-            if Config.HYPERLIQUID_SECRET_KEY:
-                final_config['secret'] = Config.HYPERLIQUID_SECRET_KEY
-            
-            print("\n📋 Final CCXT Config (masked):")
-            for key, value in final_config.items():
-                if key in ['apiKey', 'secret'] and value:
-                    print(f"  '{key}': '{value[:10]}...'")
-                else:
-                    print(f"  '{key}': {value}")
-            
-            return True
-        else:
-            print("❌ Configuration is INVALID!")
-            return False
-            
-    except Exception as e:
-        print(f"💥 Configuration test failed: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_config()
-    
-    if success:
-        print("\n🎉 Config test PASSED!")
-        sys.exit(0)
-    else:
-        print("\n💥 Config test FAILED!")
-        sys.exit(1) 

+ 0 - 21
tests/test_db_mark_price.py

@@ -1,21 +0,0 @@
-import sys
-import os
-from datetime import datetime
-
-# Adjust path if needed to import TradingStats
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-
-from src.stats.trading_stats import TradingStats
-
-def print_open_positions_mark_prices():
-    stats = TradingStats()
-    open_positions = stats.get_open_positions()
-    print(f"Found {len(open_positions)} open positions:")
-    for pos in open_positions:
-        symbol = pos.get('symbol', 'N/A')
-        entry_price = pos.get('entry_price', 'N/A')
-        mark_price = pos.get('mark_price', 'N/A')
-        print(f"Symbol: {symbol}, Entry Price: {entry_price}, Mark Price: {mark_price}")
-
-if __name__ == "__main__":
-    print_open_positions_mark_prices() 

+ 0 - 130
tests/test_exit_command.py

@@ -1,130 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script for new exit command functionality
-"""
-
-import sys
-import os
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from hyperliquid_client import HyperliquidClient
-from config import Config
-
-def test_exit_command():
-    """Test the exit command functionality."""
-    print("🧪 Testing Exit Command Functionality")
-    print("=" * 50)
-    
-    try:
-        # Test configuration
-        if not Config.validate():
-            print("❌ Configuration validation failed!")
-            return False
-        
-        print(f"✅ Configuration valid")
-        print(f"🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}")
-        print()
-        
-        # Initialize client
-        print("🔧 Initializing Hyperliquid client...")
-        client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
-        
-        if not client.sync_client:
-            print("❌ Failed to initialize client!")
-            return False
-        
-        print("✅ Client initialized successfully")
-        print()
-        
-        # Test position fetching (required for exit command)
-        print("📊 Testing position fetching...")
-        positions = client.get_positions()
-        
-        if positions is not None:
-            print(f"✅ Successfully fetched positions: {len(positions)} total")
-            
-            # Show open positions
-            open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
-            
-            if open_positions:
-                print(f"📈 Found {len(open_positions)} open positions:")
-                for pos in open_positions:
-                    symbol = pos.get('symbol', 'Unknown')
-                    contracts = float(pos.get('contracts', 0))
-                    entry_price = float(pos.get('entryPx', 0))
-                    unrealized_pnl = float(pos.get('unrealizedPnl', 0))
-                    
-                    position_type = "LONG" if contracts > 0 else "SHORT"
-                    
-                    print(f"  • {symbol}: {position_type} {abs(contracts)} @ ${entry_price:.2f} (P&L: ${unrealized_pnl:.2f})")
-                    
-                    # Test token extraction
-                    if '/' in symbol:
-                        token = symbol.split('/')[0]
-                        print(f"    → Token for exit command: {token}")
-                        print(f"    → Exit command would be: /exit {token}")
-                        
-                        # Test what exit would do
-                        exit_side = "sell" if contracts > 0 else "buy"
-                        print(f"    → Would place: {exit_side.upper()} {abs(contracts)} {token} (market order)")
-                print()
-            else:
-                print("📭 No open positions found")
-                print("💡 To test /exit command, first open a position with /long or /short")
-                print()
-        else:
-            print("❌ Could not fetch positions")
-            return False
-        
-        # Test market data fetching (required for current price in exit)
-        print("💵 Testing market data fetching...")
-        test_tokens = ['BTC', 'ETH']
-        
-        for token in test_tokens:
-            symbol = f"{token}/USDC:USDC"
-            market_data = client.get_market_data(symbol)
-            
-            if market_data:
-                price = float(market_data['ticker'].get('last', 0))
-                print(f"  ✅ {token}: ${price:,.2f}")
-            else:
-                print(f"  ❌ Failed to get price for {token}")
-        
-        print()
-        print("🎉 Exit command tests completed!")
-        print()
-        print("📝 Exit Command Summary:")
-        print("  • ✅ Position fetching: Working")
-        print("  • ✅ Market data: Working")
-        print("  • ✅ Token parsing: Working")
-        print("  • ✅ Exit logic: Ready")
-        print()
-        print("🚀 Ready to test /exit commands:")
-        print("  /exit BTC    # Close Bitcoin position")
-        print("  /exit ETH    # Close Ethereum position")
-        
-        return True
-        
-    except Exception as e:
-        print(f"💥 Test failed with error: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_exit_command()
-    
-    if success:
-        print("\n🎉 Exit command test PASSED!")
-        print("\n📱 Ready to test on Telegram:")
-        print("  /exit BTC")
-        print("  /exit ETH")
-        sys.exit(0)
-    else:
-        print("\n💥 Exit command test FAILED!")
-        sys.exit(1) 

+ 0 - 58
tests/test_heartbeat_config.py

@@ -1,58 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to demonstrate configurable heartbeat interval
-"""
-
-import os
-import sys
-from pathlib import Path
-
-# Add the src directory to the path
-project_root = Path(__file__).parent
-sys.path.insert(0, str(project_root / 'src'))
-
-def test_heartbeat_config():
-    """Test the configurable heartbeat interval."""
-    print("🧪 Testing Configurable Bot Heartbeat")
-    print("=" * 50)
-    
-    # Test with different values
-    test_values = [
-        ("10", "Fast monitoring (10 seconds)"),
-        ("30", "Default monitoring (30 seconds)"), 
-        ("60", "Slow monitoring (60 seconds)"),
-        ("300", "Very slow monitoring (5 minutes)"),
-        ("3", "Too fast (should warn)"),
-        ("700", "Too slow (should warn)")
-    ]
-    
-    for value, description in test_values:
-        print(f"\n🔍 Testing: {description}")
-        
-        # Set environment variable
-        os.environ['BOT_HEARTBEAT_SECONDS'] = value
-        
-        # Reload config (simulate fresh import)
-        if 'config' in sys.modules:
-            del sys.modules['config']
-        
-        try:
-            from config import Config
-            
-            print(f"   📊 BOT_HEARTBEAT_SECONDS: {Config.BOT_HEARTBEAT_SECONDS}")
-            
-            # Test validation
-            is_valid = Config.validate()
-            print(f"   ✅ Validation: {'PASSED' if is_valid else 'FAILED'}")
-            
-            # Show what the monitoring message would say
-            print(f"   📱 Monitoring message: 'Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)'")
-            
-        except Exception as e:
-            print(f"   ❌ Error: {e}")
-    
-    print(f"\n🎉 Heartbeat configuration test complete!")
-    print(f"💡 Set BOT_HEARTBEAT_SECONDS in your .env file to customize the monitoring interval")
-
-if __name__ == "__main__":
-    test_heartbeat_config() 

+ 0 - 110
tests/test_integrated_tracking.py

@@ -1,110 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to verify the integrated TradingStats position tracking system.
-This test ensures that the enhanced position tracking in TradingStats
-works correctly for multi-entry/exit scenarios.
-"""
-
-import sys
-import os
-import tempfile
-from datetime import datetime
-
-# Add src directory to path
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
-
-from src.stats import TradingStats
-
-def test_integrated_position_tracking():
-    """Test the integrated position tracking system."""
-    
-    # Create temporary stats file
-    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
-        temp_file = f.name
-    
-    try:
-        # Initialize TradingStats
-        stats = TradingStats(temp_file)
-        stats.set_initial_balance(10000.0)
-        
-        print("Testing Integrated Position Tracking System")
-        print("=" * 50)
-        
-        # Test Case 1: Simple long position
-        print("\n1. Testing simple long position:")
-        action_type = stats.record_trade_with_enhanced_tracking("ETH/USDC", "buy", 1.0, 3000.0, "test1")
-        print(f"   Action: {action_type}")
-        position = stats.get_enhanced_position_state("ETH/USDC")
-        print(f"   Position: {position['contracts']} ETH @ ${position['avg_entry_price']:.2f}")
-        
-        # Test Case 2: Add to long position
-        print("\n2. Adding to long position:")
-        action_type = stats.record_trade_with_enhanced_tracking("ETH/USDC", "buy", 0.5, 3100.0, "test2")
-        print(f"   Action: {action_type}")
-        position = stats.get_enhanced_position_state("ETH/USDC")
-        print(f"   Position: {position['contracts']} ETH @ ${position['avg_entry_price']:.2f}")
-        
-        # Test Case 3: Partial close
-        print("\n3. Partial close of long position:")
-        action_type = stats.record_trade_with_enhanced_tracking("ETH/USDC", "sell", 0.5, 3200.0, "test3")
-        print(f"   Action: {action_type}")
-        position = stats.get_enhanced_position_state("ETH/USDC")
-        print(f"   Remaining position: {position['contracts']} ETH @ ${position['avg_entry_price']:.2f}")
-        
-        # Calculate P&L for the partial close
-        pnl_data = stats.calculate_enhanced_position_pnl("ETH/USDC", 0.5, 3200.0)
-        print(f"   P&L for partial close: ${pnl_data['pnl']:.2f} ({pnl_data['pnl_percent']:.2f}%)")
-        
-        # Test Case 4: Full close
-        print("\n4. Full close of remaining position:")
-        action_type = stats.record_trade_with_enhanced_tracking("ETH/USDC", "sell", 1.0, 3150.0, "test4")
-        print(f"   Action: {action_type}")
-        position = stats.get_enhanced_position_state("ETH/USDC")
-        print(f"   Final position: {position['contracts']} ETH @ ${position['avg_entry_price']:.2f}")
-        
-        # Test Case 5: Short position
-        print("\n5. Testing short position:")
-        action_type = stats.record_trade_with_enhanced_tracking("BTC/USDC", "sell", 0.1, 65000.0, "test5")
-        print(f"   Action: {action_type}")
-        position = stats.get_enhanced_position_state("BTC/USDC")
-        print(f"   Position: {position['contracts']} BTC @ entry price tracking")
-        
-        # Test Case 6: Close short position
-        print("\n6. Closing short position:")
-        action_type = stats.record_trade_with_enhanced_tracking("BTC/USDC", "buy", 0.1, 64500.0, "test6")
-        print(f"   Action: {action_type}")
-        pnl_data = stats.calculate_enhanced_position_pnl("BTC/USDC", 0.1, 64500.0)
-        print(f"   P&L for short close: ${pnl_data['pnl']:.2f} ({pnl_data['pnl_percent']:.2f}%)")
-        
-        # Test Case 7: Verify stats consistency
-        print("\n7. Verifying stats consistency:")
-        basic_stats = stats.get_basic_stats()
-        trades_with_pnl = stats.calculate_trade_pnl()
-        
-        print(f"   Total trades recorded: {basic_stats['total_trades']}")
-        print(f"   Completed trade cycles: {basic_stats['completed_trades']}")
-        print(f"   Total P&L: ${basic_stats['total_pnl']:.2f}")
-        
-        # Show all recorded trades
-        print("\n8. All trades recorded:")
-        for i, trade in enumerate(stats.data['trades'], 1):
-            print(f"   {i}. {trade['side'].upper()} {trade['amount']} {trade['symbol']} @ ${trade['price']:.2f}")
-        
-        print("\n✅ Integration test completed successfully!")
-        print("🔄 Single source of truth for position tracking established!")
-        return True
-        
-    except Exception as e:
-        print(f"❌ Test failed: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-    
-    finally:
-        # Clean up temp file
-        if os.path.exists(temp_file):
-            os.unlink(temp_file)
-
-if __name__ == "__main__":
-    success = test_integrated_position_tracking()
-    exit(0 if success else 1) 

+ 0 - 104
tests/test_logging_system.py

@@ -1,104 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to demonstrate the logging system with rotation and cleanup
-"""
-
-import os
-import sys
-import time
-from pathlib import Path
-
-# Add the src directory to the path
-project_root = Path(__file__).parent
-sys.path.insert(0, str(project_root / 'src'))
-
-def test_logging_system():
-    """Test the logging system with rotation and cleanup."""
-    print("🧪 Testing Advanced Logging System")
-    print("=" * 60)
-    
-    # Set up test environment
-    os.environ['LOG_TO_FILE'] = 'true'
-    os.environ['LOG_FILE_PATH'] = 'test_logs/bot_test.log'
-    os.environ['LOG_MAX_SIZE_MB'] = '1'  # Small size for testing
-    os.environ['LOG_BACKUP_COUNT'] = '3'
-    os.environ['LOG_ROTATION_TYPE'] = 'size'
-    os.environ['LOG_LEVEL'] = 'INFO'
-    
-    # Import after setting environment
-    from logging_config import setup_logging, cleanup_logs, get_log_stats, format_log_stats
-    
-    print("📊 Test Configuration:")
-    print(f"   Log File: test_logs/bot_test.log")
-    print(f"   Max Size: 1 MB")
-    print(f"   Backup Count: 3")
-    print(f"   Rotation Type: size")
-    print()
-    
-    # Initialize logging
-    print("🔧 Initializing logging system...")
-    logger = setup_logging()
-    print("✅ Logging system initialized")
-    print()
-    
-    # Generate lots of log entries to trigger rotation
-    print("📝 Generating log entries to test rotation...")
-    for i in range(1000):
-        logger.info(f"Test log message {i:04d} - This is a sample trading bot log entry with some details about market data, orders, and trading activity.")
-        
-        if i % 100 == 0:
-            print(f"   Generated {i} log entries...")
-    
-    print("✅ Log generation complete")
-    print()
-    
-    # Check log statistics
-    print("📊 Log File Statistics:")
-    stats = get_log_stats()
-    for key, value in stats.items():
-        if key == 'current_size_mb' or key == 'total_size_mb':
-            print(f"   {key}: {value:.2f} MB")
-        else:
-            print(f"   {key}: {value}")
-    print()
-    
-    # Show formatted stats
-    print("📋 Formatted Log Status:")
-    formatted_stats = format_log_stats()
-    print(formatted_stats)
-    print()
-    
-    # Test cleanup function
-    print("🧹 Testing log cleanup (keeping files from last 1 day)...")
-    cleanup_logs(days_to_keep=1)
-    print("✅ Cleanup test complete")
-    print()
-    
-    # Final statistics
-    print("📊 Final Statistics:")
-    final_stats = get_log_stats()
-    print(format_log_stats(final_stats))
-    print()
-    
-    # Show actual files created
-    test_log_dir = Path('test_logs')
-    if test_log_dir.exists():
-        print("📁 Created Log Files:")
-        for log_file in sorted(test_log_dir.glob('*.log*')):
-            size_mb = log_file.stat().st_size / (1024 * 1024)
-            print(f"   📄 {log_file.name} ({size_mb:.2f} MB)")
-    
-    print()
-    print("🎉 Logging system test complete!")
-    print()
-    print("💡 Key Features Demonstrated:")
-    print("   ✅ Automatic log rotation by file size")
-    print("   ✅ Backup file management") 
-    print("   ✅ Log cleanup functionality")
-    print("   ✅ Statistics and monitoring")
-    print("   ✅ Configurable via environment variables")
-    print()
-    print("🚀 Ready for production use!")
-
-if __name__ == "__main__":
-    test_logging_system() 

+ 0 - 147
tests/test_order_management.py

@@ -1,147 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script for new order management features (/orders filtering and /coo)
-"""
-
-import sys
-import os
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from hyperliquid_client import HyperliquidClient
-from config import Config
-
-def test_order_management():
-    """Test the new order management functionality."""
-    print("🧪 Testing Order Management Features")
-    print("=" * 50)
-    
-    try:
-        # Test configuration
-        if not Config.validate():
-            print("❌ Configuration validation failed!")
-            return False
-        
-        print(f"✅ Configuration valid")
-        print(f"🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}")
-        print()
-        
-        # Initialize client
-        print("🔧 Initializing Hyperliquid client...")
-        client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
-        
-        if not client.sync_client:
-            print("❌ Failed to initialize client!")
-            return False
-        
-        print("✅ Client initialized successfully")
-        print()
-        
-        # Test order fetching (required for both enhanced /orders and /coo)
-        print("📋 Testing order fetching...")
-        orders = client.get_open_orders()
-        
-        if orders is not None:
-            print(f"✅ Successfully fetched orders: {len(orders)} total")
-            
-            if orders:
-                print(f"\n📊 Current Open Orders:")
-                token_groups = {}
-                
-                for order in orders:
-                    symbol = order.get('symbol', 'Unknown')
-                    side = order.get('side', 'Unknown')
-                    amount = order.get('amount', 0)
-                    price = order.get('price', 0)
-                    order_id = order.get('id', 'Unknown')
-                    
-                    # Extract token from symbol
-                    token = symbol.split('/')[0] if '/' in symbol else symbol
-                    
-                    if token not in token_groups:
-                        token_groups[token] = []
-                    token_groups[token].append(order)
-                    
-                    print(f"  • {token}: {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})")
-                
-                print(f"\n🔍 Token Groups Found:")
-                for token, token_orders in token_groups.items():
-                    print(f"  • {token}: {len(token_orders)} orders")
-                    print(f"    → /orders {token} would show these {len(token_orders)} orders")
-                    print(f"    → /coo {token} would cancel these {len(token_orders)} orders")
-                
-                # Test filtering logic
-                print(f"\n🧪 Testing Filter Logic:")
-                test_tokens = ['BTC', 'ETH', 'SOL']
-                
-                for token in test_tokens:
-                    target_symbol = f"{token}/USDC:USDC"
-                    filtered = [o for o in orders if o.get('symbol') == target_symbol]
-                    print(f"  • {token}: {len(filtered)} orders would be shown/cancelled")
-                
-                print()
-            else:
-                print("📭 No open orders found")
-                print("💡 To test order management features, first place some orders:")
-                print("  /long BTC 10 44000   # Limit order")
-                print("  /short ETH 5 3500    # Limit order")
-                print()
-        else:
-            print("❌ Could not fetch orders")
-            return False
-        
-        # Test market data fetching (used for token validation)
-        print("💵 Testing market data for token validation...")
-        test_tokens = ['BTC', 'ETH']
-        
-        for token in test_tokens:
-            symbol = f"{token}/USDC:USDC"
-            market_data = client.get_market_data(symbol)
-            
-            if market_data:
-                price = float(market_data['ticker'].get('last', 0))
-                print(f"  ✅ {token}: ${price:,.2f} (token validation would work)")
-            else:
-                print(f"  ❌ Failed to get price for {token}")
-        
-        print()
-        print("🎉 Order management tests completed!")
-        print()
-        print("📝 New Features Summary:")
-        print("  • ✅ Enhanced /orders: Working")
-        print("  • ✅ Token filtering: Working")  
-        print("  • ✅ Order cancellation: Ready")
-        print("  • ✅ Confirmation dialogs: Ready")
-        print()
-        print("🚀 Ready to test new commands:")
-        print("  /orders          # All orders")
-        print("  /orders BTC      # BTC orders only")
-        print("  /orders ETH      # ETH orders only")
-        print("  /coo BTC         # Cancel all BTC orders")
-        print("  /coo ETH         # Cancel all ETH orders")
-        
-        return True
-        
-    except Exception as e:
-        print(f"💥 Test failed with error: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_order_management()
-    
-    if success:
-        print("\n🎉 Order management test PASSED!")
-        print("\n📱 Ready to test on Telegram:")
-        print("  /orders")
-        print("  /orders BTC")
-        print("  /coo BTC")
-        sys.exit(0)
-    else:
-        print("\n💥 Order management test FAILED!")
-        sys.exit(1) 

+ 0 - 177
tests/test_order_monitoring.py

@@ -1,177 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script for order monitoring functionality
-"""
-
-import sys
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from hyperliquid_client import HyperliquidClient
-from config import Config
-
-def test_order_monitoring():
-    """Test the order monitoring functionality."""
-    print("🧪 Testing Order Monitoring System")
-    print("=" * 50)
-    
-    try:
-        # Test configuration
-        if not Config.validate():
-            print("❌ Configuration validation failed!")
-            return False
-        
-        print(f"✅ Configuration valid")
-        print(f"🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}")
-        print()
-        
-        # Initialize client
-        print("🔧 Initializing Hyperliquid client...")
-        client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
-        
-        if not client.sync_client:
-            print("❌ Failed to initialize client!")
-            return False
-        
-        print("✅ Client initialized successfully")
-        print()
-        
-        # Test order fetching (core for monitoring)
-        print("📋 Testing order fetching...")
-        orders = client.get_open_orders()
-        
-        if orders is not None:
-            print(f"✅ Successfully fetched orders: {len(orders)} open orders")
-            
-            if orders:
-                print("📊 Current open orders:")
-                for order in orders:
-                    symbol = order.get('symbol', 'Unknown')
-                    side = order.get('side', 'Unknown')
-                    amount = order.get('amount', 0)
-                    price = order.get('price', 0)
-                    order_id = order.get('id', 'Unknown')
-                    
-                    print(f"  • {symbol}: {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})")
-                    
-                # Test order tracking logic
-                order_ids = {order.get('id') for order in orders if order.get('id')}
-                print(f"📍 Order IDs tracked: {len(order_ids)}")
-                
-                print()
-            else:
-                print("📭 No open orders found")
-                print("💡 To test monitoring:")
-                print("  1. Place some orders with /long BTC 10 45000")
-                print("  2. Orders will be tracked automatically")
-                print("  3. When they fill, you'll get notifications")
-                print()
-        else:
-            print("❌ Could not fetch orders")
-            return False
-        
-        # Test position fetching (for P&L calculation)
-        print("📊 Testing position fetching...")
-        positions = client.get_positions()
-        
-        if positions is not None:
-            print(f"✅ Successfully fetched positions: {len(positions)} total")
-            
-            # Filter for open positions
-            open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
-            
-            if open_positions:
-                print(f"📈 Found {len(open_positions)} open positions for P&L tracking:")
-                position_map = {}
-                
-                for pos in open_positions:
-                    symbol = pos.get('symbol', 'Unknown')
-                    contracts = float(pos.get('contracts', 0))
-                    entry_price = float(pos.get('entryPx', 0))
-                    unrealized_pnl = float(pos.get('unrealizedPnl', 0))
-                    
-                    position_type = "LONG" if contracts > 0 else "SHORT"
-                    position_map[symbol] = {
-                        'contracts': contracts,
-                        'entry_price': entry_price
-                    }
-                    
-                    print(f"  • {symbol}: {position_type} {abs(contracts)} @ ${entry_price:.2f} (P&L: ${unrealized_pnl:.2f})")
-                
-                print(f"📍 Position tracking structure: {len(position_map)} symbols")
-                print()
-            else:
-                print("📭 No open positions found")
-                print("💡 Open a position to test P&L notifications")
-                print()
-        else:
-            print("❌ Could not fetch positions")
-            return False
-        
-        # Test monitoring logic components
-        print("🔄 Testing monitoring logic components...")
-        
-        # Test 1: Order ID tracking
-        print("  ✅ Order ID tracking: Ready")
-        
-        # Test 2: Position comparison
-        print("  ✅ Position comparison: Ready")
-        
-        # Test 3: P&L calculation
-        print("  ✅ P&L calculation: Ready")
-        
-        # Test 4: Token extraction
-        test_symbols = ['BTC/USDC:USDC', 'ETH/USDC:USDC']
-        for symbol in test_symbols:
-            token = symbol.split('/')[0] if '/' in symbol else symbol
-            print(f"  ✅ Token extraction: {symbol} → {token}")
-        
-        print()
-        print("🎉 Order monitoring tests completed!")
-        print()
-        print("📝 Monitoring Summary:")
-        print("  • ✅ Order fetching: Working")
-        print("  • ✅ Position fetching: Working")
-        print("  • ✅ Order ID tracking: Ready")
-        print("  • ✅ Position comparison: Ready")
-        print("  • ✅ P&L calculation: Ready")
-        print("  • ✅ Token extraction: Ready")
-        print()
-        print("🚀 Monitoring features ready:")
-        print("  • 30-second check interval")
-        print("  • Automatic order fill detection")
-        print("  • Real-time P&L calculation")
-        print("  • Instant Telegram notifications")
-        print()
-        print("📱 Start the bot to activate monitoring:")
-        print("  python src/telegram_bot.py")
-        print()
-        print("🔄 Monitoring will automatically:")
-        print("  • Track all open orders")
-        print("  • Detect when orders are filled")
-        print("  • Calculate P&L for closed positions")
-        print("  • Send notifications for all changes")
-        
-        return True
-        
-    except Exception as e:
-        print(f"💥 Test failed with error: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_order_monitoring()
-    
-    if success:
-        print("\n🎉 Order monitoring test PASSED!")
-        print("\n📱 Ready for live monitoring:")
-        print("  python src/telegram_bot.py")
-        sys.exit(0)
-    else:
-        print("\n💥 Order monitoring test FAILED!")
-        sys.exit(1) 

+ 0 - 210
tests/test_performance_command.py

@@ -1,210 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to demonstrate the new /performance command functionality.
-"""
-
-def demo_performance_ranking():
-    """Demo what the performance ranking will look like."""
-    print("🏆 Token Performance Ranking Demo")
-    print("=" * 50)
-    
-    # Mock token performance data (sorted by P&L)
-    token_performance = {
-        'BTC': {
-            'total_pnl': 150.75,
-            'pnl_percentage': 5.2,
-            'completed_trades': 8,
-            'win_rate': 75.0
-        },
-        'ETH': {
-            'total_pnl': 45.30,
-            'pnl_percentage': 3.1,
-            'completed_trades': 5,
-            'win_rate': 60.0
-        },
-        'SOL': {
-            'total_pnl': -25.40,
-            'pnl_percentage': -2.8,
-            'completed_trades': 3,
-            'win_rate': 33.3
-        }
-    }
-    
-    sorted_tokens = sorted(
-        token_performance.items(),
-        key=lambda x: x[1]['total_pnl'],
-        reverse=True
-    )
-    
-    print("🏆 Token Performance Ranking\n")
-    
-    for i, (token, stats) 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}"
-        
-        # P&L emoji
-        pnl_emoji = "🟢" if stats['total_pnl'] >= 0 else "🔴"
-        
-        print(f"{rank_emoji} {token}")
-        print(f"   {pnl_emoji} P&L: ${stats['total_pnl']:,.2f} ({stats['pnl_percentage']:+.1f}%)")
-        print(f"   📊 Trades: {stats['completed_trades']} | Win: {stats['win_rate']:.0f}%")
-        print()
-    
-    # Summary
-    total_pnl = sum(stats['total_pnl'] for stats in token_performance.values())
-    total_trades = sum(stats['completed_trades'] for stats in token_performance.values())
-    total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-    
-    print("💼 Portfolio Summary:")
-    print(f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}")
-    print(f"   📈 Tokens Traded: {len(token_performance)}")
-    print(f"   🔄 Completed Trades: {total_trades}")
-    print()
-    print("💡 Usage: /performance BTC for detailed BTC stats")
-
-def demo_detailed_performance():
-    """Demo what detailed token performance will look like."""
-    print("\n📊 BTC Detailed Performance Demo")
-    print("=" * 50)
-    
-    # Mock detailed BTC stats
-    token_stats = {
-        'token': 'BTC',
-        'total_pnl': 150.75,
-        'pnl_percentage': 5.2,
-        'completed_volume': 2900.00,
-        'expectancy': 18.84,
-        'total_trades': 12,
-        'completed_trades': 8,
-        'buy_trades': 6,
-        'sell_trades': 6,
-        'win_rate': 75.0,
-        'profit_factor': 3.2,
-        'total_wins': 6,
-        'total_losses': 2,
-        'largest_win': 85.50,
-        'largest_loss': 32.20,
-        'avg_win': 42.15,
-        'avg_loss': 28.75,
-        'recent_trades': [
-            {
-                'side': 'buy',
-                'value': 500,
-                'timestamp': '2023-12-01T10:30:00',
-                'pnl': 0
-            },
-            {
-                'side': 'sell',
-                'value': 500,
-                'timestamp': '2023-12-01T14:15:00',
-                'pnl': 45.20
-            },
-            {
-                'side': 'buy',
-                'value': 300,
-                'timestamp': '2023-12-01T16:45:00',
-                'pnl': 0
-            }
-        ]
-    }
-    
-    pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "🔴"
-    
-    print(f"📊 {token_stats['token']} Detailed Performance\n")
-    
-    print("💰 P&L Summary:")
-    print(f"• {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)")
-    print(f"• 💵 Total Volume: ${token_stats['completed_volume']:,.2f}")
-    print(f"• 📈 Expectancy: ${token_stats['expectancy']:,.2f}")
-    print()
-    
-    print("📊 Trading Activity:")
-    print(f"• Total Trades: {token_stats['total_trades']}")
-    print(f"• Completed: {token_stats['completed_trades']}")
-    print(f"• Buy Orders: {token_stats['buy_trades']}")
-    print(f"• Sell Orders: {token_stats['sell_trades']}")
-    print()
-    
-    print("🏆 Performance Metrics:")
-    print(f"• Win Rate: {token_stats['win_rate']:.1f}%")
-    print(f"• Profit Factor: {token_stats['profit_factor']:.2f}")
-    print(f"• Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}")
-    print()
-    
-    print("💡 Best/Worst:")
-    print(f"• Largest Win: ${token_stats['largest_win']:,.2f}")
-    print(f"• Largest Loss: ${token_stats['largest_loss']:,.2f}")
-    print(f"• Avg Win: ${token_stats['avg_win']:,.2f}")
-    print(f"• Avg Loss: ${token_stats['avg_loss']:,.2f}")
-    print()
-    
-    print("🔄 Recent Trades:")
-    for trade in token_stats['recent_trades'][-3:]:
-        side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
-        pnl_display = f" | P&L: ${trade['pnl']:.2f}" if trade['pnl'] != 0 else ""
-        print(f"• {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ 12/01 10:30{pnl_display}")
-    print()
-    print("🔄 Use /performance to see all token rankings")
-
-def demo_no_data_scenarios():
-    """Demo what happens when there's no trading data."""
-    print("\n📭 No Data Scenarios Demo")
-    print("=" * 50)
-    
-    print("1. No trading data at all:")
-    print("📊 Token Performance\n")
-    print("📭 No trading data available yet.\n")
-    print("💡 Performance tracking starts after your first completed trades.")
-    print("Use /long or /short to start trading!")
-    print()
-    
-    print("2. No trading history for specific token:")
-    print("📊 SOL Performance\n")
-    print("📭 No trading history found for SOL.\n")
-    print("💡 Start trading SOL with:")
-    print("• /long SOL 100")
-    print("• /short SOL 100")
-    print()
-    print("🔄 Use /performance to see all token rankings.")
-    print()
-    
-    print("3. Open positions but no completed trades:")
-    print("📊 ETH Performance\n")
-    print("ETH has open positions but no completed trades yet\n")
-    print("📈 Current Activity:")
-    print("• Total Trades: 3")
-    print("• Buy Orders: 2")
-    print("• Sell Orders: 1")
-    print("• Volume: $1,500.00")
-    print()
-    print("💡 Complete some trades to see P&L statistics!")
-    print("🔄 Use /performance to see all token rankings.")
-
-if __name__ == "__main__":
-    print("🚀 Performance Command Demo")
-    print("=" * 60)
-    
-    demo_performance_ranking()
-    demo_detailed_performance() 
-    demo_no_data_scenarios()
-    
-    print("\n" + "=" * 60)
-    print("✅ Key Features:")
-    print("• Token performance ranking (best to worst P&L)")
-    print("• Detailed stats for specific tokens")
-    print("• Win rate, profit factor, expectancy calculations")
-    print("• Recent trade history included")
-    print("• Mobile-friendly compressed and detailed views")
-    print("• Handles cases with no data gracefully")
-    print("• Easy navigation between ranking and details")
-    print("\n💡 Usage:")
-    print("• /performance - Show all token rankings")
-    print("• /performance BTC - Show detailed BTC stats")
-    print("• /performance ETH - Show detailed ETH stats") 

+ 0 - 1
tests/test_period_commands.py

@@ -1 +0,0 @@
- 

+ 0 - 142
tests/test_period_stats_consistency.py

@@ -1,142 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to verify period stats consistency (daily, weekly, monthly).
-This test ensures that the stats show consistent time periods regardless of trading activity.
-"""
-
-import sys
-import os
-import tempfile
-from datetime import datetime, timedelta
-
-# Add src directory to path
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
-
-from src.stats import TradingStats
-
-def test_period_stats_consistency():
-    """Test that period stats show consistent time periods."""
-    
-    # Create temporary stats file
-    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
-        temp_file = f.name
-    
-    try:
-        # Initialize TradingStats
-        stats = TradingStats(temp_file)
-        stats.set_initial_balance(10000.0)
-        
-        print("Testing Period Stats Consistency")
-        print("=" * 40)
-        
-        # Test Case 1: No trades - should still show 10 periods
-        print("\n1. Testing with no trades:")
-        daily_stats = stats.get_daily_stats(10)
-        weekly_stats = stats.get_weekly_stats(10)
-        monthly_stats = stats.get_monthly_stats(10)
-        
-        print(f"   Daily periods: {len(daily_stats)} (should be 10)")
-        print(f"   Weekly periods: {len(weekly_stats)} (should be 10)")
-        print(f"   Monthly periods: {len(monthly_stats)} (should be 10)")
-        
-        # Verify all periods have has_trades = False
-        daily_no_trades = sum(1 for day in daily_stats if not day['has_trades'])
-        weekly_no_trades = sum(1 for week in weekly_stats if not week['has_trades'])
-        monthly_no_trades = sum(1 for month in monthly_stats if not month['has_trades'])
-        
-        print(f"   Days with no trades: {daily_no_trades}/10")
-        print(f"   Weeks with no trades: {weekly_no_trades}/10")
-        print(f"   Months with no trades: {monthly_no_trades}/10")
-        
-        # Test Case 2: Add some trades on specific days
-        print("\n2. Testing with selective trades:")
-        
-        # Add trade today
-        today = datetime.now()
-        stats.record_trade_with_enhanced_tracking("BTC/USDC", "buy", 0.1, 50000.0, "test1")
-        stats.record_trade_with_enhanced_tracking("BTC/USDC", "sell", 0.1, 51000.0, "test2")
-        
-        # Add trade 3 days ago
-        three_days_ago = today - timedelta(days=3)
-        stats.record_trade_with_enhanced_tracking("ETH/USDC", "buy", 1.0, 3000.0, "test3")
-        stats.record_trade_with_enhanced_tracking("ETH/USDC", "sell", 1.0, 3100.0, "test4")
-        
-        # Get updated stats
-        daily_stats = stats.get_daily_stats(10)
-        weekly_stats = stats.get_weekly_stats(10)
-        monthly_stats = stats.get_monthly_stats(10)
-        
-        print(f"   Daily periods: {len(daily_stats)} (should still be 10)")
-        print(f"   Weekly periods: {len(weekly_stats)} (should still be 10)")
-        print(f"   Monthly periods: {len(monthly_stats)} (should still be 10)")
-        
-        # Count periods with trades
-        daily_with_trades = sum(1 for day in daily_stats if day['has_trades'])
-        weekly_with_trades = sum(1 for week in weekly_stats if week['has_trades'])
-        monthly_with_trades = sum(1 for month in monthly_stats if month['has_trades'])
-        
-        print(f"   Days with trades: {daily_with_trades} (should be 2)")
-        print(f"   Weeks with trades: {weekly_with_trades}")
-        print(f"   Months with trades: {monthly_with_trades}")
-        
-        # Test Case 3: Check date consistency
-        print("\n3. Testing date consistency:")
-        today_str = datetime.now().strftime('%Y-%m-%d')
-        
-        # Today should be the first entry (index 0)
-        if daily_stats[0]['date'] == today_str:
-            print(f"   ✅ Today ({today_str}) is first in daily stats")
-        else:
-            print(f"   ❌ Today mismatch: expected {today_str}, got {daily_stats[0]['date']}")
-        
-        # Check that dates are consecutive going backwards
-        dates_correct = True
-        for i in range(1, len(daily_stats)):
-            expected_date = (datetime.now().date() - timedelta(days=i)).strftime('%Y-%m-%d')
-            if daily_stats[i]['date'] != expected_date:
-                dates_correct = False
-                print(f"   ❌ Date mismatch at index {i}: expected {expected_date}, got {daily_stats[i]['date']}")
-                break
-        
-        if dates_correct:
-            print(f"   ✅ All daily dates are consecutive and correct")
-        
-        # Test Case 4: Verify P&L calculations only for trading periods
-        print("\n4. Testing P&L calculations:")
-        total_pnl = 0
-        trading_days = 0
-        
-        for day in daily_stats:
-            if day['has_trades']:
-                total_pnl += day['pnl']
-                trading_days += 1
-                print(f"   Trading day {day['date_formatted']}: P&L ${day['pnl']:.2f}")
-            else:
-                # Should have zero P&L and zero trades
-                if day['pnl'] == 0 and day['trades'] == 0:
-                    print(f"   No-trade day {day['date_formatted']}: ✅ Correctly shows $0.00")
-                else:
-                    print(f"   No-trade day {day['date_formatted']}: ❌ Should show $0.00")
-        
-        print(f"   Total P&L from trading days: ${total_pnl:.2f}")
-        print(f"   Trading days count: {trading_days}")
-        
-        print("\n✅ Period stats consistency test completed!")
-        print("📊 All periods now show consistent time ranges")
-        print("🎯 Days/weeks/months with no trades are properly displayed")
-        return True
-        
-    except Exception as e:
-        print(f"❌ Test failed: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-    
-    finally:
-        # Clean up temp file
-        if os.path.exists(temp_file):
-            os.unlink(temp_file)
-
-if __name__ == "__main__":
-    success = test_period_stats_consistency()
-    exit(0 if success else 1) 

+ 0 - 91
tests/test_perps_commands.py

@@ -1,91 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script for new perps trading commands
-"""
-
-import sys
-import os
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from hyperliquid_client import HyperliquidClient
-from config import Config
-
-def test_perps_commands():
-    """Test the perps trading functionality."""
-    print("🧪 Testing Perps Trading Commands")
-    print("=" * 50)
-    
-    try:
-        # Test configuration
-        if not Config.validate():
-            print("❌ Configuration validation failed!")
-            return False
-        
-        print(f"✅ Configuration valid")
-        print(f"🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}")
-        print()
-        
-        # Initialize client
-        print("🔧 Initializing Hyperliquid client...")
-        client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
-        
-        if not client.sync_client:
-            print("❌ Failed to initialize client!")
-            return False
-        
-        print("✅ Client initialized successfully")
-        print()
-        
-        # Test token symbol conversion
-        tokens_to_test = ['BTC', 'ETH', 'SOL']
-        
-        for token in tokens_to_test:
-            print(f"📊 Testing {token}...")
-            
-            # Convert to full symbol
-            symbol = f"{token}/USDC:USDC"
-            print(f"  Symbol: {token} → {symbol}")
-            
-            # Test market data fetching
-            market_data = client.get_market_data(symbol)
-            
-            if market_data:
-                price = float(market_data['ticker'].get('last', 0))
-                print(f"  ✅ Current price: ${price:,.2f}")
-                
-                # Test calculation
-                usdc_amount = 100
-                token_amount = usdc_amount / price
-                print(f"  📈 Long ${usdc_amount} USDC = {token_amount:.6f} {token}")
-                print(f"  📉 Short ${usdc_amount} USDC = {token_amount:.6f} {token}")
-                
-            else:
-                print(f"  ❌ Could not fetch price for {token}")
-                
-            print()
-        
-        return True
-        
-    except Exception as e:
-        print(f"💥 Test failed with error: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_perps_commands()
-    
-    if success:
-        print("🎉 Perps commands test PASSED!")
-        print("\n📱 Ready to test on Telegram:")
-        print("  /long BTC 100")
-        print("  /short ETH 50")
-        sys.exit(0)
-    else:
-        print("💥 Perps commands test FAILED!")
-        sys.exit(1) 

+ 0 - 114
tests/test_position_roe.py

@@ -1,114 +0,0 @@
-import unittest
-import asyncio
-from unittest.mock import Mock, patch, AsyncMock
-from datetime import datetime, timezone
-from src.monitoring.simple_position_tracker import SimplePositionTracker
-from src.commands.info.positions import PositionsCommands
-
-class TestPositionROE(unittest.TestCase):
-    def setUp(self):
-        self.trading_engine = Mock()
-        self.notification_manager = Mock()
-        self.position_tracker = SimplePositionTracker(self.trading_engine, self.notification_manager)
-        self.stats = Mock()
-        self.timestamp = datetime.now(timezone.utc)
-
-    async def async_test_position_size_change_roe_calculation(self):
-        """Test ROE calculation during position size changes."""
-        # Mock exchange position data
-        exchange_pos = {
-            'contracts': 0.01,
-            'side': 'long',
-            'entryPrice': 50000.0,
-            'unrealizedPnl': -16.21,
-            'info': {
-                'position': {
-                    'returnOnEquity': '-0.324'  # -32.4%
-                }
-            }
-        }
-        
-        # Mock database position data
-        db_pos = {
-            'trade_lifecycle_id': 'test_lifecycle',
-            'current_position_size': 0.005,
-            'position_side': 'long',
-            'entry_price': 50000.0
-        }
-        
-        # Mock stats manager
-        self.stats.trade_manager.update_trade_market_data = AsyncMock(return_value=True)
-        
-        # Call the method
-        await self.position_tracker._handle_position_size_change('BTC/USD', exchange_pos, db_pos, self.stats, self.timestamp)
-        
-        # Verify the call
-        call_args = self.stats.trade_manager.update_trade_market_data.call_args[1]
-        self.assertEqual(call_args['roe_percentage'], -32.4)  # Should match exchange ROE
-
-    def test_position_size_change_roe_calculation(self):
-        """Test ROE calculation during position size changes."""
-        asyncio.run(self.async_test_position_size_change_roe_calculation())
-
-    async def async_test_position_opened_roe_calculation(self):
-        """Test ROE calculation when position is opened."""
-        # Mock exchange position data
-        exchange_pos = {
-            'contracts': 0.01,
-            'side': 'long',
-            'entryPrice': 50000.0,
-            'unrealizedPnl': -16.21,
-            'info': {
-                'position': {
-                    'returnOnEquity': '-0.324'  # -32.4%
-                }
-            }
-        }
-        
-        # Mock stats manager
-        self.stats.create_trade_lifecycle = AsyncMock(return_value='test_lifecycle')
-        self.stats.update_trade_position_opened = AsyncMock(return_value=True)
-        
-        # Call the method
-        await self.position_tracker._handle_position_opened('BTC/USD', exchange_pos, self.stats, self.timestamp)
-        
-        # Verify the call
-        call_args = self.stats.update_trade_position_opened.call_args[1]
-        self.assertEqual(call_args['roe_percentage'], -32.4)  # Should match exchange ROE
-
-    def test_position_opened_roe_calculation(self):
-        """Test ROE calculation when position is opened."""
-        asyncio.run(self.async_test_position_opened_roe_calculation())
-
-    async def async_test_positions_command_roe_display(self):
-        """Test ROE display in positions command."""
-        # Mock open positions data
-        open_positions = [{
-            'symbol': 'BTC/USD',
-            'position_side': 'long',
-            'current_position_size': 0.01,
-            'entry_price': 50000.0,
-            'duration': '1h',
-            'unrealized_pnl': -16.21,
-            'roe_percentage': -32.4,  # ROE from database
-            'position_value': 500.0,
-            'margin_used': 50.0,
-            'leverage': 10.0,
-            'liquidation_price': 45000.0
-        }]
-        
-        # Mock stats manager
-        self.stats.get_open_positions = AsyncMock(return_value=open_positions)
-        
-        # Call the positions command
-        result = await positions_command(None, self.stats)
-        
-        # Verify the output
-        self.assertIn('-32.4%', result)  # Should display ROE correctly
-
-    def test_positions_command_roe_display(self):
-        """Test ROE display in positions command."""
-        asyncio.run(self.async_test_positions_command_roe_display())
-
-if __name__ == '__main__':
-    unittest.main() 

+ 0 - 111
tests/test_positions_display.py

@@ -1,111 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to demonstrate the enhanced positions display with P&L percentages.
-"""
-
-def demo_positions_display():
-    """Demo what the enhanced positions message will look like."""
-    print("📈 Enhanced Positions Display Demo")
-    print("=" * 50)
-    
-    # Mock position data
-    positions = [
-        {
-            'symbol': 'BTC/USDC:USDC',
-            'contracts': 0.05,  # Long position
-            'entryPx': 45000,
-            'unrealizedPnl': 1250
-        },
-        {
-            'symbol': 'ETH/USDC:USDC', 
-            'contracts': -2.0,  # Short position
-            'entryPx': 3000,
-            'unrealizedPnl': -150
-        },
-        {
-            'symbol': 'SOL/USDC:USDC',
-            'contracts': 10.0,  # Long position
-            'entryPx': 100,
-            'unrealizedPnl': 75
-        }
-    ]
-    
-    print("📈 <b>Open Positions</b>\n")
-    
-    total_unrealized = 0
-    total_position_value = 0
-    
-    for position in positions:
-        symbol = position['symbol']
-        contracts = float(position['contracts'])
-        unrealized_pnl = float(position['unrealizedPnl'])
-        entry_price = float(position['entryPx'])
-        
-        # Calculate position value and P&L percentage
-        position_value = abs(contracts) * entry_price
-        pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
-        
-        pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-        
-        # Extract token name for cleaner display
-        token = symbol.split('/')[0] if '/' in symbol else symbol
-        position_type = "LONG" if contracts > 0 else "SHORT"
-        
-        print(f"📊 {token} ({position_type})")
-        print(f"   📏 Size: {abs(contracts):.6f} {token}")
-        print(f"   💰 Entry: ${entry_price:,.2f}")
-        print(f"   💵 Value: ${position_value:,.2f}")
-        print(f"   {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)")
-        print()
-        
-        total_unrealized += unrealized_pnl
-        total_position_value += position_value
-    
-    # Calculate overall P&L percentage
-    total_pnl_percentage = (total_unrealized / total_position_value * 100) if total_position_value > 0 else 0
-    total_pnl_emoji = "🟢" if total_unrealized >= 0 else "🔴"
-    
-    print(f"💼 Total Portfolio:")
-    print(f"   💵 Total Value: ${total_position_value:,.2f}")
-    print(f"   {total_pnl_emoji} Total P&L: ${total_unrealized:,.2f} ({total_pnl_percentage:+.2f}%)")
-
-def demo_comparison():
-    """Show before vs after comparison."""
-    print("\n📊 Before vs After Comparison")
-    print("=" * 50)
-    
-    print("BEFORE (old format):")
-    print("📊 BTC/USDC:USDC")
-    print("   📏 Size: 0.05 contracts")
-    print("   💰 Entry: $45,000.00")
-    print("   🟢 PnL: $1,250.00")
-    print()
-    print("💼 Total Unrealized P&L: $1,175.00")
-    
-    print("\nAFTER (enhanced format):")
-    print("📊 BTC (LONG)")
-    print("   📏 Size: 0.050000 BTC")
-    print("   💰 Entry: $45,000.00")
-    print("   💵 Value: $2,250.00")
-    print("   🟢 P&L: $1,250.00 (+55.56%)")
-    print()
-    print("💼 Total Portfolio:")
-    print("   💵 Total Value: $8,250.00")
-    print("   🟢 Total P&L: $1,175.00 (+14.24%)")
-
-if __name__ == "__main__":
-    print("🚀 Enhanced Positions Display Test")
-    print("=" * 60)
-    
-    demo_positions_display()
-    demo_comparison()
-    
-    print("\n" + "=" * 60)
-    print("✅ Enhanced features:")
-    print("• Shows position direction (LONG/SHORT)")
-    print("• Displays token names instead of full symbols")
-    print("• Shows position size in token units")
-    print("• Adds position value for context")
-    print("• P&L shown as both $ and % for easy assessment")
-    print("• Total portfolio value and P&L percentage")
-    print("• Clean, mobile-friendly formatting") 

+ 0 - 150
tests/test_risk_management.py

@@ -1,150 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script for risk management commands (/sl and /tp)
-"""
-
-import sys
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from hyperliquid_client import HyperliquidClient
-from config import Config
-
-def test_risk_management():
-    """Test the risk management functionality."""
-    print("🧪 Testing Risk Management Commands")
-    print("=" * 50)
-    
-    try:
-        # Test configuration
-        if not Config.validate():
-            print("❌ Configuration validation failed!")
-            return False
-        
-        print(f"✅ Configuration valid")
-        print(f"🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}")
-        print()
-        
-        # Initialize client
-        print("🔧 Initializing Hyperliquid client...")
-        client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
-        
-        if not client.sync_client:
-            print("❌ Failed to initialize client!")
-            return False
-        
-        print("✅ Client initialized successfully")
-        print()
-        
-        # Test position fetching (required for SL/TP commands)
-        print("📊 Testing position fetching...")
-        positions = client.get_positions()
-        
-        if positions is not None:
-            print(f"✅ Successfully fetched positions: {len(positions)} total")
-            
-            # Show open positions for risk management testing
-            open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
-            
-            if open_positions:
-                print(f"📈 Found {len(open_positions)} open positions for risk management:")
-                for pos in open_positions:
-                    symbol = pos.get('symbol', 'Unknown')
-                    contracts = float(pos.get('contracts', 0))
-                    entry_price = float(pos.get('entryPx', 0))
-                    unrealized_pnl = float(pos.get('unrealizedPnl', 0))
-                    
-                    position_type = "LONG" if contracts > 0 else "SHORT"
-                    
-                    print(f"  • {symbol}: {position_type} {abs(contracts)} @ ${entry_price:.2f} (P&L: ${unrealized_pnl:.2f})")
-                    
-                    # Test token extraction for SL/TP commands
-                    if '/' in symbol:
-                        token = symbol.split('/')[0]
-                        print(f"    → Token: {token}")
-                        
-                        # Test stop loss scenarios
-                        if contracts > 0:  # Long position
-                            sl_price = entry_price * 0.95  # 5% below entry
-                            tp_price = entry_price * 1.1   # 10% above entry
-                            print(f"    → Stop Loss (Long): /sl {token} {sl_price:.0f}")
-                            print(f"    → Take Profit (Long): /tp {token} {tp_price:.0f}")
-                        else:  # Short position
-                            sl_price = entry_price * 1.05  # 5% above entry
-                            tp_price = entry_price * 0.9   # 10% below entry
-                            print(f"    → Stop Loss (Short): /sl {token} {sl_price:.0f}")
-                            print(f"    → Take Profit (Short): /tp {token} {tp_price:.0f}")
-                        
-                print()
-            else:
-                print("📭 No open positions found")
-                print("💡 To test risk management commands, first open a position:")
-                print("  /long BTC 10     # Open long position")
-                print("  /sl BTC 42000    # Set stop loss")
-                print("  /tp BTC 48000    # Set take profit")
-                print()
-        else:
-            print("❌ Could not fetch positions")
-            return False
-        
-        # Test stop loss and take profit order placement methods
-        print("🛑 Testing stop loss order methods...")
-        test_tokens = ['BTC', 'ETH']
-        
-        for token in test_tokens:
-            symbol = f"{token}/USDC:USDC"
-            
-            # Get current price for realistic SL/TP prices
-            market_data = client.get_market_data(symbol)
-            if market_data:
-                current_price = float(market_data['ticker'].get('last', 0))
-                print(f"  📊 {token}: ${current_price:,.2f}")
-                
-                # Test stop loss order method (without actually placing)
-                sl_price = current_price * 0.95
-                tp_price = current_price * 1.05
-                
-                print(f"    🛑 Stop loss method ready: place_stop_loss_order({symbol}, 'sell', 0.001, {sl_price:.0f})")
-                print(f"    🎯 Take profit method ready: place_take_profit_order({symbol}, 'sell', 0.001, {tp_price:.0f})")
-            else:
-                print(f"  ❌ Could not get price for {token}")
-        
-        print()
-        print("🎉 Risk management tests completed!")
-        print()
-        print("📝 Risk Management Summary:")
-        print("  • ✅ Position fetching: Working")
-        print("  • ✅ Price validation: Working")
-        print("  • ✅ Direction detection: Working")
-        print("  • ✅ Order methods: Ready")
-        print()
-        print("🚀 Ready to test risk management commands:")
-        print("  /sl BTC 42000    # Stop loss for Bitcoin")
-        print("  /tp BTC 48000    # Take profit for Bitcoin")
-        print("  /sl ETH 3200     # Stop loss for Ethereum")
-        print("  /tp ETH 3800     # Take profit for Ethereum")
-        
-        return True
-        
-    except Exception as e:
-        print(f"💥 Test failed with error: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_risk_management()
-    
-    if success:
-        print("\n🎉 Risk management test PASSED!")
-        print("\n📱 Ready to test on Telegram:")
-        print("  /sl BTC 42000")
-        print("  /tp BTC 48000")
-        sys.exit(0)
-    else:
-        print("\n💥 Risk management test FAILED!")
-        sys.exit(1) 

+ 0 - 111
tests/test_stats_fix.py

@@ -1,111 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to verify the stats command fix
-"""
-
-import sys
-from pathlib import Path
-
-# Add the project root and src directory to the path
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-sys.path.insert(0, str(project_root / 'src'))
-
-from src.stats import TradingStats
-
-def test_stats_fix():
-    """Test that stats work with no trades and with trades."""
-    print("🧪 Testing Stats Fix")
-    print("=" * 40)
-    
-    try:
-        # Test 1: New stats instance (no trades)
-        print("📊 Test 1: Empty stats (no trades)")
-        stats = TradingStats(stats_file="test_stats.json")
-        
-        # Try to format stats message
-        message = stats.format_stats_message(1000.0)
-        print("✅ Empty stats message generated successfully")
-        print(f"📝 Message length: {len(message)} characters")
-        
-        # Test 2: Performance stats with no trades
-        print("\n📊 Test 2: Performance stats with no trades")
-        perf_stats = stats.get_performance_stats()
-        
-        # Check that expectancy key exists
-        if 'expectancy' in perf_stats:
-            print(f"✅ Expectancy key exists: {perf_stats['expectancy']}")
-        else:
-            print("❌ Expectancy key missing!")
-            return False
-        
-        # Check all required keys
-        required_keys = [
-            'win_rate', 'profit_factor', 'avg_win', 'avg_loss',
-            'largest_win', 'largest_loss', 'consecutive_wins',
-            'consecutive_losses', 'total_wins', 'total_losses', 'expectancy'
-        ]
-        
-        missing_keys = [key for key in required_keys if key not in perf_stats]
-        if missing_keys:
-            print(f"❌ Missing keys: {missing_keys}")
-            return False
-        else:
-            print("✅ All required performance keys present")
-        
-        # Test 3: Add some sample trades
-        print("\n📊 Test 3: Stats with sample trades")
-        stats.record_trade("BTC/USDC:USDC", "buy", 0.001, 45000.0, "test1")
-        stats.record_trade("BTC/USDC:USDC", "sell", 0.001, 46000.0, "test2")
-        
-        # Try to format stats message with trades
-        message_with_trades = stats.format_stats_message(1010.0)
-        print("✅ Stats message with trades generated successfully")
-        
-        # Test performance stats with trades
-        perf_stats_with_trades = stats.get_performance_stats()
-        print(f"✅ Win rate: {perf_stats_with_trades['win_rate']:.1f}%")
-        print(f"✅ Expectancy: ${perf_stats_with_trades['expectancy']:.2f}")
-        
-        # Test 4: Error handling
-        print("\n📊 Test 4: Error handling")
-        try:
-            # This should not fail due to the safe .get() access
-            comprehensive_stats = stats.get_comprehensive_stats(1010.0)
-            print("✅ Comprehensive stats generated successfully")
-        except Exception as e:
-            print(f"❌ Comprehensive stats failed: {e}")
-            return False
-        
-        print("\n🎉 All stats tests passed!")
-        print("\n📝 Sample stats message:")
-        print("-" * 40)
-        print(message[:200] + "..." if len(message) > 200 else message)
-        print("-" * 40)
-        
-        # Clean up test file
-        import os
-        try:
-            os.remove("test_stats.json")
-            print("\n🧹 Test file cleaned up")
-        except:
-            pass
-        
-        return True
-        
-    except Exception as e:
-        print(f"💥 Test failed with error: {e}")
-        import traceback
-        traceback.print_exc()
-        return False
-
-if __name__ == "__main__":
-    success = test_stats_fix()
-    
-    if success:
-        print("\n🎉 Stats fix test PASSED!")
-        print("\n✅ The /stats command should now work properly")
-        sys.exit(0)
-    else:
-        print("\n💥 Stats fix test FAILED!")
-        sys.exit(1) 

+ 0 - 128
tests/test_stop_loss_config.py

@@ -1,128 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to demonstrate the stop loss configuration and functionality.
-This script shows how the automatic stop loss system would work.
-"""
-
-import sys
-import os
-sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
-
-from config import Config
-
-def test_stop_loss_config():
-    """Test and display stop loss configuration."""
-    print("🛡️ Stop Loss Configuration Test")
-    print("=" * 50)
-    
-    # Display current configuration
-    print(f"📊 Risk Management Enabled: {Config.RISK_MANAGEMENT_ENABLED}")
-    print(f"🛑 Stop Loss Percentage: {Config.STOP_LOSS_PERCENTAGE}%")
-    print(f"⏰ Monitoring Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds")
-    print()
-    
-    # Simulate position scenarios
-    scenarios = [
-        {
-            'name': 'Long BTC Position',
-            'position_type': 'long',
-            'entry_price': 45000,
-            'current_prices': [44000, 42000, 40500, 39000],
-            'position_size': 0.1
-        },
-        {
-            'name': 'Short ETH Position', 
-            'position_type': 'short',
-            'entry_price': 3000,
-            'current_prices': [3100, 3200, 3300, 3400],
-            'position_size': 2.0
-        }
-    ]
-    
-    for scenario in scenarios:
-        print(f"📋 Scenario: {scenario['name']}")
-        print(f"   Direction: {scenario['position_type'].upper()}")
-        print(f"   Entry Price: ${scenario['entry_price']:,.2f}")
-        print(f"   Position Size: {scenario['position_size']}")
-        print()
-        
-        for current_price in scenario['current_prices']:
-            # Calculate P&L percentage
-            if scenario['position_type'] == 'long':
-                pnl_percent = ((current_price - scenario['entry_price']) / scenario['entry_price']) * 100
-            else:  # short
-                pnl_percent = ((scenario['entry_price'] - current_price) / scenario['entry_price']) * 100
-            
-            # Check if stop loss would trigger
-            would_trigger = pnl_percent <= -Config.STOP_LOSS_PERCENTAGE
-            
-            # Calculate loss value
-            loss_value = scenario['position_size'] * abs(current_price - scenario['entry_price'])
-            
-            status = "🛑 STOP LOSS TRIGGERED!" if would_trigger else "✅ Safe"
-            
-            print(f"   Current Price: ${current_price:,.2f} | P&L: {pnl_percent:+.2f}% | Loss: ${loss_value:,.2f} | {status}")
-        
-        print()
-
-def test_stop_loss_thresholds():
-    """Test different stop loss threshold scenarios."""
-    print("🔧 Stop Loss Threshold Testing")
-    print("=" * 50)
-    
-    thresholds = [5.0, 10.0, 15.0, 20.0]
-    entry_price = 50000  # BTC example
-    
-    print(f"Entry Price: ${entry_price:,.2f}")
-    print()
-    
-    for threshold in thresholds:
-        # Calculate trigger prices
-        long_trigger_price = entry_price * (1 - threshold/100)
-        short_trigger_price = entry_price * (1 + threshold/100)
-        
-        print(f"Stop Loss Threshold: {threshold}%")
-        print(f"   Long Position Trigger: ${long_trigger_price:,.2f} (loss of ${entry_price - long_trigger_price:,.2f})")
-        print(f"   Short Position Trigger: ${short_trigger_price:,.2f} (loss of ${short_trigger_price - entry_price:,.2f})")
-        print()
-
-def test_monitoring_frequency():
-    """Test monitoring frequency scenarios."""
-    print("⏰ Monitoring Frequency Analysis")
-    print("=" * 50)
-    
-    frequencies = [10, 30, 60, 120, 300]  # seconds
-    
-    print("Different monitoring intervals and their implications:")
-    print()
-    
-    for freq in frequencies:
-        checks_per_minute = 60 / freq
-        checks_per_hour = 3600 / freq
-        
-        print(f"Interval: {freq} seconds")
-        print(f"   Checks per minute: {checks_per_minute:.1f}")
-        print(f"   Checks per hour: {checks_per_hour:.0f}")
-        print(f"   Responsiveness: {'High' if freq <= 30 else 'Medium' if freq <= 120 else 'Low'}")
-        print(f"   API Usage: {'High' if freq <= 10 else 'Medium' if freq <= 60 else 'Low'}")
-        print()
-
-if __name__ == "__main__":
-    print("🚀 Stop Loss System Configuration Test")
-    print("=" * 60)
-    print()
-    
-    # Test current configuration
-    test_stop_loss_config()
-    
-    print("\n" + "=" * 60)
-    test_stop_loss_thresholds()
-    
-    print("\n" + "=" * 60)
-    test_monitoring_frequency()
-    
-    print("\n" + "=" * 60)
-    print("✅ Stop Loss System Ready!")
-    print(f"📊 Current Settings: {Config.STOP_LOSS_PERCENTAGE}% stop loss, {Config.BOT_HEARTBEAT_SECONDS}s monitoring")
-    print("🛡️ Automatic position protection is enabled")
-    print("📱 You'll receive Telegram notifications for all stop loss events") 

+ 0 - 49
tests/test_update_and_check_mark_price.py

@@ -1,49 +0,0 @@
-import sys
-import os
-from datetime import datetime
-
-# Adjust path if needed to import TradingStats and TradingEngine
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-
-from src.stats.trading_stats import TradingStats
-from src.trading.trading_engine import TradingEngine
-
-def update_db_with_latest_mark_prices():
-    engine = TradingEngine()
-    stats = TradingStats()
-    exchange_positions = engine.get_positions() or []
-    print(f"Fetched {len(exchange_positions)} positions from exchange.")
-    updated = 0
-    for pos in exchange_positions:
-        symbol = pos.get('symbol')
-        mark_price = pos.get('markPrice') or pos.get('markPx')
-        if symbol and mark_price is not None:
-            # Find the open trade in DB
-            open_positions = stats.get_open_positions()
-            for db_pos in open_positions:
-                if db_pos.get('symbol') == symbol:
-                    lifecycle_id = db_pos.get('trade_lifecycle_id')
-                    try:
-                        stats.trade_manager.update_trade_market_data(
-                            lifecycle_id,
-                            mark_price=float(mark_price)
-                        )
-                        updated += 1
-                        print(f"Updated {symbol} (Lifecycle: {lifecycle_id}) with mark_price={mark_price}")
-                    except Exception as e:
-                        print(f"Failed to update {symbol}: {e}")
-    print(f"Updated {updated} positions in DB with latest mark prices.")
-
-def print_open_positions_mark_prices():
-    stats = TradingStats()
-    open_positions = stats.get_open_positions()
-    print(f"\nDB now has {len(open_positions)} open positions:")
-    for pos in open_positions:
-        symbol = pos.get('symbol', 'N/A')
-        entry_price = pos.get('entry_price', 'N/A')
-        mark_price = pos.get('mark_price', 'N/A')
-        print(f"Symbol: {symbol}, Entry Price: {entry_price}, Mark Price: {mark_price}")
-
-if __name__ == "__main__":
-    update_db_with_latest_mark_prices()
-    print_open_positions_mark_prices() 

+ 94 - 59
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.4.231"
+BOT_VERSION = "2.4.232"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))
@@ -69,78 +69,113 @@ class BotManager:
     
     def print_banner(self):
         """Print startup banner."""
+        # Get network and database status
+        network_status = 'Testnet' if Config.HYPERLIQUID_TESTNET else '🚨 MAINNET 🚨'
+        db_path = "data/trading_stats.sqlite"
+        db_status = "✅ Found" if os.path.exists(db_path) else "❌ Not Found"
+
         banner = f"""
-╔══════════════════════════════════════════════════════════════╗
-║                    📱 HYPERLIQUID TRADING BOT                ║
-║                       Version {BOT_VERSION}                       ║
-╠══════════════════════════════════════════════════════════════╣
-║                                                              ║
-║  🤖 Manual phone control via Telegram                       ║
-║  📊 Comprehensive trading statistics                        ║
-║  🛡️ Systemd managed service                                 ║
-║  💾 Persistent data between restarts                        ║
-║  📱 Professional mobile interface                            ║
-║                                                              ║
-╚══════════════════════════════════════════════════════════════╝
+\033[1;35m
+  +-------------------------------------------------------------+
+  |                                                             |
+  |      📱    H Y P E R L I Q U I D   T R A D I N G   B O T    📱    |
+  |                                                             |
+  |                      Version {BOT_VERSION}                      |
+  |                                                             |
+  +-------------------------------------------------------------+
+\033[0m
+  \033[1;34m▶  System Status\033[0m
+  +--------------------+----------------------------------------+
+  | \033[1;36mStart Time\033[0m         | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}         |
+  | \033[1;36mWorking Dir\033[0m        | {os.getcwd()}                                |
+  | \033[1;36mNetwork\033[0m            | {network_status}                     |
+  | \033[1;36mDatabase\033[0m           | {db_status} at {db_path}          |
+  +--------------------+----------------------------------------+
 
-🚀 Starting at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
-📁 Working directory: {os.getcwd()}
-🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else '🚨 MAINNET 🚨'}
-        """
+  \033[1;34m▶  Key Features\033[0m
+  +-------------------------------------------------------------+
+  |                                                             |
+  |    🤖   Control your trades from your phone via Telegram     |
+  |    📊   Get comprehensive, persistent trading statistics   |
+  |    🛡️   Managed as a systemd service for reliability       |
+  |    💾   Data is persistent between restarts                |
+  |    📱   Enjoy a professional, mobile-friendly interface    |
+  |                                                             |
+  +-------------------------------------------------------------+
+"""
         print(banner)
     
     def validate_configuration(self):
         """Validate bot configuration."""
         self.logger.info("🔍 Validating configuration...")
         
-        missing_config = []
-        
-        if not hasattr(Config, 'HYPERLIQUID_WALLET_ADDRESS') or not Config.HYPERLIQUID_WALLET_ADDRESS:
-            missing_config.append("HYPERLIQUID_WALLET_ADDRESS")
+        # Define required configuration attributes
+        required_configs = {
+            "HYPERLIQUID_WALLET_ADDRESS": "Your Hyperliquid wallet address",
+            "TELEGRAM_BOT_TOKEN": "Your Telegram bot token",
+            "TELEGRAM_CHAT_ID": "Your Telegram chat ID",
+            "TELEGRAM_ENABLED": "Must be set to true to enable Telegram features"
+        }
         
-        if not hasattr(Config, 'TELEGRAM_BOT_TOKEN') or not Config.TELEGRAM_BOT_TOKEN:
-            missing_config.append("TELEGRAM_BOT_TOKEN")
+        missing_items = []
         
-        if not hasattr(Config, 'TELEGRAM_CHAT_ID') or not Config.TELEGRAM_CHAT_ID:
-            missing_config.append("TELEGRAM_CHAT_ID")
+        # Check for missing attributes in the Config class
+        for attr, description in required_configs.items():
+            if not getattr(Config, attr, None):
+                missing_items.append(f"- {attr}: {description}")
         
-        if not hasattr(Config, 'TELEGRAM_ENABLED') or not Config.TELEGRAM_ENABLED:
-            missing_config.append("TELEGRAM_ENABLED (must be true)")
-        
-        if missing_config:
-            error_msg = f"❌ Missing configuration: {', '.join(missing_config)}"
-            self.logger.error(error_msg)
-            print(f"\n{error_msg}")
-            print("\n💡 Setup steps:")
-            print("1. Copy config: cp config/env.example .env")
-            print("2. Get Telegram setup: python utils/get_telegram_chat_id.py")
-            print("3. Edit .env with your details")
-            print("4. See: SETUP_GUIDE.md for detailed instructions")
+        # If there are missing items, log and print an error
+        if missing_items:
+            error_message = "❌ Missing or invalid configuration:"
+            detailed_errors = "\n".join(missing_items)
+            
+            self.logger.error(f"{error_message}\n{detailed_errors}")
+            
+            # Print a user-friendly guide for fixing the configuration
+            print(f"\n{error_message}")
+            print(detailed_errors)
+            print("\n💡 To fix this, please follow these steps:")
+            print("   1. Copy the example config: cp config/env.example .env")
+            print("   2. For Telegram setup, run: python utils/get_telegram_chat_id.py")
+            print("   3. Edit the .env file with your credentials.")
+            print("   4. For more help, see the SETUP_GUIDE.md.")
+            
             return False
-        
+            
         self.logger.info("✅ Configuration validation passed")
         return True
     
     def check_stats_persistence(self):
         """Check and report on statistics persistence."""
-        # Check if we have persistent statistics
-        self.logger.info("📊 Checking statistics persistence...")
-        sqlite_file = "data/trading_stats.sqlite"
-        if os.path.exists(sqlite_file):
-            try:
-                # Quick check to see if database has data
-                stats = TradingStats()
-                basic_stats = stats.get_basic_stats()
-                total_trades = basic_stats.get('total_trades', 0)
-                start_date = basic_stats.get('start_date', 'unknown')
-                self.logger.info(f"📊 Existing SQLite database found - {total_trades} trades since {start_date}")
-                return True
-            except Exception as e:
-                self.logger.warning(f"⚠️ SQLite database {sqlite_file} exists but couldn't load: {e}")
-                return False
-        else:
-            self.logger.info(f"📊 No existing SQLite database at {sqlite_file} - will create new tracking from launch")
-            return False
+        self.logger.info("📊 Checking for persistent statistics...")
+        
+        db_file = "data/trading_stats.sqlite"
+        
+        if not os.path.exists(db_file):
+            self.logger.warning(f"⚠️ No database found at {db_file}. New statistics will be tracked from scratch.")
+            print(f"📊 No persistent database found. A new one will be created at {db_file}.")
+            return
+            
+        try:
+            # Attempt to connect and retrieve basic stats
+            stats = TradingStats()
+            basic_stats = stats.get_basic_stats()
+            
+            total_trades = basic_stats.get('total_trades', 0)
+            start_date = basic_stats.get('start_date', 'N/A')
+            
+            # Log and print success message
+            success_message = f"✅ Found existing database with {total_trades} trades since {start_date}."
+            self.logger.info(success_message)
+            print(success_message)
+            
+        except Exception as e:
+            # Log and print a detailed error message if the database is corrupt or inaccessible
+            error_message = f"❌ Error loading stats from {db_file}: {e}"
+            self.logger.error(error_message, exc_info=True)
+            print(error_message)
+            print("   - The database file might be corrupted.")
+            print("   - Consider backing up and deleting the file to start fresh.")
     
     async def run_bot(self):
         """Run the main bot."""
@@ -151,9 +186,6 @@ class BotManager:
             # Set version for bot to use in messages
             self.bot.version = BOT_VERSION
             
-            self.logger.info("📊 Checking statistics persistence...")
-            self.check_stats_persistence()
-            
             self.logger.info("🚀 Starting Telegram bot...")
             
             # Run the bot
@@ -173,6 +205,9 @@ class BotManager:
     async def start(self):
         """Main entry point for BotManager."""
         try:
+            # Check stats persistence before printing the banner
+            self.check_stats_persistence()
+            
             self.print_banner()
             
             if not self.validate_configuration():