Procházet zdrojové kódy

Add price alarm system and logging enhancements - Introduce a comprehensive price alarm manager with functionality for creating, triggering, and managing alarms. Implement logging configuration with rotation and cleanup support, and integrate alarm checks into the Telegram bot for real-time notifications. Add tests for alarm management and ensure persistence across sessions.

Carles Sentis před 5 dny
rodič
revize
01b6eed4d8

+ 28 - 1
config/env.example

@@ -38,8 +38,35 @@ TELEGRAM_CHAT_ID=your_chat_id_here
 # Enable/disable Telegram integration
 # Enable/disable Telegram integration
 TELEGRAM_ENABLED=true
 TELEGRAM_ENABLED=true
 
 
+# ========================================
+# Bot Monitoring Configuration
+# ========================================
+# Heartbeat interval for monitoring orders, positions, and price alarms (in seconds)
+# Default: 30 seconds - good balance between responsiveness and API usage
+# Minimum recommended: 10 seconds (to avoid rate limiting)
+# Maximum recommended: 300 seconds (5 minutes)
+BOT_HEARTBEAT_SECONDS=30
+
 # ========================================
 # ========================================
 # Logging
 # Logging
 # ========================================
 # ========================================
 # Options: DEBUG, INFO, WARNING, ERROR
 # Options: DEBUG, INFO, WARNING, ERROR
-LOG_LEVEL=INFO 
+LOG_LEVEL=INFO
+
+# Enable/disable logging to file (true/false)
+LOG_TO_FILE=true
+
+# Log file path (relative to project root)
+LOG_FILE_PATH=logs/trading_bot.log
+
+# Log rotation type: 'size' (rotate when file gets too big) or 'time' (rotate daily/hourly)
+LOG_ROTATION_TYPE=size
+
+# For size-based rotation: max file size in MB before rotation
+LOG_MAX_SIZE_MB=10
+
+# For time-based rotation: when to rotate ('midnight', 'H' for hourly, 'D' for daily)
+LOG_ROTATION_INTERVAL=midnight
+
+# Number of backup log files to keep (older files are automatically deleted)
+LOG_BACKUP_COUNT=5 

+ 240 - 0
src/alarm_manager.py

@@ -0,0 +1,240 @@
+#!/usr/bin/env python3
+"""
+Price Alarm Manager
+
+Manages price alarms for tokens with persistence and monitoring integration.
+"""
+
+import json
+import os
+import logging
+from datetime import datetime
+from typing import Dict, List, Any, Optional, Tuple
+from config import Config
+
+logger = logging.getLogger(__name__)
+
+class AlarmManager:
+    """Manages price alarms with persistence and monitoring."""
+    
+    def __init__(self, alarms_file: str = "price_alarms.json"):
+        """Initialize the alarm manager."""
+        self.alarms_file = alarms_file
+        self.data = self._load_alarms()
+        
+        # Initialize if first run
+        if not self.data:
+            self._initialize_alarms()
+    
+    def _load_alarms(self) -> Dict[str, Any]:
+        """Load alarms from file."""
+        try:
+            if os.path.exists(self.alarms_file):
+                with open(self.alarms_file, 'r') as f:
+                    return json.load(f)
+        except Exception as e:
+            logger.error(f"Error loading alarms: {e}")
+        return {}
+    
+    def _save_alarms(self):
+        """Save alarms to file."""
+        try:
+            with open(self.alarms_file, 'w') as f:
+                json.dump(self.data, f, indent=2, default=str)
+        except Exception as e:
+            logger.error(f"Error saving alarms: {e}")
+    
+    def _initialize_alarms(self):
+        """Initialize alarms structure."""
+        self.data = {
+            'next_id': 1,
+            'alarms': [],
+            'triggered_alarms': [],
+            'last_update': datetime.now().isoformat()
+        }
+        self._save_alarms()
+    
+    def create_alarm(self, token: str, target_price: float, current_price: float) -> Dict[str, Any]:
+        """Create a new price alarm."""
+        # Determine direction based on current vs target price
+        direction = "above" if target_price > current_price else "below"
+        
+        alarm = {
+            'id': self.data['next_id'],
+            'token': token.upper(),
+            'target_price': target_price,
+            'current_price_when_set': current_price,
+            'direction': direction,
+            'status': 'active',
+            'created_at': datetime.now().isoformat(),
+            'triggered_at': None
+        }
+        
+        self.data['alarms'].append(alarm)
+        self.data['next_id'] += 1
+        self.data['last_update'] = datetime.now().isoformat()
+        self._save_alarms()
+        
+        logger.info(f"Created alarm {alarm['id']}: {token} @ ${target_price} ({direction})")
+        return alarm
+    
+    def get_alarm_by_id(self, alarm_id: int) -> Optional[Dict[str, Any]]:
+        """Get alarm by ID."""
+        for alarm in self.data['alarms']:
+            if alarm['id'] == alarm_id:
+                return alarm
+        return None
+    
+    def get_alarms_by_token(self, token: str) -> List[Dict[str, Any]]:
+        """Get all active alarms for a specific token."""
+        token = token.upper()
+        return [alarm for alarm in self.data['alarms'] if alarm['token'] == token and alarm['status'] == 'active']
+    
+    def get_all_active_alarms(self) -> List[Dict[str, Any]]:
+        """Get all active alarms."""
+        return [alarm for alarm in self.data['alarms'] if alarm['status'] == 'active']
+    
+    def get_recent_triggered_alarms(self, limit: int = 10) -> List[Dict[str, Any]]:
+        """Get recently triggered alarms."""
+        return self.data['triggered_alarms'][-limit:] if self.data['triggered_alarms'] else []
+    
+    def remove_alarm(self, alarm_id: int) -> bool:
+        """Remove an alarm by ID."""
+        for i, alarm in enumerate(self.data['alarms']):
+            if alarm['id'] == alarm_id:
+                removed_alarm = self.data['alarms'].pop(i)
+                self.data['last_update'] = datetime.now().isoformat()
+                self._save_alarms()
+                logger.info(f"Removed alarm {alarm_id}: {removed_alarm['token']} @ ${removed_alarm['target_price']}")
+                return True
+        return False
+    
+    def trigger_alarm(self, alarm_id: int, triggered_price: float) -> Optional[Dict[str, Any]]:
+        """Mark an alarm as triggered."""
+        for alarm in self.data['alarms']:
+            if alarm['id'] == alarm_id and alarm['status'] == 'active':
+                alarm['status'] = 'triggered'
+                alarm['triggered_at'] = datetime.now().isoformat()
+                alarm['triggered_price'] = triggered_price
+                
+                # Move to triggered alarms list
+                self.data['triggered_alarms'].append(alarm.copy())
+                
+                # Keep only last 50 triggered alarms
+                if len(self.data['triggered_alarms']) > 50:
+                    self.data['triggered_alarms'] = self.data['triggered_alarms'][-50:]
+                
+                self.data['last_update'] = datetime.now().isoformat()
+                self._save_alarms()
+                
+                logger.info(f"Triggered alarm {alarm_id}: {alarm['token']} @ ${triggered_price}")
+                return alarm
+        return None
+    
+    def check_alarms(self, price_data: Dict[str, float]) -> List[Dict[str, Any]]:
+        """Check all active alarms against current prices."""
+        triggered_alarms = []
+        
+        for alarm in self.get_all_active_alarms():
+            token = alarm['token']
+            target_price = alarm['target_price']
+            direction = alarm['direction']
+            
+            # Get current price for this token
+            current_price = price_data.get(token)
+            if current_price is None:
+                continue
+            
+            # Check if alarm should trigger
+            should_trigger = False
+            
+            if direction == "above" and current_price >= target_price:
+                should_trigger = True
+            elif direction == "below" and current_price <= target_price:
+                should_trigger = True
+            
+            if should_trigger:
+                triggered_alarm = self.trigger_alarm(alarm['id'], current_price)
+                if triggered_alarm:
+                    triggered_alarms.append(triggered_alarm)
+        
+        return triggered_alarms
+    
+    def format_alarm_list(self, alarms: List[Dict[str, Any]], title: str = "Price Alarms") -> str:
+        """Format a list of alarms for display."""
+        if not alarms:
+            return f"📢 <b>{title}</b>\n\n📭 No alarms found."
+        
+        message = f"📢 <b>{title}</b>\n\n"
+        
+        for alarm in alarms:
+            status_emoji = "✅" if alarm['status'] == 'active' else "🔔"
+            direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
+            
+            message += f"{status_emoji} <b>ID {alarm['id']}</b> - {alarm['token']}\n"
+            message += f"   {direction_emoji} Alert {alarm['direction']} ${alarm['target_price']:,.2f}\n"
+            
+            if alarm['status'] == 'active':
+                created_date = datetime.fromisoformat(alarm['created_at']).strftime('%m/%d %H:%M')
+                message += f"   📅 Created: {created_date}\n"
+            else:
+                triggered_date = datetime.fromisoformat(alarm['triggered_at']).strftime('%m/%d %H:%M')
+                message += f"   🔔 Triggered: {triggered_date} @ ${alarm['triggered_price']:,.2f}\n"
+            
+            message += "\n"
+        
+        if title == "Price Alarms":
+            message += f"💡 <b>Commands:</b>\n"
+            message += f"• <code>/alarm BTC 50000</code> - Set new alarm\n"
+            message += f"• <code>/alarm BTC</code> - Show BTC alarms\n"
+            message += f"• <code>/alarm 3</code> - Remove alarm ID 3"
+        
+        return message.strip()
+    
+    def format_triggered_alarm(self, alarm: Dict[str, Any]) -> str:
+        """Format a triggered alarm notification."""
+        direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
+        price_change = alarm['triggered_price'] - alarm['current_price_when_set']
+        change_percent = (price_change / alarm['current_price_when_set']) * 100
+        
+        message = f"""
+🔔 <b>Price Alert Triggered!</b>
+
+📊 <b>Alarm Details:</b>
+• Token: {alarm['token']}
+• Alert ID: {alarm['id']}
+• Target Price: ${alarm['target_price']:,.2f}
+• Direction: {alarm['direction'].upper()}
+
+💰 <b>Price Info:</b>
+• Current Price: ${alarm['triggered_price']:,.2f}
+• Price When Set: ${alarm['current_price_when_set']:,.2f}
+• Change: ${price_change:,.2f} ({change_percent:+.2f}%)
+
+{direction_emoji} <b>Alert Condition:</b> Price moved {alarm['direction']} ${alarm['target_price']:,.2f}
+
+⏰ <b>Triggered:</b> {datetime.now().strftime('%H:%M:%S')}
+
+✅ <b>Status:</b> Alarm completed and removed from active list
+        """
+        
+        return message.strip()
+    
+    def get_statistics(self) -> Dict[str, Any]:
+        """Get alarm statistics."""
+        active_alarms = self.get_all_active_alarms()
+        triggered_alarms = self.data.get('triggered_alarms', [])
+        
+        # Count by token
+        token_counts = {}
+        for alarm in active_alarms:
+            token = alarm['token']
+            token_counts[token] = token_counts.get(token, 0) + 1
+        
+        return {
+            'total_active': len(active_alarms),
+            'total_triggered': len(triggered_alarms),
+            'tokens_tracked': len(token_counts),
+            'token_breakdown': token_counts,
+            'next_id': self.data['next_id']
+        } 

