This commit is contained in:
Gal Podlipnik 2025-07-17 02:43:19 +02:00
parent 69462cf3e0
commit 761e595389
7 changed files with 835 additions and 8 deletions

10
.gitignore vendored
View File

@ -68,3 +68,13 @@ Thumbs.db
tmp/ tmp/
temp/ temp/
.tmp/ .tmp/
# Docker
.dockerignore
# Backups and deployment
backups/
backup_*/
# Health check files
/tmp/health

288
DOCKER_DEPLOYMENT.md Normal file
View File

@ -0,0 +1,288 @@
# Docker Deployment Guide
This guide covers deploying the trading system for 24/7 operation using Docker.
## 🚀 Quick Start
### 1. Setup Environment
```bash
# Copy environment template
cp .env.production .env
# Edit with your API keys
nano .env
```
### 2. Deploy the System
```bash
# Start the trading system
./deploy.sh start
# View logs
./deploy.sh logs live
# Check status
./deploy.sh status
```
## 📋 Deployment Options
### Paper Trading (Recommended)
```bash
# Safe paper trading mode
./deploy.sh start
```
### Live Trading (Advanced)
```bash
# Edit .env file first
TRADING_MODE=live
# Deploy with extreme caution
./deploy.sh start
```
## 🐳 Docker Architecture
### Services
1. **trading-bot**: Main trading application
- Runs 24/7 with auto-restart
- Health monitoring every 60 seconds
- Resource limits for safety
2. **monitoring**: Optional monitoring service
- Displays system status every 5 minutes
- Shows recent performance and trades
- Lightweight Alpine Linux container
### Volumes
- `./logs:/app/logs` - Persistent log storage
- `./config:/app/config:ro` - Read-only configuration
### Security Features
- Non-root user (trader:1000)
- Resource limits (512MB RAM, 0.5 CPU)
- Read-only configuration mounting
- Environment-based secrets
## 🛡️ Safety Features
### Health Monitoring
- Container health checks every 60 seconds
- Application health checks every 5 minutes
- Automatic restart on failure
### Risk Management
- Paper trading by default
- Resource constraints
- Graceful shutdown handling
- Comprehensive error logging
### Backup & Recovery
```bash
# Create backup
./deploy.sh backup
# View backups
ls backups/
```
## 📊 Monitoring & Logs
### Real-time Monitoring
```bash
# Follow live logs
./deploy.sh logs live
# System status
./deploy.sh status
# Container status
docker compose ps
```
### Log Files
- `logs/trading.log` - General system logs
- `logs/trades.log` - Trade execution details
- `logs/risk.log` - Risk management events
- `logs/performance.log` - Performance metrics
### Health Endpoints
- Health file: `/tmp/health` (inside container)
- Status format: `OK:timestamp:price`
## 🔧 Management Commands
### Deployment Script (`deploy.sh`)
```bash
./deploy.sh start # Start system
./deploy.sh stop # Stop system
./deploy.sh restart # Restart system
./deploy.sh logs # Show recent logs
./deploy.sh logs live # Follow live logs
./deploy.sh status # System status
./deploy.sh backup # Create backup
./deploy.sh update # Update and restart
```
### Docker Compose Commands
```bash
# Manual control
docker compose up -d # Start services
docker compose down # Stop services
docker compose logs -f # Follow logs
docker compose ps # Service status
# Rebuild containers
docker compose build --no-cache
```
## 🔄 Updates & Maintenance
### Updating the System
```bash
# Automated update (with backup)
./deploy.sh update
# Manual update
docker compose down
git pull # if using git
docker compose build --no-cache
docker compose up -d
```
### Configuration Changes
```bash
# Edit configuration
nano .env
# Restart to apply changes
./deploy.sh restart
```
### Log Rotation
Logs are automatically managed by Docker. To manually clean:
```bash
# Clean old logs (be careful!)
docker system prune -f
# Or manually rotate logs
./deploy.sh backup # Backup first
> logs/trading.log
> logs/trades.log
```
## 🚨 Troubleshooting
### Common Issues
1. **Container won't start**
```bash
# Check logs
docker compose logs trading-bot
# Verify environment
cat .env
# Test configuration
docker compose config
```
2. **API connection errors**
```bash
# Verify API keys in .env
grep -E "ALPACA_(API|SECRET)_KEY" .env
# Test connection
docker exec trading-system python3 -c "from src.data_handler import DataHandler; print(DataHandler().get_latest_price('AAPL'))"
```
3. **Health check failures**
```bash
# Check health status
docker inspect trading-system | grep -A 5 Health
# Manual health check
docker exec trading-system python3 -c "from main import health_check; print(health_check())"
```
4. **Performance issues**
```bash
# Check resource usage
docker stats trading-system
# Increase limits in docker compose.yml
nano docker compose.yml
```
### Emergency Procedures
1. **Immediate stop**
```bash
./deploy.sh stop
# or
docker kill trading-system
```
2. **Emergency backup**
```bash
./deploy.sh backup
cp -r logs backups/emergency_backup_$(date +%Y%m%d_%H%M%S)/
```
3. **Reset system**
```bash
./deploy.sh stop
docker compose down -v # WARNING: Removes volumes
./deploy.sh start
```
## 🌐 Production Considerations
### Server Requirements
- **Minimum**: 1 CPU, 1GB RAM, 10GB storage
- **Recommended**: 2 CPU, 2GB RAM, 50GB storage
- **OS**: Linux (Ubuntu 20.04+ recommended)
### Network Requirements
- Stable internet connection
- HTTPS access to Alpaca API
- NTP for accurate timestamps
### Security Best Practices
1. Use paper trading initially
2. Limit server access (SSH keys only)
3. Regular backups to external storage
4. Monitor logs for anomalies
5. Keep system updated
### Scaling Considerations
- Multiple symbols: Use separate containers
- High frequency: Increase resource limits
- Redundancy: Deploy across multiple servers
## 📞 Support
### Getting Help
1. Check logs: `./deploy.sh logs`
2. Verify configuration: `docker compose config`
3. Test health: `./deploy.sh status`
4. Review documentation
### Important Notes
- Always test in paper trading first
- Monitor performance regularly
- Keep backups of profitable configurations
- Never disable risk management features
## 🔒 Disclaimer
This deployment setup is for educational purposes. Trading involves risk of loss. Always:
- Use paper trading for testing
- Understand the risks involved
- Never trade more than you can afford to lose
- Monitor the system regularly
- Keep security best practices

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
# Trading System Dockerfile
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy source code
COPY . .
# Create logs directory
RUN mkdir -p logs
# Create non-root user for security
RUN useradd -m -u 1000 trader && \
chown -R trader:trader /app
USER trader
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python3 -c "import sys; sys.path.append('src'); from src.data_handler import DataHandler; DataHandler().get_latest_price('AAPL')" || exit 1
# Default command
CMD ["python3", "main.py", "--mode", "paper"]