+ 54 - 14
src/config.py

@@ -1,10 +1,13 @@
 import os
 import os
 from dotenv import load_dotenv
 from dotenv import load_dotenv
 from typing import Optional
 from typing import Optional
+import logging
 
 
 # Load environment variables from .env file
 # Load environment variables from .env file
 load_dotenv()
 load_dotenv()
 
 
+logger = logging.getLogger(__name__)
+
 class Config:
 class Config:
     """Configuration class for the Hyperliquid trading bot."""
     """Configuration class for the Hyperliquid trading bot."""
     
     
@@ -24,31 +27,68 @@ class Config:
     # Telegram Bot Configuration
     # Telegram Bot Configuration
     TELEGRAM_BOT_TOKEN: Optional[str] = os.getenv('TELEGRAM_BOT_TOKEN')
     TELEGRAM_BOT_TOKEN: Optional[str] = os.getenv('TELEGRAM_BOT_TOKEN')
     TELEGRAM_CHAT_ID: Optional[str] = os.getenv('TELEGRAM_CHAT_ID')
     TELEGRAM_CHAT_ID: Optional[str] = os.getenv('TELEGRAM_CHAT_ID')
-    TELEGRAM_ENABLED: bool = os.getenv('TELEGRAM_ENABLED', 'false').lower() == 'true'
+    TELEGRAM_ENABLED: bool = os.getenv('TELEGRAM_ENABLED', 'true').lower() == 'true'
+    
+    # Bot monitoring configuration
+    BOT_HEARTBEAT_SECONDS = int(os.getenv('BOT_HEARTBEAT_SECONDS', '30'))
     
     
     # Logging
     # Logging
     LOG_LEVEL: str = os.getenv('LOG_LEVEL', 'INFO')
     LOG_LEVEL: str = os.getenv('LOG_LEVEL', 'INFO')
     
     
+    # Log file configuration
+    LOG_TO_FILE: bool = os.getenv('LOG_TO_FILE', 'true').lower() == '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'))
+    LOG_ROTATION_TYPE: str = os.getenv('LOG_ROTATION_TYPE', 'size')  # 'size' or 'time'
+    LOG_ROTATION_INTERVAL: str = os.getenv('LOG_ROTATION_INTERVAL', 'midnight')  # for time rotation
+    
     @classmethod
     @classmethod
     def validate(cls) -> bool:
     def validate(cls) -> bool:
-        """Validate that required configuration is present."""
+        """Validate all configuration settings."""
+        is_valid = True
+        
+        # Validate Hyperliquid settings
         if not cls.HYPERLIQUID_PRIVATE_KEY:
         if not cls.HYPERLIQUID_PRIVATE_KEY:
-            print("❌ HYPERLIQUID_PRIVATE_KEY is required")
-            return False
+            logger.error("❌ HYPERLIQUID_PRIVATE_KEY is required")
+            is_valid = False
+        
+        # Validate Telegram settings (if enabled)
+        if cls.TELEGRAM_ENABLED:
+            if not cls.TELEGRAM_BOT_TOKEN:
+                logger.error("❌ TELEGRAM_BOT_TOKEN is required when Telegram is enabled")
+                is_valid = False
+            
+            if not cls.TELEGRAM_CHAT_ID:
+                logger.error("❌ TELEGRAM_CHAT_ID is required when Telegram is enabled")
+                is_valid = False
+        
+        # Validate heartbeat interval
+        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:
+            logger.warning("⚠️ BOT_HEARTBEAT_SECONDS is very high (>10 minutes), monitoring may be slow")
+        
+        # Validate trading settings
+        if cls.DEFAULT_TRADE_AMOUNT <= 0:
+            logger.error("❌ DEFAULT_TRADE_AMOUNT must be positive")
+            is_valid = False
         
         
-        # Note: SECRET_KEY might be optional depending on Hyperliquid implementation
-        # Some CCXT exchanges require both apiKey and secret, others only one
+        # Validate logging settings
+        if cls.LOG_MAX_SIZE_MB <= 0:
+            logger.error("❌ LOG_MAX_SIZE_MB must be positive")
+            is_valid = False
         
         
-        if cls.TELEGRAM_ENABLED and not cls.TELEGRAM_BOT_TOKEN:
-            print("❌ TELEGRAM_BOT_TOKEN is required when TELEGRAM_ENABLED=true")
-            return False
+        if cls.LOG_BACKUP_COUNT < 0:
+            logger.error("❌ LOG_BACKUP_COUNT must be non-negative")
+            is_valid = False
         
         
-        if cls.TELEGRAM_ENABLED and not cls.TELEGRAM_CHAT_ID:
-            print("❌ TELEGRAM_CHAT_ID is required when TELEGRAM_ENABLED=true")
-            return False
+        if cls.LOG_ROTATION_TYPE not in ['size', 'time']:
+            logger.error("❌ LOG_ROTATION_TYPE must be 'size' or 'time'")
+            is_valid = False
         
         
-        print("✅ Configuration is valid")
-        return True
+        return is_valid
     
     
     @classmethod
     @classmethod
     def get_hyperliquid_config(cls) -> dict:
     def get_hyperliquid_config(cls) -> dict:

+ 69 - 2
src/hyperliquid_client.py

@@ -4,8 +4,7 @@ from typing import Optional, Dict, Any, List
 from hyperliquid import HyperliquidSync, HyperliquidAsync
 from hyperliquid import HyperliquidSync, HyperliquidAsync
 from config import Config
 from config import Config
 
 
-# Set up logging
-logging.basicConfig(level=getattr(logging, Config.LOG_LEVEL))
+# Use existing logger setup (will be configured by main application)
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 class HyperliquidClient:
 class HyperliquidClient:
@@ -425,4 +424,72 @@ class HyperliquidClient:
             return order
             return order
         except Exception as e:
         except Exception as e:
             logger.error(f"❌ Error placing take profit order: {e}")
             logger.error(f"❌ Error placing take profit order: {e}")