249
deploy.sh Executable file
View File

@ -0,0 +1,249 @@
#!/bin/bash
# Trading System Deployment Script
# Usage: ./deploy.sh [start|stop|restart|logs|status|backup]
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if .env file exists
check_env() {
if [ ! -f .env ]; then
log_warning ".env file not found. Creating from template..."
if [ -f .env.production ]; then
cp .env.production .env
log_info "Please edit .env file with your actual API keys"
log_info "nano .env"
exit 1
else
log_error "No .env template found!"
exit 1
fi
fi
}
# Validate environment
validate_env() {
log_info "Validating environment configuration..."
# Source the .env file
export $(grep -v '^#' .env | xargs)
# Check required variables
if [ -z "$ALPACA_API_KEY" ] || [ "$ALPACA_API_KEY" = "your_paper_api_key_here" ]; then
log_error "ALPACA_API_KEY not set in .env file"
exit 1
fi
if [ -z "$ALPACA_SECRET_KEY" ] || [ "$ALPACA_SECRET_KEY" = "your_paper_secret_key_here" ]; then
log_error "ALPACA_SECRET_KEY not set in .env file"
exit 1
fi
# Warn about live trading
if [ "$TRADING_MODE" = "live" ]; then
log_warning "⚠️ LIVE TRADING MODE ENABLED! ⚠️"
log_warning "This will use real money. Are you sure? (y/N)"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
log_info "Switching to paper trading mode for safety"
sed -i.bak 's/TRADING_MODE=live/TRADING_MODE=paper/' .env
fi
fi
log_success "Environment validation passed"
}
# Start the trading system
start_system() {
log_info "Starting trading system..."
check_env
validate_env
# Create logs directory
mkdir -p logs
# Build and start containers
docker compose build
docker compose up -d
log_success "Trading system started!"
log_info "View logs with: ./deploy.sh logs"
log_info "Check status with: ./deploy.sh status"
}
# Stop the trading system
stop_system() {
log_info "Stopping trading system..."
docker compose down
log_success "Trading system stopped"
}
# Restart the trading system
restart_system() {
log_info "Restarting trading system..."
stop_system
sleep 2
start_system
}
# Show logs
show_logs() {
if [ "$2" = "live" ]; then
log_info "Following live logs (Ctrl+C to exit)..."
docker compose logs -f trading-bot
else
log_info "Recent logs:"
docker compose logs --tail=50 trading-bot
fi
}
# Show system status
show_status() {
log_info "Trading system status:"
echo
# Container status
docker compose ps
echo
# Recent performance
log_info "Recent performance (last 5 entries):"
if [ -f logs/performance.log ]; then
tail -5 logs/performance.log
else
log_warning "No performance data yet"
fi
echo
# Recent trades
log_info "Recent trades (last 3 entries):"
if [ -f logs/trades.log ]; then
tail -3 logs/trades.log
else
log_warning "No trades yet"
fi
echo
# Risk events
log_info "Recent risk events:"
if [ -f logs/risk.log ]; then
tail -3 logs/risk.log
else
log_success "No risk events"
fi
}
# Backup logs and data
backup_data() {
log_info "Creating backup..."
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_DIR="backups/backup_$TIMESTAMP"
mkdir -p "$BACKUP_DIR"
# Backup logs
if [ -d logs ]; then
cp -r logs "$BACKUP_DIR/"
fi
# Backup configuration
cp .env "$BACKUP_DIR/.env.backup" 2>/dev/null || true
cp docker-compose.yml "$BACKUP_DIR/"
log_success "Backup created: $BACKUP_DIR"
}
# Update system
update_system() {
log_info "Updating trading system..."
# Stop system
stop_system
# Backup current state
backup_data
# Rebuild containers
docker compose build --no-cache
# Start system
start_system
log_success "System updated successfully"
}
# Main script logic
case "${1:-help}" in
start)
start_system
;;
stop)
stop_system
;;
restart)
restart_system
;;
logs)
show_logs "$@"
;;
status)
show_status
;;
backup)
backup_data
;;
update)
update_system
;;
help|*)
echo "Trading System Deployment Script"
echo
echo "Usage: $0 [command]"
echo
echo "Commands:"
echo " start - Start the trading system"
echo " stop - Stop the trading system"
echo " restart - Restart the trading system"
echo " logs - Show recent logs"
echo " logs live - Follow live logs"
echo " status - Show system status and performance"
echo " backup - Backup logs and configuration"
echo " update - Update and restart system"
echo " help - Show this help message"
echo
echo "Examples:"
echo " $0 start # Start trading"
echo " $0 logs live # Watch live logs"
echo " $0 status # Check performance"
echo
;;
esac