+            return None
+
+    def get_recent_fills(self, limit: int = 100) -> Optional[List[Dict[str, Any]]]:
+        """
+        Get recent fills/trades for the account.
+        
+        Args:
+            limit: Maximum number of fills to return
+            
+        Returns:
+            List of recent fills/trades or None if error
+        """
+        try:
+            if not self.sync_client:
+                logger.error("❌ Client not initialized")
+                return None
+            
+            # Add user parameter for Hyperliquid CCXT compatibility
+            params = {}
+            if Config.HYPERLIQUID_PRIVATE_KEY:
+                wallet_address = Config.HYPERLIQUID_PRIVATE_KEY
+                params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
+                
+            # Fetch recent trades/fills for the account
+            # Use fetch_my_trades to get account-specific trades
+            logger.debug(f"🔍 Fetching recent fills with params: {params}")
+            
+            # Get recent fills across all symbols
+            # We'll fetch trades for all symbols and merge them
+            try:
+                # Option 1: Try fetch_my_trades if available
+                fills = self.sync_client.fetch_my_trades(None, limit=limit, params=params)
+                logger.info(f"✅ Successfully fetched {len(fills)} recent fills")
+                return fills
+            except AttributeError:
+                # Option 2: If fetch_my_trades not available, try alternative approach
+                logger.debug("fetch_my_trades not available, trying alternative approach")
+                
+                # Get positions to determine active symbols
+                positions = self.get_positions()
+                if not positions:
+                    logger.info("No positions found, no recent fills to fetch")
+                    return []
+                
+                # Get symbols from positions
+                symbols = list(set([pos.get('symbol') for pos in positions if pos.get('symbol')]))
+                
+                all_fills = []
+                for symbol in symbols[:5]:  # Limit to 5 symbols to avoid too many requests
+                    try:
+                        symbol_trades = self.sync_client.fetch_my_trades(symbol, limit=limit//len(symbols), params=params)
+                        if symbol_trades:
+                            all_fills.extend(symbol_trades)
+                    except Exception as e:
+                        logger.warning(f"Could not fetch trades for {symbol}: {e}")
+                        continue
+                
+                # Sort by timestamp (newest first)
+                all_fills.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
+                
+                # Return only the requested limit
+                result = all_fills[:limit]
+                logger.info(f"✅ Successfully fetched {len(result)} recent fills from {len(symbols)} symbols")
+                return result
+                
+        except Exception as e:
+            logger.error(f"❌ Error fetching recent fills: {e}")
+            logger.debug(f"💡 Attempted with params: {params}")
             return None 
             return None 

+ 241 - 0
src/logging_config.py

@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+"""
+Logging configuration module with rotation and cleanup support.
+"""
+
+import logging
+import logging.handlers
+import os
+from pathlib import Path
+from datetime import datetime, timedelta
+from typing import Optional
+from config import Config
+
+
+class LoggingManager:
+    """Manages logging configuration with rotation and cleanup."""
+    
+    def __init__(self):
+        """Initialize the logging manager."""
+        self.logger = None
+        self.file_handler = None
+        self.console_handler = None
+        
+    def setup_logging(self) -> logging.Logger:
+        """
+        Set up comprehensive logging with rotation and cleanup.
+        
+        Returns:
+            Configured logger instance
+        """
+        # Create logs directory if it doesn't exist
+        if Config.LOG_TO_FILE:
+            log_dir = Path(Config.LOG_FILE_PATH).parent
+            log_dir.mkdir(parents=True, exist_ok=True)
+        
+        # Create root logger
+        logger = logging.getLogger()
+        logger.setLevel(getattr(logging, Config.LOG_LEVEL.upper()))
+        
+        # Clear any existing handlers
+        logger.handlers.clear()
+        
+        # Create formatter
+        formatter = logging.Formatter(
+            fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+            datefmt='%Y-%m-%d %H:%M:%S'
+        )
+        
+        # Set up console handler
+        self.console_handler = logging.StreamHandler()
+        self.console_handler.setLevel(getattr(logging, Config.LOG_LEVEL.upper()))
+        self.console_handler.setFormatter(formatter)
+        logger.addHandler(self.console_handler)
+        
+        # Set up file handler with rotation (if enabled)
+        if Config.LOG_TO_FILE:
+            if Config.LOG_ROTATION_TYPE == 'size':
+                self.file_handler = logging.handlers.RotatingFileHandler(
+                    filename=Config.LOG_FILE_PATH,
+                    maxBytes=Config.LOG_MAX_SIZE_MB * 1024 * 1024,  # Convert MB to bytes
+                    backupCount=Config.LOG_BACKUP_COUNT,
+                    encoding='utf-8'
+                )
+            else:  # time-based rotation
+                self.file_handler = logging.handlers.TimedRotatingFileHandler(
+                    filename=Config.LOG_FILE_PATH,
+                    when=Config.LOG_ROTATION_INTERVAL,
+                    backupCount=Config.LOG_BACKUP_COUNT,
+                    encoding='utf-8'
+                )
+            
+            self.file_handler.setLevel(getattr(logging, Config.LOG_LEVEL.upper()))
+            self.file_handler.setFormatter(formatter)
+            logger.addHandler(self.file_handler)
+        
+        # Store reference
+        self.logger = logger
+        
+        # Log the configuration
+        logger.info("🔧 Logging system initialized")
+        logger.info(f"📊 Log Level: {Config.LOG_LEVEL}")
+        logger.info(f"📁 Log to File: {Config.LOG_TO_FILE}")
+        
+        if Config.LOG_TO_FILE:
+            logger.info(f"📂 Log File: {Config.LOG_FILE_PATH}")
+            logger.info(f"🔄 Rotation Type: {Config.LOG_ROTATION_TYPE}")
+            logger.info(f"📏 Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation)")
+            logger.info(f"🗃️ Backup Count: {Config.LOG_BACKUP_COUNT}")
+            logger.info(f"⏰ Rotation Interval: {Config.LOG_ROTATION_INTERVAL} (time rotation)")
+        
+        return logger
+    
+    def cleanup_old_logs(self, days_to_keep: int = 30) -> None:
+        """
+        Clean up old log files beyond the backup count.
+        
+        Args:
+            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_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 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
+        """
+        stats = {
+            'log_to_file': Config.LOG_TO_FILE,
+            'log_file_path': Config.LOG_FILE_PATH,
+            'current_size_mb': 0,
+            'backup_files': 0,
+            'total_size_mb': 0
+        }
+        
+        if not Config.LOG_TO_FILE:
+            return stats
+        
+        try:
+            log_dir = Path(Config.LOG_FILE_PATH).parent
+            log_base_name = Path(Config.LOG_FILE_PATH).stem
+            
+            # 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']
+            
+            # Backup files
+            backup_files = list(log_dir.glob(f"{log_base_name}*.log.*"))
+            stats['backup_files'] = len(backup_files)
+            
+            # 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:
+            if self.logger:
+                self.logger.error(f"❌ Error getting log stats: {e}")
+        
+        return stats
+    
+    def format_log_stats(self, stats: Optional[dict] = None) -> str:
+        """
+        Format log statistics for display.
+        
+        Args:
+            stats: Log statistics dictionary (will fetch if None)
+            
+        Returns:
+            Formatted string with log statistics
+        """
+        if stats is None:
+            stats = self.get_log_stats()
+        
+        if not stats['log_to_file']:
+            return "📊 <b>Logging:</b> Console only"
+        
+        return f"""📊 <b>Log File Statistics:</b>
+• File: {Path(stats['log_file_path']).name}
+• Current Size: {stats['current_size_mb']:.2f} MB
+• Backup Files: {stats['backup_files']}
+• Total Size: {stats['total_size_mb']:.2f} MB
+• Max Size: {Config.LOG_MAX_SIZE_MB} MB
+• Rotation: {Config.LOG_ROTATION_TYPE}
+• Backups Kept: {Config.LOG_BACKUP_COUNT}"""
+
+
+# Global logging manager instance
+logging_manager = LoggingManager()
+
+
+def setup_logging() -> logging.Logger:
+    """
+    Set up logging configuration.
+    
+    Returns:
+        Configured logger instance
+    """
+    return logging_manager.setup_logging()
+
+
+def cleanup_logs(days_to_keep: int = 30) -> None:
+    """
+    Clean up old log files.
+    
+    Args:
+        days_to_keep: Number of days worth of logs to keep
+    """
+    logging_manager.cleanup_old_logs(days_to_keep)
+
+
+def get_log_stats() -> dict:
+    """Get log file statistics."""
+    return logging_manager.get_log_stats()
+
+
+def format_log_stats(stats: Optional[dict] = None) -> str:
+    """Format log statistics for display."""
+    return logging_manager.format_log_stats(stats) 

+ 508 - 59
src/telegram_bot.py

@@ -9,20 +9,18 @@ with comprehensive statistics tracking and phone-friendly controls.
 import logging
 import logging
 import asyncio
 import asyncio
 import re
 import re
-from datetime import datetime
+from datetime import datetime, timedelta
 from typing import Optional, Dict, Any
 from typing import Optional, Dict, Any
 from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
 from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
 from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
 from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
 from hyperliquid_client import HyperliquidClient
 from hyperliquid_client import HyperliquidClient
 from trading_stats import TradingStats
 from trading_stats import TradingStats
 from config import Config
 from config import Config
+from alarm_manager import AlarmManager
+from logging_config import setup_logging, cleanup_logs, format_log_stats
 
 
-# Set up logging
-logging.basicConfig(
-    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
-    level=getattr(logging, Config.LOG_LEVEL)
-)
-logger = logging.getLogger(__name__)
+# Set up logging using the new configuration system
+logger = setup_logging().getChild(__name__)
 
 
 class TelegramTradingBot:
 class TelegramTradingBot:
     """Telegram bot for manual trading with comprehensive statistics."""
     """Telegram bot for manual trading with comprehensive statistics."""
@@ -31,6 +29,7 @@ class TelegramTradingBot:
         """Initialize the Telegram trading bot."""
         """Initialize the Telegram trading bot."""
         self.client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
         self.client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
         self.stats = TradingStats()
         self.stats = TradingStats()
+        self.alarm_manager = AlarmManager()
         self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
         self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
         self.application = None
         self.application = None
         
         
@@ -39,6 +38,9 @@ class TelegramTradingBot:
         self.last_known_orders = set()  # Track order IDs we've seen
         self.last_known_orders = set()  # Track order IDs we've seen
         self.last_known_positions = {}  # Track position sizes for P&L calculation
         self.last_known_positions = {}  # Track position sizes for P&L calculation
         
         
+        # External trade monitoring
+        self.last_processed_trade_time = None  # Track last processed external trade
+        
         # Initialize stats with current balance
         # Initialize stats with current balance
         self._initialize_stats()
         self._initialize_stats()
         
         
@@ -90,8 +92,10 @@ Tap the buttons below for instant access to key functions.
 /stats - Trading statistics
 /stats - Trading statistics
 
 
 <b>📊 Market Commands:</b>
 <b>📊 Market Commands:</b>
-/market - Market data
-/price - Current price
+/market - Market data (default token)
+/market SOL - Market data for SOL
+/price - Current price (default token)
+/price BTC - Price for BTC
 
 
 <b>🚀 Perps Trading:</b>
 <b>🚀 Perps Trading:</b>
 • /long BTC 100 - Long BTC with $100 USDC (Market Order)
 • /long BTC 100 - Long BTC with $100 USDC (Market Order)
@@ -114,16 +118,32 @@ Tap the buttons below for instant access to key functions.
 /performance - Performance metrics
 /performance - Performance metrics
 /risk - Risk analysis
 /risk - Risk analysis
 
 
-<b>🔄 Automatic Notifications:</b>
+<b>🔔 Price Alerts:</b>
+• /alarm - List all alarms
+• /alarm BTC 50000 - Set alarm for BTC at $50,000
+• /alarm BTC - Show BTC alarms only
+• /alarm 3 - Remove alarm ID 3
+
+<b>🔄 Automatic Monitoring:</b>
 • Real-time order fill alerts
 • Real-time order fill alerts
 • Position opened/closed notifications  
 • Position opened/closed notifications  
 • P&L calculations on trade closure
 • P&L calculations on trade closure
-• 30-second monitoring interval
+• Price alarm triggers
+• External trade detection & sync
+• Auto stats synchronization
+• {Config.BOT_HEARTBEAT_SECONDS}-second monitoring interval
+
+<b>📊 Universal Trade Tracking:</b>
+• Bot trades: Full logging & notifications
+• Platform trades: Auto-detected & synced
+• Mobile app trades: Monitored & recorded
+• API trades: Tracked & included in stats
 
 
 Type /help for detailed command information.
 Type /help for detailed command information.
 
 
 <b>🔄 Order Monitoring:</b>
 <b>🔄 Order Monitoring:</b>
 • /monitoring - View monitoring status
 • /monitoring - View monitoring status
+• /logs - View log file statistics and cleanup
 
 
 <b>⚙️ Configuration:</b>
 <b>⚙️ Configuration:</b>
 • Symbol: {symbol}
 • Symbol: {symbol}
@@ -186,8 +206,10 @@ For support, contact your bot administrator.
 • /orders - Show open orders
 • /orders - Show open orders
 
 
 <b>📊 Market Data:</b>
 <b>📊 Market Data:</b>
-• /market - Detailed market data
-• /price - Quick price check
+• /market - Detailed market data (default token)
+• /market BTC - Market data for specific token
+• /price - Quick price check (default token)
+• /price SOL - Price for specific token
 
 
 <b>🚀 Perps Trading:</b>
 <b>🚀 Perps Trading:</b>
 • /long BTC 100 - Long BTC with $100 USDC (Market Order)
 • /long BTC 100 - Long BTC with $100 USDC (Market Order)
@@ -211,8 +233,15 @@ For support, contact your bot administrator.
 • /risk - Sharpe ratio, drawdown, VaR
 • /risk - Sharpe ratio, drawdown, VaR
 • /trades - Recent trade history
 • /trades - Recent trade history
 
 
+<b>🔔 Price Alerts:</b>
+• /alarm - List all active alarms
+• /alarm BTC 50000 - Set alarm for BTC at $50,000
+• /alarm BTC - Show all BTC alarms
+• /alarm 3 - Remove alarm ID 3
+
 <b>🔄 Order Monitoring:</b>
 <b>🔄 Order Monitoring:</b>
 • /monitoring - View monitoring status
 • /monitoring - View monitoring status
+• /logs - View log file statistics and cleanup
 
 
 <b>⚙️ Configuration:</b>
 <b>⚙️ Configuration:</b>
 • Symbol: {symbol}
 • Symbol: {symbol}
@@ -457,35 +486,83 @@ For support, contact your bot administrator.
             await update.message.reply_text("❌ Unauthorized access.")
             await update.message.reply_text("❌ Unauthorized access.")
             return
             return
         
         
-        symbol = Config.DEFAULT_TRADING_SYMBOL
+        # Check if token is provided as argument
+        if context.args and len(context.args) >= 1:
+            symbol = context.args[0].upper()
+        else:
+            symbol = Config.DEFAULT_TRADING_SYMBOL
+        
         market_data = self.client.get_market_data(symbol)
         market_data = self.client.get_market_data(symbol)
         
         
-        if market_data:
-            ticker = market_data['ticker']
-            orderbook = market_data['orderbook']
-            
-            # Calculate 24h change
-            current_price = float(ticker.get('last', 0))
-            high_24h = float(ticker.get('high', 0))
-            low_24h = float(ticker.get('low', 0))
-            
-            market_text = f"📊 <b>Market Data - {symbol}</b>\n\n"
-            market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
-            market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
-            market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
-            market_text += f"📊 <b>24h Volume:</b> {ticker.get('baseVolume', 'N/A')}\n\n"
-            
-            if orderbook.get('bids') and orderbook.get('asks'):
-                best_bid = float(orderbook['bids'][0][0]) if orderbook['bids'] else 0
-                best_ask = float(orderbook['asks'][0][0]) if orderbook['asks'] else 0
-                spread = best_ask - best_bid
-                spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
+        if market_data and market_data.get('ticker'):
+            try:
+                ticker = market_data['ticker']
+                orderbook = market_data.get('orderbook', {})
+                
+                # Safely extract ticker data with fallbacks
+                current_price = float(ticker.get('last') or 0)
+                high_24h = float(ticker.get('high') or 0)
+                low_24h = float(ticker.get('low') or 0)
+                volume_24h = ticker.get('baseVolume') or ticker.get('volume') or 'N/A'
                 
                 
-                market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
-                market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
-                market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
+                market_text = f"📊 <b>Market Data - {symbol}</b>\n\n"
+                
+                if current_price > 0:
+                    market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
+                else:
+                    market_text += f"💵 <b>Current Price:</b> N/A\n"
+                
+                if high_24h > 0:
+                    market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
+                else:
+                    market_text += f"📈 <b>24h High:</b> N/A\n"
+                
+                if low_24h > 0:
+                    market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
+                else:
+                    market_text += f"📉 <b>24h Low:</b> N/A\n"
+                
+                market_text += f"📊 <b>24h Volume:</b> {volume_24h}\n\n"
+                
+                # Handle orderbook data safely
+                if orderbook and orderbook.get('bids') and orderbook.get('asks'):
+                    try:
+                        bids = orderbook.get('bids', [])
+                        asks = orderbook.get('asks', [])
+                        
+                        if bids and asks and len(bids) > 0 and len(asks) > 0:
+                            best_bid = float(bids[0][0]) if bids[0][0] else 0
+                            best_ask = float(asks[0][0]) if asks[0][0] else 0
+                            
+                            if best_bid > 0 and best_ask > 0:
+                                spread = best_ask - best_bid
+                                spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
+                                
+                                market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
+                                market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
+                                market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
+                            else:
+                                market_text += f"📋 <b>Orderbook:</b> Data unavailable\n"
+                        else:
+                            market_text += f"📋 <b>Orderbook:</b> No orders available\n"
+                    except (IndexError, ValueError, TypeError) as e:
+                        market_text += f"📋 <b>Orderbook:</b> Error parsing data\n"
+                else:
+                    market_text += f"📋 <b>Orderbook:</b> Not available\n"
+                
+                # Add usage hint
+                market_text += f"\n💡 <b>Usage:</b> <code>/market {symbol}</code> or <code>/market</code> for default"
+                
+            except (ValueError, TypeError) as e:
+                market_text = f"❌ <b>Error parsing market data</b>\n\n"
+                market_text += f"🔧 Raw data received but couldn't parse values.\n"
+                market_text += f"📞 Please try again or contact support if this persists."
         else:
         else:
-            market_text = "❌ Could not fetch market data"
+            market_text = f"❌ <b>Could not fetch market data for {symbol}</b>\n\n"
+            market_text += f"🔄 Please try again in a moment.\n"
+            market_text += f"🌐 Check your network connection.\n"
+            market_text += f"📡 API may be temporarily unavailable.\n\n"
+            market_text += f"💡 <b>Usage:</b> <code>/market BTC</code>, <code>/market ETH</code>, <code>/market SOL</code>, etc."
         
         
         await update.message.reply_text(market_text, parse_mode='HTML')
         await update.message.reply_text(market_text, parse_mode='HTML')
     
     
@@ -495,18 +572,37 @@ For support, contact your bot administrator.
             await update.message.reply_text("❌ Unauthorized access.")
             await update.message.reply_text("❌ Unauthorized access.")
             return
             return
         
         
-        symbol = Config.DEFAULT_TRADING_SYMBOL
+        # Check if token is provided as argument
+        if context.args and len(context.args) >= 1:
+            symbol = context.args[0].upper()
+        else:
+            symbol = Config.DEFAULT_TRADING_SYMBOL
+        
         market_data = self.client.get_market_data(symbol)
         market_data = self.client.get_market_data(symbol)
         
         
-        if market_data:
-            price = float(market_data['ticker'].get('last', 0))
-            price_text = f"💵 <b>{symbol}</b>: ${price:,.2f}"
-            
-            # Add timestamp
-            timestamp = datetime.now().strftime('%H:%M:%S')
-            price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
+        if market_data and market_data.get('ticker'):
+            try:
+                ticker = market_data['ticker']
+                price_value = ticker.get('last')
+                
+                if price_value is not None:
+                    price = float(price_value)
+                    price_text = f"💵 <b>{symbol}</b>: ${price:,.2f}"
+                    
+                    # Add timestamp
+                    timestamp = datetime.now().strftime('%H:%M:%S')
+                    price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
+                    
+                    # Add usage hint
+                    price_text += f"\n💡 <i>Usage: </i><code>/price {symbol}</code><i> or </i><code>/price</code><i> for default</i>"
+                else:
+                    price_text = f"💵 <b>{symbol}</b>: Price not available\n⚠️ <i>Data temporarily unavailable</i>"
+                    
+            except (ValueError, TypeError) as e:
+                price_text = f"❌ <b>Error parsing price for {symbol}</b>\n🔧 <i>Please try again</i>"
         else:
         else:
-            price_text = f"❌ Could not fetch price for {symbol}"
+            price_text = f"❌ <b>Could not fetch price for {symbol}</b>\n🔄 <i>Please try again in a moment</i>\n\n"
+            price_text += f"💡 <b>Usage:</b> <code>/price BTC</code>, <code>/price ETH</code>, <code>/price SOL</code>, etc."
         
         
         await update.message.reply_text(price_text, parse_mode='HTML')
         await update.message.reply_text(price_text, parse_mode='HTML')
     
     
@@ -953,6 +1049,8 @@ For support, contact your bot administrator.
         self.application.add_handler(CommandHandler("sl", self.sl_command))
         self.application.add_handler(CommandHandler("sl", self.sl_command))
         self.application.add_handler(CommandHandler("tp", self.tp_command))
         self.application.add_handler(CommandHandler("tp", self.tp_command))
         self.application.add_handler(CommandHandler("monitoring", self.monitoring_command))
         self.application.add_handler(CommandHandler("monitoring", self.monitoring_command))
+        self.application.add_handler(CommandHandler("alarm", self.alarm_command))
+        self.application.add_handler(CommandHandler("logs", self.logs_command))
         
         
         # Callback query handler for inline keyboards
         # Callback query handler for inline keyboards
         self.application.add_handler(CallbackQueryHandler(self.button_callback))
         self.application.add_handler(CallbackQueryHandler(self.button_callback))
@@ -988,11 +1086,22 @@ For support, contact your bot administrator.
                 f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
                 f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
                 f"📊 Default Symbol: {Config.DEFAULT_TRADING_SYMBOL}\n"
                 f"📊 Default Symbol: {Config.DEFAULT_TRADING_SYMBOL}\n"
                 f"📱 Manual trading ready!\n"
                 f"📱 Manual trading ready!\n"
-                f"🔄 Order monitoring: Active\n"
+                f"🔄 Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)\n"
+                f"🔄 External trade monitoring: Active\n"
+                f"🔔 Price alarms: Active\n"
+                f"📊 Auto stats sync: Enabled\n"
+                f"📝 Logging: {'File + Console' if Config.LOG_TO_FILE else 'Console only'}\n"
                 f"⏰ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
                 f"⏰ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
                 "Use /start for quick actions or /help for all commands."
                 "Use /start for quick actions or /help for all commands."
             )
             )
             
             
+            # Perform initial log cleanup
+            try:
+                cleanup_logs(days_to_keep=30)
+                logger.info("🧹 Initial log cleanup completed")
+            except Exception as e:
+                logger.warning(f"⚠️ Initial log cleanup failed: {e}")
+            
             # Start the application
             # Start the application
             await self.application.start()
             await self.application.start()
             
             
@@ -1696,17 +1805,17 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
             logger.error(f"❌ Error initializing order tracking: {e}")
             logger.error(f"❌ Error initializing order tracking: {e}")
     
     
     async def _order_monitoring_loop(self):
     async def _order_monitoring_loop(self):
-        """Main monitoring loop that runs every 30 seconds."""
+        """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds."""
         while self.monitoring_active:
         while self.monitoring_active:
             try:
             try:
                 await self._check_order_fills()
                 await self._check_order_fills()
-                await asyncio.sleep(30)  # Wait 30 seconds
+                await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)  # Use configurable interval
             except asyncio.CancelledError:
             except asyncio.CancelledError:
                 logger.info("🛑 Order monitoring cancelled")
                 logger.info("🛑 Order monitoring cancelled")
                 break
                 break
             except Exception as e:
             except Exception as e:
                 logger.error(f"❌ Error in order monitoring loop: {e}")
                 logger.error(f"❌ Error in order monitoring loop: {e}")