89
docker-compose.yml Normal file
View File

@ -0,0 +1,89 @@
services:
trading-bot:
build: .
container_name: trading-system
restart: unless-stopped
environment:
# Alpaca API Configuration
- ALPACA_API_KEY=${ALPACA_API_KEY}
- ALPACA_SECRET_KEY=${ALPACA_SECRET_KEY}
- ALPACA_BASE_URL=${ALPACA_BASE_URL:-https://paper-api.alpaca.markets}
# Trading Configuration
- STRATEGY_TYPE=${STRATEGY_TYPE:-enhanced}
- SYMBOL=${SYMBOL:-AAPL}
- TRADING_MODE=${TRADING_MODE:-paper}
# Risk Management
- MAX_POSITION_SIZE=${MAX_POSITION_SIZE:-0.95}
- RISK_PER_TRADE=${RISK_PER_TRADE:-0.02}
- MAX_DRAWDOWN_LIMIT=${MAX_DRAWDOWN_LIMIT:-0.15}
# Logging
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- TZ=UTC
volumes:
# Persist logs
- ./logs:/app/logs
# Optional: Mount config for easy updates
- ./config:/app/config:ro
# Resource limits for safety
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
# Health check
healthcheck:
test: ["CMD", "python3", "-c", "import sys; sys.path.append('src'); from src.data_handler import DataHandler; DataHandler().get_latest_price('AAPL')"]
interval: 60s
timeout: 30s
retries: 3
start_period: 30s
# Command override based on mode
command: >
sh -c "
if [ '${TRADING_MODE}' = 'live' ]; then
echo '⚠️ LIVE TRADING MODE - USE WITH EXTREME CAUTION ⚠️';
sleep 10;
python3 main.py --mode live --strategy ${STRATEGY_TYPE:-enhanced};
else
echo '📄 PAPER TRADING MODE (Safe)';
python3 main.py --mode paper --strategy ${STRATEGY_TYPE:-enhanced};
fi
"
# Optional: Monitoring service
monitoring:
image: alpine:latest
container_name: trading-monitor
restart: unless-stopped
depends_on:
- trading-bot
volumes:
- ./logs:/logs:ro
command: >
sh -c "
while true; do
echo '📊 Trading System Status - $(date)';
echo '📈 Latest Performance:';
tail -5 /logs/performance.log 2>/dev/null || echo 'No performance data yet';
echo '🔄 Latest Trades:';
tail -3 /logs/trades.log 2>/dev/null || echo 'No trades yet';
echo '⚠️ Latest Warnings:';
tail -3 /logs/risk.log 2>/dev/null || echo 'No risk events';
echo '----------------------------------------';
sleep 300; # Check every 5 minutes
done
"
networks:
default:
name: trading-network

60
main.py
View File

@ -2,6 +2,7 @@
""" """
Main Trading Application Main Trading Application
Entry point for the automated trading system. Entry point for the automated trading system.
Supports Docker deployment with health monitoring.
""" """
import sys import sys
@ -22,6 +23,7 @@ from config.trading_config import trading_config
# Global variables for graceful shutdown # Global variables for graceful shutdown
running = True running = True
trading_engine = None trading_engine = None
last_health_check = datetime.now()
def signal_handler(signum, frame): def signal_handler(signum, frame):
"""Handle shutdown signals gracefully""" """Handle shutdown signals gracefully"""
@ -30,6 +32,26 @@ def signal_handler(signum, frame):
logger.info(f"Received signal {signum}, shutting down gracefully...") logger.info(f"Received signal {signum}, shutting down gracefully...")
running = False running = False
def health_check():
"""Perform health check for Docker monitoring"""
global last_health_check
try:
from src.data_handler import DataHandler
data_handler = DataHandler()
# Quick health check - just verify we can get market data
price = data_handler.get_latest_price(trading_config.symbol)
last_health_check = datetime.now()
# Write health status to file for Docker monitoring
with open('/tmp/health', 'w') as f:
f.write(f"OK:{datetime.now().isoformat()}:{price}")
return True
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Health check failed: {e}")
return False
def run_backtesting_mode(strategy_type=None): def run_backtesting_mode(strategy_type=None):
"""Run backtesting using historical data""" """Run backtesting using historical data"""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -228,20 +250,28 @@ def display_backtest_results(results, strategy_name):
print("="*60) print("="*60)
def run_live_trading_mode(): def run_live_trading_mode():
"""Run live trading mode""" """Run live trading mode with Docker health monitoring"""
global trading_engine, running global trading_engine, running
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info("Starting live trading mode") logger.info("🚀 Starting live trading mode for 24/7 operation")
try: try:
# Initialize trading engine # Initialize trading engine
trading_engine = TradingEngine(paper_trading=True) trading_engine = TradingEngine(paper_trading=True)
# Track start time for uptime monitoring
start_time = datetime.now()
# Main trading loop # Main trading loop
cycle_count = 0 cycle_count = 0
last_summary_time = datetime.now() last_summary_time = datetime.now()
# Initial health check
health_check()
logger.info("📊 Entering main trading loop...")
while running: while running:
try: try:
# Run trading cycle # Run trading cycle
@ -250,7 +280,7 @@ def run_live_trading_mode():
# Log cycle results # Log cycle results
if cycle_results['success']: if cycle_results['success']:
logger.info(f"Cycle {cycle_count} completed successfully") logger.info(f"Cycle {cycle_count} completed successfully")
if cycle_results['actions_taken']: if cycle_results['actions_taken']:
log_trading_action("trading_cycle", { log_trading_action("trading_cycle", {
@ -260,8 +290,9 @@ def run_live_trading_mode():
'signals': cycle_results['signals'] 'signals': cycle_results['signals']
}) })
# Log performance metrics periodically # Log performance metrics periodically and health check
if datetime.now() - last_summary_time > timedelta(hours=1): if datetime.now() - last_summary_time > timedelta(hours=1):
# Performance logging
summary = trading_engine.get_trading_summary() summary = trading_engine.get_trading_summary()
if summary['current_position'] > 0 and summary['entry_price']: if summary['current_position'] > 0 and summary['entry_price']:
current_profit = (cycle_results['current_price'] / summary['entry_price'] - 1) * 100 current_profit = (cycle_results['current_price'] / summary['entry_price'] - 1) * 100
@ -270,16 +301,25 @@ def run_live_trading_mode():
'entry_price': summary['entry_price'], 'entry_price': summary['entry_price'],
'current_price': cycle_results['current_price'] 'current_price': cycle_results['current_price']
}) })
# Health check for Docker monitoring
health_check()
last_summary_time = datetime.now() last_summary_time = datetime.now()
# Docker deployment status
logger.info(f"🐳 Docker Status - Cycle: {cycle_count}, Uptime: {datetime.now() - start_time}")
else: else:
logger.error(f"Cycle {cycle_count} failed") logger.error(f"Cycle {cycle_count} failed")
# Wait before next cycle (3600 seconds = 1 hour for hourly timeframe) # Wait before next cycle (3600 seconds = 1 hour for hourly timeframe)
sleep_duration = 3600 # 1 hour sleep_duration = 3600 # 1 hour
for _ in range(sleep_duration): # Health check every 5 minutes during sleep
for i in range(sleep_duration):
if not running: if not running:
break break
if i % 300 == 0: # Every 5 minutes
health_check()
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
@ -300,12 +340,14 @@ def main():
# Setup command line arguments # Setup command line arguments
parser = argparse.ArgumentParser(description='Automated Trading System') parser = argparse.ArgumentParser(description='Automated Trading System')
parser.add_argument('--mode', choices=['live', 'backtest'], default='backtest', parser.add_argument('--mode', choices=['live', 'paper', 'backtest'], default='backtest',
help='Trading mode: live or backtest (default: backtest)') help='Trading mode: live, paper, or backtest (default: backtest)')
parser.add_argument('--strategy', choices=['conservative', 'enhanced'], parser.add_argument('--strategy', choices=['conservative', 'enhanced'],
default=None, help='Strategy type (default: from config)') default=None, help='Strategy type (default: from config)')
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
default='INFO', help='Logging level (default: INFO)') default='INFO', help='Logging level (default: INFO)')
parser.add_argument('--days', type=int, default=180,
help='Days of historical data for backtesting (default: 180)')
args = parser.parse_args() args = parser.parse_args()
@ -330,7 +372,9 @@ def main():
try: try:
if args.mode == 'backtest': if args.mode == 'backtest':
run_backtesting_mode(args.strategy) run_backtesting_mode(args.strategy)
elif args.mode == 'live': elif args.mode in ['live', 'paper']:
# Both live and paper use the same function - paper is safer
logger.info(f"🛡️ Running in {'PAPER' if args.mode == 'paper' else 'LIVE'} trading mode")
run_live_trading_mode() run_live_trading_mode()
except Exception as e: except Exception as e:

112
monitor.py Normal file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
Trading System Monitor
Real-time monitoring for Docker deployment.
"""
import time
import os
from datetime import datetime, timedelta
def monitor_system():
"""Monitor the trading system performance"""
print("🔍 Trading System Monitor - Starting...")
print("=" * 60)
while True:
try:
current_time = datetime.now()
print(f"\n📊 System Status - {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
print("-" * 40)
# Check health file
health_status = "❌ UNKNOWN"
if os.path.exists('/tmp/health'):
try:
with open('/tmp/health', 'r') as f:
health_data = f.read().strip()
if health_data.startswith('OK:'):
parts = health_data.split(':')
health_time = datetime.fromisoformat(parts[1])
price = parts[2] if len(parts) > 2 else "N/A"
# Check if health is recent (within 10 minutes)
if current_time - health_time < timedelta(minutes=10):
health_status = f"✅ HEALTHY (Price: ${price})"
else:
health_status = f"⚠️ STALE ({(current_time - health_time).seconds//60}m ago)"
else:
health_status = "❌ ERROR"
except Exception as e:
health_status = f"❌ ERROR: {e}"
print(f"Health Status: {health_status}")
# Check log files
log_files = {
'trading.log': 'General',
'trades.log': 'Trades',
'risk.log': 'Risk Events',
'performance.log': 'Performance'
}
for log_file, description in log_files.items():
log_path = f'/app/logs/{log_file}'
if os.path.exists(log_path):
try:
stat = os.stat(log_path)
size_mb = stat.st_size / (1024 * 1024)
mod_time = datetime.fromtimestamp(stat.st_mtime)
age_minutes = (current_time - mod_time).seconds // 60
print(f"{description:12}: {size_mb:.1f}MB, {age_minutes}m ago")
except Exception as e:
print(f"{description:12}: Error - {e}")
else:
print(f"{description:12}: Not found")
# Show recent performance if available
perf_file = '/app/logs/performance.log'
if os.path.exists(perf_file):
try:
with open(perf_file, 'r') as f:
lines = f.readlines()
if lines:
recent_lines = lines[-3:] # Last 3 entries
print("\n📈 Recent Performance:")
for line in recent_lines:
if line.strip():
print(f" {line.strip()}")
except Exception as e:
print(f"Performance read error: {e}")
# Show recent trades if available
trades_file = '/app/logs/trades.log'
if os.path.exists(trades_file):
try:
with open(trades_file, 'r') as f:
lines = f.readlines()
if lines:
recent_trades = [line for line in lines[-5:] if 'BUY' in line or 'SELL' in line]
if recent_trades:
print("\n💰 Recent Trades:")
for trade in recent_trades[-2:]: # Last 2 trades
print(f" {trade.strip()}")
except Exception as e:
print(f"Trades read error: {e}")
print("\n" + "=" * 60)
# Wait 5 minutes before next check
time.sleep(300)
except KeyboardInterrupt:
print("\n👋 Monitor stopped by user")
break
except Exception as e:
print(f"❌ Monitor error: {e}")
time.sleep(60) # Wait 1 minute on error
if __name__ == "__main__":
monitor_system()