-                await asyncio.sleep(30)  # Continue monitoring even if there's an error
+                await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)  # Continue monitoring even if there's an error
     
     
     async def _check_order_fills(self):
     async def _check_order_fills(self):
         """Check for filled orders and send notifications."""
         """Check for filled orders and send notifications."""
@@ -1729,9 +1838,168 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
             self.last_known_orders = current_order_ids
             self.last_known_orders = current_order_ids
             await self._update_position_tracking(current_positions)
             await self._update_position_tracking(current_positions)
             
             
+            # Check price alarms
+            await self._check_price_alarms()
+            
+            # Check external trades (trades made outside the bot)
+            await self._check_external_trades()
+            
         except Exception as e:
         except Exception as e:
             logger.error(f"❌ Error checking order fills: {e}")
             logger.error(f"❌ Error checking order fills: {e}")
-    
+
+    async def _check_price_alarms(self):
+        """Check all active price alarms."""
+        try:
+            # Get all active alarms
+            active_alarms = self.alarm_manager.get_all_active_alarms()
+            if not active_alarms:
+                return
+            
+            # Get unique tokens from alarms
+            tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
+            
+            # Fetch current prices for all tokens
+            price_data = {}
+            for token in tokens_to_check:
+                symbol = f"{token}/USDC:USDC"
+                market_data = self.client.get_market_data(symbol)
+                
+                if market_data and market_data.get('ticker'):
+                    current_price = market_data['ticker'].get('last')
+                    if current_price is not None:
+                        price_data[token] = float(current_price)
+            
+            # Check alarms against current prices
+            triggered_alarms = self.alarm_manager.check_alarms(price_data)
+            
+            # Send notifications for triggered alarms
+            for alarm in triggered_alarms:
+                await self._send_alarm_notification(alarm)
+                
+        except Exception as e:
+            logger.error(f"❌ Error checking price alarms: {e}")
+
+    async def _send_alarm_notification(self, alarm: Dict[str, Any]):
+        """Send notification for triggered alarm."""
+        try:
+            message = self.alarm_manager.format_triggered_alarm(alarm)
+            await self.send_message(message)
+            logger.info(f"📢 Sent alarm notification: {alarm['token']} ID {alarm['id']} @ ${alarm['triggered_price']}")
+        except Exception as e:
+            logger.error(f"❌ Error sending alarm notification: {e}")
+
+    async def _check_external_trades(self):
+        """Check for trades made outside the Telegram bot and update stats."""
+        try:
+            # Get recent fills from Hyperliquid
+            recent_fills = self.client.get_recent_fills()
+            
+            if not recent_fills:
+                return
+            
+            # Initialize last processed time if first run
+            if self.last_processed_trade_time is None:
+                # Set to current time minus 1 hour to catch recent activity
+                self.last_processed_trade_time = (datetime.now() - timedelta(hours=1)).isoformat()
+            
+            # Filter for new trades since last check
+            new_trades = []
+            latest_trade_time = self.last_processed_trade_time
+            
+            for fill in recent_fills:
+                fill_time = fill.get('timestamp')
+                if fill_time and fill_time > self.last_processed_trade_time:
+                    new_trades.append(fill)
+                    if fill_time > latest_trade_time:
+                        latest_trade_time = fill_time
+            
+            if not new_trades:
+                return
+            
+            # Process new trades
+            for trade in new_trades:
+                await self._process_external_trade(trade)
+            
+            # Update last processed time
+            self.last_processed_trade_time = latest_trade_time
+            
+            if new_trades:
+                logger.info(f"📊 Processed {len(new_trades)} external trades")
+                
+        except Exception as e:
+            logger.error(f"❌ Error checking external trades: {e}")
+
+    async def _process_external_trade(self, trade: Dict[str, Any]):
+        """Process an individual external trade."""
+        try:
+            # Extract trade information
+            symbol = trade.get('symbol', '')
+            side = trade.get('side', '')
+            amount = float(trade.get('amount', 0))
+            price = float(trade.get('price', 0))
+            trade_id = trade.get('id', 'external')
+            timestamp = trade.get('timestamp', '')
+            
+            if not all([symbol, side, amount, price]):
+                return
+            
+            # Record trade in stats
+            self.stats.record_trade(symbol, side, amount, price, trade_id)
+            
+            # Send notification for significant trades
+            await self._send_external_trade_notification(trade)
+            
+            logger.info(f"📋 Processed external trade: {side} {amount} {symbol} @ ${price}")
+            
+        except Exception as e:
+            logger.error(f"❌ Error processing external trade: {e}")
+
+    async def _send_external_trade_notification(self, trade: Dict[str, Any]):
+        """Send notification for external trades."""
+        try:
+            symbol = trade.get('symbol', '')
+            side = trade.get('side', '')
+            amount = float(trade.get('amount', 0))
+            price = float(trade.get('price', 0))
+            timestamp = trade.get('timestamp', '')
+            
+            # Extract token from symbol
+            token = symbol.split('/')[0] if '/' in symbol else symbol
+            
+            # Format timestamp
+            try:
+                trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
+                time_str = trade_time.strftime('%H:%M:%S')
+            except:
+                time_str = "Unknown"
+            
+            # Determine trade type and emoji
+            side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
+            trade_value = amount * price
+            
+            message = f"""
+🔄 <b>External Trade Detected</b>
+
+📊 <b>Trade Details:</b>
+• Token: {token}
+• Side: {side.upper()}
+• Amount: {amount} {token}
+• Price: ${price:,.2f}
+• Value: ${trade_value:,.2f}
+
+{side_emoji} <b>Source:</b> Direct Platform Trade
+⏰ <b>Time:</b> {time_str}
+
+📈 <b>Note:</b> This trade was executed outside the Telegram bot
+📊 Stats have been automatically updated
+            """
+            
+            await self.send_message(message.strip())
+            logger.info(f"📢 Sent external trade notification: {side} {amount} {token}")
+            
+        except Exception as e:
+            logger.error(f"❌ Error sending external trade notification: {e}")
+
     async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
     async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
         """Process filled orders and determine if they opened or closed positions."""
         """Process filled orders and determine if they opened or closed positions."""
         try:
         try:
@@ -1928,20 +2196,35 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
             await update.message.reply_text("❌ Unauthorized access.")
             await update.message.reply_text("❌ Unauthorized access.")
             return
             return
         
         
+        # Get alarm statistics
+        alarm_stats = self.alarm_manager.get_statistics()
+        
         status_text = f"""
         status_text = f"""
-🔄 <b>Order Monitoring Status</b>
+🔄 <b>System Monitoring Status</b>
 
 
-📊 <b>Current Status:</b>
+📊 <b>Order Monitoring:</b>
 • Active: {'✅ Yes' if self.monitoring_active else '❌ No'}
 • Active: {'✅ Yes' if self.monitoring_active else '❌ No'}
-• Check Interval: 30 seconds
+• Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
 • Orders Tracked: {len(self.last_known_orders)}
 • Orders Tracked: {len(self.last_known_orders)}
 • Positions Tracked: {len(self.last_known_positions)}
 • Positions Tracked: {len(self.last_known_positions)}
 
 
+🔔 <b>Price Alarms:</b>
+• Active Alarms: {alarm_stats['total_active']}
+• Triggered Today: {alarm_stats['total_triggered']}
+• Tokens Monitored: {alarm_stats['tokens_tracked']}
+• Next Alarm ID: {alarm_stats['next_id']}
+
+🔄 <b>External Trade Monitoring:</b>
+• Last Check: {self.last_processed_trade_time or 'Not started'}
+• Auto Stats Update: ✅ Enabled
+• External Notifications: ✅ Enabled
+
 📈 <b>Notifications:</b>
 📈 <b>Notifications:</b>
-• 🚀 Position Opened
-• 📈 Position Increased
-• 📉 Position Partially Closed
-• 🎯 Position Closed (with P&L)
+• 🚀 Position Opened/Increased
+• 📉 Position Partially/Fully Closed
+• 🎯 P&L Calculations
+• 🔔 Price Alarm Triggers
+• 🔄 External Trade Detection
 
 
 ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
 ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
 
 
@@ -1949,11 +2232,177 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
 • Real-time order fill detection
 • Real-time order fill detection
 • Automatic P&L calculation
 • Automatic P&L calculation
 • Position change tracking
 • Position change tracking
+• Price alarm monitoring
+• External trade monitoring
+• Auto stats synchronization
 • Instant Telegram notifications
 • Instant Telegram notifications
         """
         """
         
         
+        if alarm_stats['token_breakdown']:
+            status_text += f"\n\n📋 <b>Active Alarms by Token:</b>\n"
+            for token, count in alarm_stats['token_breakdown'].items():
+                status_text += f"• {token}: {count} alarm{'s' if count != 1 else ''}\n"
+        
         await update.message.reply_text(status_text.strip(), parse_mode='HTML')
         await update.message.reply_text(status_text.strip(), parse_mode='HTML')
 
 
+    async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Handle the /alarm command for price alerts."""
+        if not self.is_authorized(update.effective_chat.id):
+            await update.message.reply_text("❌ Unauthorized access.")
+            return
+        
+        try:
+            if not context.args or len(context.args) == 0:
+                # No arguments - list all alarms
+                alarms = self.alarm_manager.get_all_active_alarms()
+                message = self.alarm_manager.format_alarm_list(alarms)
+                await update.message.reply_text(message, parse_mode='HTML')
+                return
+            
+            elif len(context.args) == 1:
+                arg = context.args[0]
+                
+                # Check if argument is a number (alarm ID to remove)
+                try:
+                    alarm_id = int(arg)
+                    # Remove alarm by ID
+                    if self.alarm_manager.remove_alarm(alarm_id):
+                        await update.message.reply_text(f"✅ Alarm ID {alarm_id} has been removed.")
+                    else:
+                        await update.message.reply_text(f"❌ Alarm ID {alarm_id} not found.")
+                    return
+                except ValueError:
+                    # Not a number, treat as token
+                    token = arg.upper()
+                    alarms = self.alarm_manager.get_alarms_by_token(token)
+                    message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
+                    await update.message.reply_text(message, parse_mode='HTML')
+                    return
+            
+            elif len(context.args) == 2:
+                # Set new alarm: /alarm TOKEN PRICE
+                token = context.args[0].upper()
+                target_price = float(context.args[1])
+                
+                # Get current market price
+                symbol = f"{token}/USDC:USDC"
+                market_data = self.client.get_market_data(symbol)
+                
+                if not market_data or not market_data.get('ticker'):
+                    await update.message.reply_text(f"❌ Could not fetch current price for {token}")
+                    return
+                
+                current_price = float(market_data['ticker'].get('last', 0))
+                if current_price <= 0:
+                    await update.message.reply_text(f"❌ Invalid current price for {token}")
+                    return
+                
+                # Create the alarm
+                alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
+                
+                # Format confirmation message
+                direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
+                price_diff = abs(target_price - current_price)
+                price_diff_percent = (price_diff / current_price) * 100
+                
+                message = f"""
+✅ <b>Price Alarm Created</b>
+
+📊 <b>Alarm Details:</b>
+• Alarm ID: {alarm['id']}
+• Token: {token}
+• Target Price: ${target_price:,.2f}
+• Current Price: ${current_price:,.2f}
+• Direction: {alarm['direction'].upper()}
+
+{direction_emoji} <b>Alert Condition:</b>
+Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
+
+💰 <b>Price Difference:</b>
+• Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
+• Status: ACTIVE ✅
+
+⏰ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
+
+💡 The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
+                """
+                
+                await update.message.reply_text(message.strip(), parse_mode='HTML')
+                
+            else:
+                # Too many arguments
+                await update.message.reply_text(
+                    "❌ Invalid usage. Examples:\n\n"
+                    "• <code>/alarm</code> - List all alarms\n"
+                    "• <code>/alarm BTC</code> - List BTC alarms\n"
+                    "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
+                    "• <code>/alarm 3</code> - Remove alarm ID 3",
+                    parse_mode='HTML'
+                )
+                
+        except ValueError:
+            await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
+        except Exception as e:
+            error_message = f"❌ Error processing alarm command: {str(e)}"
+            await update.message.reply_text(error_message)
+            logger.error(f"Error in alarm command: {e}")
+
+    async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Handle the /logs command to show log file statistics and cleanup options."""
+        if not self.is_authorized(update.effective_chat.id):
+            await update.message.reply_text("❌ Unauthorized access.")
+            return
+        
+        try:
+            # Check for cleanup argument
+            if context.args and len(context.args) >= 1:
+                if context.args[0].lower() == 'cleanup':
+                    # Get days parameter (default 30)
+                    days_to_keep = 30
+                    if len(context.args) >= 2:
+                        try:
+                            days_to_keep = int(context.args[1])
+                        except ValueError:
+                            await update.message.reply_text("❌ Invalid number of days. Using default (30).")
+                    
+                    # Perform cleanup
+                    await update.message.reply_text(f"🧹 Cleaning up log files older than {days_to_keep} days...")
+                    cleanup_logs(days_to_keep)
+                    await update.message.reply_text(f"✅ Log cleanup completed!")
+                    return
+            
+            # Show log statistics
+            log_stats_text = format_log_stats()
+            
+            # Add additional info
+            status_text = f"""
+📊 <b>System Logging Status</b>
+
+{log_stats_text}
+
+📈 <b>Log Configuration:</b>
+• Log Level: {Config.LOG_LEVEL}
+• Heartbeat Interval: {Config.BOT_HEARTBEAT_SECONDS}s
+• Bot Uptime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+
+💡 <b>Log Management:</b>
+• <code>/logs cleanup</code> - Clean old logs (30 days)
+• <code>/logs cleanup 7</code> - Clean logs older than 7 days
+• Log rotation happens automatically
+• Old backups are removed automatically
+
+🔧 <b>Configuration:</b>
+• Rotation Type: {Config.LOG_ROTATION_TYPE}
+• Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation)
+• Backup Count: {Config.LOG_BACKUP_COUNT}
+            """
+            
+            await update.message.reply_text(status_text.strip(), parse_mode='HTML')
+            
+        except Exception as e:
+            error_message = f"❌ Error processing logs command: {str(e)}"
+            await update.message.reply_text(error_message)
+            logger.error(f"Error in logs command: {e}")
 
 
 async def main_async():
 async def main_async():
     """Async main entry point for the Telegram bot."""
     """Async main entry point for the Telegram bot."""

+ 58 - 0
test_heartbeat_config.py

@@ -0,0 +1,58 @@
+#!/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() 

+ 104 - 0
test_logging_system.py

@@ -0,0 +1,104 @@
+#!/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() 

+ 257 - 0
tests/test_alarm_system.py

@@ -0,0 +1,257 @@
+#!/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()