浏览代码

Add web UI configuration options and update dependencies

- Introduced new environment variables for web UI configuration, including WEB_ENABLED, WEB_HOST, WEB_PORT, WEB_API_KEY, and WEB_CORS_ORIGINS.
- Updated pyproject.toml to include new dependencies: fastapi, uvicorn, jinja2, python-multipart, websockets, and sse-starlette for web functionality.
- Enhanced configuration validation to ensure proper web settings are provided when enabled.
- Updated documentation to include web UI setup instructions.
Carles Sentis 3 天之前
父节点
当前提交
1c89efc157

+ 16 - 0
config/env.example

@@ -96,6 +96,22 @@ TELEGRAM_CHAT_ID=your_chat_id_here
 # Enable/disable Telegram integration
 TELEGRAM_ENABLED=true
 
+# ========================================
+# Web UI Configuration
+# ========================================
+# Enable/disable Web UI
+WEB_ENABLED=true
+
+# Web server host and port
+WEB_HOST=127.0.0.1
+WEB_PORT=8080
+
+# API key for web authentication (generate a secure random string)
+WEB_API_KEY=your_secure_api_key_here
+
+# CORS origins (comma-separated list)
+WEB_CORS_ORIGINS=http://localhost:8080,http://127.0.0.1:8080
+
 # ========================================
 # Custom Keyboard Configuration
 # ========================================

+ 4 - 0
docs/documentation-index.md

@@ -8,6 +8,9 @@
 ## 📱 **Daily Usage** 
 **[📖 Commands Reference](commands.md)** - Complete command guide with examples and mobile optimization
 
+## 🌐 **Web Interface**
+**[📖 Web UI Setup](web-ui-setup.md)** - Web dashboard setup and configuration guide
+
 ## 🏗️ **Technical Reference**
 **[📖 Project Structure](project-structure.md)** - Detailed architecture and module breakdown  
 **[📖 System Integration](system-integration.md)** - Advanced integration and customization  
@@ -22,6 +25,7 @@
 |-------|---------|---------------|----------|
 | **[setup.md](setup.md)** | **5-minute installation** | 5 minutes | All users |
 | **[commands.md](commands.md)** | **Daily trading commands** | 10 minutes | Traders |
+| **[web-ui-setup.md](web-ui-setup.md)** | **Web dashboard setup** | 10 minutes | All users |
 | **[project-structure.md](project-structure.md)** | **Code architecture** | 15 minutes | Developers |
 | **[system-integration.md](system-integration.md)** | **Advanced setup** | 20 minutes | Advanced users |
 | **[position-notifications.md](position-notifications.md)** | **Position tracking features** | 10 minutes | Traders & Developers |

+ 207 - 0
docs/web-ui-setup.md

@@ -0,0 +1,207 @@
+# Web UI Setup Guide
+
+This guide covers setting up and running the Hyperliquid Trading Bot Web UI (Phase 1 implementation).
+
+## 🚀 Quick Start
+
+### 1. Install Dependencies
+
+The web UI requires additional FastAPI dependencies that have been added to `pyproject.toml`:
+
+```bash
+# If using uv
+uv install
+
+# If using pip
+pip install -e .
+```
+
+### 2. Configure Environment
+
+Add the following web UI settings to your `.env` file:
+
+```bash
+# Web UI Configuration
+WEB_ENABLED=true
+WEB_HOST=127.0.0.1
+WEB_PORT=8080
+WEB_API_KEY=your_secure_api_key_here
+WEB_CORS_ORIGINS=http://localhost:8080,http://127.0.0.1:8080
+```
+
+**Important**: Generate a secure API key for `WEB_API_KEY`. This will be used for authentication.
+
+### 3. Start the Web Server
+
+Run the web server alongside your existing Telegram bot:
+
+```bash
+python web_start.py
+```
+
+The web UI will be available at: `http://127.0.0.1:8080`
+
+## 📊 Dashboard Features (Phase 1)
+
+### Current Implementation
+
+✅ **Account Summary**
+- Account balance
+- Total P&L and return percentage
+- Win rate and profit factor
+- Open positions count
+- Max drawdown
+
+✅ **Live Positions**
+- Open positions with real-time P&L
+- Entry price, mark price, leverage
+- Side (long/short) indicators
+
+✅ **Recent Trades**
+- Last 10 completed trades
+- P&L and ROE for each trade
+- Trade timestamps
+
+✅ **Market Overview**
+- Real-time prices for major tokens (BTC, ETH, SOL, DOGE)
+- 24-hour change percentages
+
+✅ **Responsive Design**
+- Mobile-friendly interface
+- Tailwind CSS styling
+- Auto-refresh functionality
+
+### Authentication
+
+The web UI uses simple Bearer token authentication with the API key you configure. Enter your API key in the dashboard to access the data.
+
+### Copy Trading Tab Features
+
+The copy trading tab provides comprehensive functionality including:
+
+#### 4. Enhanced Account Analysis (NEW)
+- **Clickable Analysis Results**: All account addresses in analysis results and leaderboard are now clickable
+- **Deep Dive Analysis**: Click any address to open a detailed analysis modal showing:
+  - **Account Overview**: Address, analysis period, last trade, trading type
+  - **Score & Recommendation**: 0-100 relative score with color-coded recommendation
+  - **Current Positions**: Real-time positions with P&L, leverage, and position details
+  - **Performance Metrics**: Total P&L, win rate, trades, duration, drawdown, profit factor
+  - **Trading Patterns**: Frequency, leverage usage, token diversity, directional capability
+  - **Trading Style**: Pattern analysis and top traded tokens
+  - **Recent Trading Activity**: Last 10 trades with timestamps
+  - **Copy Trading Evaluation**: Specific evaluation points and copyability assessment
+  - **Recommendations**: Portfolio allocation suggestions and leverage limits
+  - **Quick Actions**: "Set as Copy Target" button for recommended accounts
+
+#### Enhanced User Experience
+- **Responsive Design**: Modal adapts to screen size with scrollable content
+- **Visual Indicators**: Emojis and color coding for quick interpretation
+- **Interactive Elements**: Clickable addresses throughout the interface
+- **Real-time Data**: Fresh analysis with current positions and recent trades
+- **Smart Recommendations**: Context-aware suggestions based on account performance
+
+## 🔧 Technical Details
+
+### Architecture
+
+- **Backend**: FastAPI with async support
+- **Frontend**: Vanilla JavaScript + Alpine.js + Tailwind CSS
+- **Database**: Uses existing SQLite database
+- **Real-time**: Auto-refresh every 30 seconds
+
+### File Structure
+
+```
+src/web/
+├── __init__.py
+├── app.py                 # Main FastAPI application
+├── dependencies.py        # Authentication and dependencies
+├── routers/
+│   ├── __init__.py
+│   └── dashboard.py       # Dashboard API endpoints
+├── templates/
+│   ├── base.html         # Base template
+│   └── dashboard.html    # Dashboard page
+└── static/
+    ├── css/
+    │   └── app.css       # Custom styles
+    └── js/
+        └── app.js        # JavaScript functionality
+```
+
+### API Endpoints
+
+- `GET /` - Dashboard page
+- `GET /api/dashboard/summary` - Account summary data
+- `GET /api/dashboard/positions` - Current positions
+- `GET /api/dashboard/recent-trades` - Recent completed trades
+- `GET /api/dashboard/market-overview` - Market data
+- `GET /health` - Health check
+
+## 🚀 Running with Telegram Bot
+
+You can run both the Telegram bot and Web UI simultaneously:
+
+**Terminal 1** (Telegram Bot):
+```bash
+python trading_bot.py
+```
+
+**Terminal 2** (Web UI):
+```bash
+python web_start.py
+```
+
+Both interfaces will access the same database and trading engine data.
+
+## 🔍 Troubleshooting
+
+### Common Issues
+
+1. **Port already in use**
+   - Change `WEB_PORT` in your `.env` file
+   - Or stop the process using the port: `lsof -ti:8080 | xargs kill`
+
+2. **API key authentication fails**
+   - Ensure `WEB_API_KEY` is set in `.env`
+   - Enter the correct API key in the dashboard
+
+3. **Data not loading**
+   - Check that your trading bot has data in the database
+   - Verify API key is correctly entered
+   - Check browser console for errors
+
+4. **Configuration validation errors**
+   - Ensure all required environment variables are set
+   - Check that `WEB_ENABLED=true`
+
+### Logs
+
+Web UI logs are integrated with the existing logging system. Check:
+- Console output when running `web_start.py`
+- Log files in the `logs/` directory
+
+## 🛣️ Roadmap
+
+### Phase 2 (Next)
+- Trading interface (place orders, close positions)
+- Position management controls
+- Real-time updates via WebSockets
+
+### Phase 3
+- Copy trading dashboard and controls
+- Target trader monitoring
+- Copy trading performance metrics
+
+### Phase 4
+- Advanced analytics and charts
+- Performance visualizations
+- Export functionality
+
+## 🤝 Support
+
+If you encounter issues:
+1. Check the troubleshooting section above
+2. Review logs for error messages
+3. Ensure your `.env` configuration is correct
+4. Verify the Telegram bot is working properly first 

+ 6 - 0
pyproject.toml

@@ -13,6 +13,12 @@ dependencies = [
     "aiohttp",
     "aiofiles",
     "hyperliquid",
+    "fastapi>=0.104.0",
+    "uvicorn[standard]>=0.24.0",
+    "jinja2>=3.1.0",
+    "python-multipart>=0.0.6",
+    "websockets>=12.0",
+    "sse-starlette>=1.6.0",
 ]
 
 [build-system]

+ 31 - 1
src/config/config.py

@@ -56,6 +56,13 @@ class Config:
     # Bot settings
     BOT_HEARTBEAT_SECONDS: int = int(os.getenv('BOT_HEARTBEAT_SECONDS', '5'))
     
+    # Web UI Configuration
+    WEB_ENABLED: bool = get_bool_env('WEB_ENABLED', 'true')
+    WEB_HOST: str = os.getenv('WEB_HOST', '127.0.0.1')
+    WEB_PORT: int = int(os.getenv('WEB_PORT', '8080'))
+    WEB_API_KEY: Optional[str] = os.getenv('WEB_API_KEY')
+    WEB_CORS_ORIGINS: str = os.getenv('WEB_CORS_ORIGINS', 'http://localhost:8080,http://127.0.0.1:8080')
+    
     # Removed market monitor settings - simplified architecture
     
     # Order settings
@@ -81,7 +88,8 @@ class Config:
             cls._validate_bot_settings,
             cls._validate_rsi,
             cls._validate_copy_trading,
-            cls._validate_logging
+            cls._validate_logging,
+            cls._validate_web
         ]
         return all(validator() for validator in validators)
 
@@ -192,6 +200,28 @@ class Config:
             logger.error("❌ LOG_ROTATION_TYPE must be 'size' or 'time'")
             return False
         return True
+
+    @classmethod
+    def _validate_web(cls) -> bool:
+        """Validate web configuration."""
+        if not cls.WEB_ENABLED:
+            return True  # Skip validation if web is disabled
+        
+        if not cls.WEB_HOST:
+            logger.error("❌ WEB_HOST is required when WEB_ENABLED is enabled")
+            return False
+        if not cls.WEB_PORT:
+            logger.error("❌ WEB_PORT is required when WEB_ENABLED is enabled")
+            return False
+        if cls.WEB_PORT < 1 or cls.WEB_PORT > 65535:
+            logger.error("❌ WEB_PORT must be between 1 and 65535")
+            return False
+        if not cls.WEB_API_KEY:
+            logger.warning("⚠️ WEB_API_KEY not set - web interface will require manual authentication")
+        if not cls.WEB_CORS_ORIGINS:
+            logger.warning("⚠️ WEB_CORS_ORIGINS not set - using default CORS settings")
+        
+        return True
     
     @classmethod
     def get_hyperliquid_config(cls) -> dict:

+ 8 - 0
src/web/__init__.py

@@ -0,0 +1,8 @@
+"""
+Web UI Module for Hyperliquid Trading Bot
+
+Provides a FastAPI-based web interface for trading operations,
+position monitoring, copy trading, and performance analytics.
+"""
+
+__version__ = "1.0.0" 

+ 150 - 0
src/web/app.py

@@ -0,0 +1,150 @@
+"""
+FastAPI Web Application for Hyperliquid Trading Bot
+
+Provides web interface for:
+- Dashboard with real-time trading data
+- Analytics with comprehensive trading statistics
+- Copy trading management and account analysis
+"""
+
+import logging
+import os
+from pathlib import Path
+from fastapi import FastAPI, Request, HTTPException
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import HTMLResponse
+
+from src.config.config import Config
+from .dependencies import init_dependencies
+from .routers import dashboard_router, analytics_router, copy_trading_router
+
+logger = logging.getLogger(__name__)
+
+# Get the web module directory
+WEB_DIR = Path(__file__).parent
+TEMPLATES_DIR = WEB_DIR / "templates"
+STATIC_DIR = WEB_DIR / "static"
+
+# Ensure directories exist
+TEMPLATES_DIR.mkdir(exist_ok=True)
+STATIC_DIR.mkdir(exist_ok=True)
+(STATIC_DIR / "css").mkdir(exist_ok=True)
+(STATIC_DIR / "js").mkdir(exist_ok=True)
+(STATIC_DIR / "img").mkdir(exist_ok=True)
+
+
+def create_app() -> FastAPI:
+    """Create and configure the FastAPI application."""
+    
+    app = FastAPI(
+        title="Hyperliquid Trading Bot Web Interface",
+        description="Web interface for managing and monitoring Hyperliquid trading operations",
+        version="1.0.0",
+        docs_url="/docs" if Config.WEB_ENABLED else None,
+        redoc_url="/redoc" if Config.WEB_ENABLED else None
+    )
+    
+    # Add CORS middleware
+    if Config.WEB_CORS_ORIGINS:
+        origins = [origin.strip() for origin in Config.WEB_CORS_ORIGINS.split(",")]
+        app.add_middleware(
+            CORSMiddleware,
+            allow_origins=origins,
+            allow_credentials=True,
+            allow_methods=["*"],
+            allow_headers=["*"],
+        )
+    
+    # Mount static files
+    app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
+    
+    # Initialize templates
+    templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
+    
+    # Include routers
+    app.include_router(dashboard_router, prefix="/api")
+    app.include_router(analytics_router, prefix="/api")
+    app.include_router(copy_trading_router, prefix="/api")
+    
+    # Root route - Dashboard
+    @app.get("/", response_class=HTMLResponse)
+    async def dashboard_page(request: Request):
+        """Serve the main dashboard page."""
+        try:
+            return templates.TemplateResponse(
+                "dashboard.html",
+                {
+                    "request": request,
+                    "title": "Dashboard",
+                    "config": {
+                        "api_base": "",
+                        "default_token": Config.DEFAULT_TRADING_TOKEN,
+                        "testnet": Config.HYPERLIQUID_TESTNET
+                    }
+                }
+            )
+        except Exception as e:
+            logger.error(f"Error serving dashboard: {e}")
+            raise HTTPException(status_code=500, detail="Error loading dashboard")
+    
+    # Analytics page
+    @app.get("/analytics", response_class=HTMLResponse)
+    async def analytics_page(request: Request):
+        """Serve the analytics page."""
+        try:
+            return templates.TemplateResponse(
+                "analytics.html",
+                {
+                    "request": request,
+                    "title": "Analytics",
+                    "config": {
+                        "api_base": "",
+                        "default_token": Config.DEFAULT_TRADING_TOKEN,
+                        "testnet": Config.HYPERLIQUID_TESTNET
+                    }
+                }
+            )
+        except Exception as e:
+            logger.error(f"Error serving analytics: {e}")
+            raise HTTPException(status_code=500, detail="Error loading analytics")
+    
+    # Copy trading page
+    @app.get("/copy-trading", response_class=HTMLResponse)
+    async def copy_trading_page(request: Request):
+        """Serve the copy trading management page."""
+        try:
+            return templates.TemplateResponse(
+                "copy_trading.html",
+                {
+                    "request": request,
+                    "title": "Copy Trading",
+                    "config": {
+                        "api_base": "",
+                        "default_token": Config.DEFAULT_TRADING_TOKEN,
+                        "testnet": Config.HYPERLIQUID_TESTNET
+                    }
+                }
+            )
+        except Exception as e:
+            logger.error(f"Error serving copy trading: {e}")
+            raise HTTPException(status_code=500, detail="Error loading copy trading")
+    
+    # Health check endpoint
+    @app.get("/health")
+    async def health_check():
+        """Health check endpoint."""
+        return {
+            "status": "healthy",
+            "service": "hyperliquid-bot-web"
+        }
+    
+    logger.info("✅ FastAPI application created successfully")
+    return app
+
+
+def initialize_app_dependencies(trading_engine, monitoring_coordinator):
+    """Initialize the application with dependencies."""
+    init_dependencies(trading_engine, monitoring_coordinator)
+    logger.info("✅ App dependencies initialized") 

+ 91 - 0
src/web/dependencies.py

@@ -0,0 +1,91 @@
+"""
+FastAPI Dependencies for Web UI
+
+Handles authentication, database access, and other shared resources.
+"""
+
+import logging
+from typing import Optional, Annotated
+from fastapi import Depends, HTTPException, status, Header
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+
+from src.config.config import Config
+from src.trading.trading_engine import TradingEngine
+from src.monitoring.monitoring_coordinator import MonitoringCoordinator
+from src.clients.hyperliquid_client import HyperliquidClient
+from src.stats.trading_stats import TradingStats
+
+logger = logging.getLogger(__name__)
+
+# Security scheme
+security = HTTPBearer()
+
+# Global instances (will be initialized in app.py)
+trading_engine: Optional[TradingEngine] = None
+monitoring_coordinator: Optional[MonitoringCoordinator] = None
+
+
+def init_dependencies(engine: TradingEngine, coordinator: MonitoringCoordinator):
+    """Initialize global dependencies."""
+    global trading_engine, monitoring_coordinator
+    trading_engine = engine
+    monitoring_coordinator = coordinator
+    logger.info("✅ Web dependencies initialized")
+
+
+async def verify_api_key(
+    credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
+) -> str:
+    """Verify API key authentication."""
+    if not Config.WEB_API_KEY:
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="API key not configured"
+        )
+    
+    if credentials.credentials != Config.WEB_API_KEY:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Invalid API key",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+    
+    return credentials.credentials
+
+
+async def get_trading_engine() -> TradingEngine:
+    """Get the trading engine instance."""
+    if trading_engine is None:
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="Trading engine not initialized"
+        )
+    return trading_engine
+
+
+async def get_monitoring_coordinator() -> MonitoringCoordinator:
+    """Get the monitoring coordinator instance."""
+    if monitoring_coordinator is None:
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="Monitoring coordinator not initialized"
+        )
+    return monitoring_coordinator
+
+
+async def get_client() -> HyperliquidClient:
+    """Get the Hyperliquid client instance."""
+    engine = await get_trading_engine()
+    return engine.client
+
+
+async def get_stats() -> TradingStats:
+    """Get the TradingStats instance."""
+    engine = await get_trading_engine()
+    return engine.get_stats()
+
+
+# Dependency aliases for convenience
+AuthenticatedUser = Annotated[str, Depends(verify_api_key)]
+TradingEngineInstance = Annotated[TradingEngine, Depends(get_trading_engine)]
+MonitoringInstance = Annotated[MonitoringCoordinator, Depends(get_monitoring_coordinator)] 

+ 11 - 0
src/web/routers/__init__.py

@@ -0,0 +1,11 @@
+"""
+API Routers for Web UI
+
+Contains all the API endpoints organized by functionality.
+"""
+
+from .dashboard import router as dashboard_router
+from .analytics import router as analytics_router
+from .copy_trading import router as copy_trading_router
+
+__all__ = ["dashboard_router", "analytics_router", "copy_trading_router"] 

+ 353 - 0
src/web/routers/analytics.py

@@ -0,0 +1,353 @@
+import logging
+from fastapi import APIRouter, Depends, HTTPException, Query
+from typing import List, Optional, Dict, Any
+from pydantic import BaseModel
+from datetime import datetime
+
+from src.stats.trading_stats import TradingStats
+from ..dependencies import get_stats
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter(prefix="/analytics", tags=["analytics"])
+
+# Pydantic models for response data
+class OverallStats(BaseModel):
+    balance: float
+    initial_balance: float
+    total_pnl: float
+    total_return_pct: float
+    win_rate: float
+    profit_factor: float
+    total_trades: int
+    total_wins: int
+    total_losses: int
+    expectancy: float
+    avg_trade_pnl: float
+    avg_win_pnl: float
+    avg_loss_pnl: float
+    largest_win: float
+    largest_loss: float
+    largest_win_token: str
+    largest_loss_token: str
+    max_drawdown: float
+    max_drawdown_pct: float
+    best_token: str
+    best_token_pnl: float
+    worst_token: str
+    worst_token_pnl: float
+    total_volume: float
+
+class TokenStats(BaseModel):
+    token: str
+    total_pnl: float
+    win_rate: float
+    profit_factor: float
+    total_trades: int
+    winning_trades: int
+    losing_trades: int
+    roe_percentage: float
+    entry_volume: float
+    exit_volume: float
+    avg_duration: str
+    largest_win: float
+    largest_loss: float
+
+class PeriodStats(BaseModel):
+    period: str
+    period_formatted: str
+    has_trades: bool
+    pnl: float
+    pnl_pct: float
+    roe: float
+    trades: int
+    volume: float
+
+class PerformanceMetrics(BaseModel):
+    sharpe_ratio: Optional[float]
+    max_consecutive_wins: int
+    max_consecutive_losses: int
+    avg_trade_duration: str
+    best_roe_trade: Optional[Dict[str, Any]]
+    worst_roe_trade: Optional[Dict[str, Any]]
+    volatility: float
+    risk_metrics: Dict[str, Any]
+
+@router.get("/stats", response_model=OverallStats)
+async def get_overall_stats(
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get overall trading statistics - equivalent to /stats command."""
+    try:
+        # Get performance stats
+        perf_stats = stats.get_performance_stats()
+        basic_stats = stats.get_basic_stats()
+        
+        return OverallStats(
+            balance=basic_stats.get('current_balance', 0.0),
+            initial_balance=perf_stats.get('initial_balance', 0.0),
+            total_pnl=perf_stats.get('total_pnl', 0.0),
+            total_return_pct=(perf_stats.get('total_pnl', 0.0) / max(perf_stats.get('initial_balance', 1.0), 1.0) * 100),
+            win_rate=perf_stats.get('win_rate', 0.0),
+            profit_factor=perf_stats.get('profit_factor', 0.0),
+            total_trades=perf_stats.get('total_trades', 0),
+            total_wins=perf_stats.get('total_wins', 0),
+            total_losses=perf_stats.get('total_losses', 0),
+            expectancy=perf_stats.get('expectancy', 0.0),
+            avg_trade_pnl=perf_stats.get('avg_trade_pnl', 0.0),
+            avg_win_pnl=perf_stats.get('avg_win_pnl', 0.0),
+            avg_loss_pnl=perf_stats.get('avg_loss_pnl', 0.0),
+            largest_win=perf_stats.get('largest_win', 0.0),
+            largest_loss=perf_stats.get('largest_loss', 0.0),
+            largest_win_token=perf_stats.get('largest_win_token', 'N/A'),
+            largest_loss_token=perf_stats.get('largest_loss_token', 'N/A'),
+            max_drawdown=perf_stats.get('max_drawdown', 0.0),
+            max_drawdown_pct=perf_stats.get('max_drawdown_pct', 0.0),
+            best_token=perf_stats.get('best_token', 'N/A'),
+            best_token_pnl=perf_stats.get('best_token_pnl', 0.0),
+            worst_token=perf_stats.get('worst_token', 'N/A'),
+            worst_token_pnl=perf_stats.get('worst_token_pnl', 0.0),
+            total_volume=perf_stats.get('total_entry_volume', 0.0)
+        )
+    except Exception as e:
+        logger.error(f"Error getting overall stats: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting overall stats: {str(e)}")
+
+
+@router.get("/stats/{token}", response_model=TokenStats)
+async def get_token_stats(
+    token: str,
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get detailed statistics for a specific token - equivalent to /stats {token} command."""
+    try:
+        token_data = stats.get_token_detailed_stats(token.upper())
+        
+        if not token_data or token_data.get('summary_total_trades', 0) == 0:
+            raise HTTPException(
+                status_code=404, 
+                detail=f"No trading data found for {token.upper()}"
+            )
+        
+        perf_summary = token_data.get('performance_summary', {})
+        
+        return TokenStats(
+            token=token.upper(),
+            total_pnl=perf_summary.get('total_pnl', 0.0),
+            win_rate=perf_summary.get('win_rate', 0.0),
+            profit_factor=perf_summary.get('profit_factor', 0.0),
+            total_trades=perf_summary.get('completed_trades', 0),
+            winning_trades=perf_summary.get('total_wins', 0),
+            losing_trades=perf_summary.get('total_losses', 0),
+            roe_percentage=(perf_summary.get('total_pnl', 0.0) / max(perf_summary.get('completed_entry_volume', 1.0), 1.0) * 100),
+            entry_volume=perf_summary.get('completed_entry_volume', 0.0),
+            exit_volume=perf_summary.get('completed_exit_volume', 0.0),
+            avg_duration=perf_summary.get('avg_trade_duration', 'N/A'),
+            largest_win=perf_summary.get('largest_win', 0.0),
+            largest_loss=perf_summary.get('largest_loss', 0.0)
+        )
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"Error getting token stats for {token}: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting token stats: {str(e)}")
+
+
+@router.get("/performance", response_model=List[TokenStats])
+async def get_performance_ranking(
+    limit: int = Query(20, ge=1, le=50),
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get performance ranking of all tokens - equivalent to /performance command."""
+    try:
+        token_performance = stats.get_token_performance(limit=limit)
+        
+        result = []
+        for token_data in token_performance:
+            result.append(TokenStats(
+                token=token_data['token'],
+                total_pnl=token_data.get('total_realized_pnl', 0.0),
+                win_rate=token_data.get('win_rate', 0.0),
+                profit_factor=token_data.get('profit_factor', 0.0),
+                total_trades=token_data.get('total_completed_cycles', 0),
+                winning_trades=token_data.get('winning_cycles', 0),
+                losing_trades=token_data.get('losing_cycles', 0),
+                roe_percentage=token_data.get('roe_percentage', 0.0),
+                entry_volume=token_data.get('total_entry_volume', 0.0),
+                exit_volume=token_data.get('total_exit_volume', 0.0),
+                avg_duration=token_data.get('average_trade_duration_formatted', 'N/A'),
+                largest_win=token_data.get('largest_winning_cycle_pnl', 0.0),
+                largest_loss=token_data.get('largest_losing_cycle_pnl', 0.0)
+            ))
+        
+        return result
+    except Exception as e:
+        logger.error(f"Error getting performance ranking: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting performance ranking: {str(e)}")
+
+
+@router.get("/daily", response_model=List[PeriodStats])
+async def get_daily_stats(
+    limit: int = Query(10, ge=1, le=30),
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get daily performance stats - equivalent to /daily command."""
+    try:
+        daily_stats = stats.get_daily_stats(limit=limit)
+        
+        result = []
+        for day_data in daily_stats:
+            result.append(PeriodStats(
+                period=day_data['date'],
+                period_formatted=day_data['date_formatted'],
+                has_trades=day_data['has_trades'],
+                pnl=day_data['pnl'],
+                pnl_pct=day_data['pnl_pct'],
+                roe=day_data['roe'],
+                trades=day_data['trades'],
+                volume=day_data.get('volume', 0.0)
+            ))
+        
+        return result
+    except Exception as e:
+        logger.error(f"Error getting daily stats: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting daily stats: {str(e)}")
+
+
+@router.get("/weekly", response_model=List[PeriodStats])
+async def get_weekly_stats(
+    limit: int = Query(10, ge=1, le=20),
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get weekly performance stats - equivalent to /weekly command."""
+    try:
+        weekly_stats = stats.get_weekly_stats(limit=limit)
+        
+        result = []
+        for week_data in weekly_stats:
+            result.append(PeriodStats(
+                period=week_data['week'],
+                period_formatted=week_data['week_formatted'],
+                has_trades=week_data['has_trades'],
+                pnl=week_data['pnl'],
+                pnl_pct=week_data['pnl_pct'],
+                roe=week_data.get('roe', 0.0),
+                trades=week_data['trades'],
+                volume=week_data['volume']
+            ))
+        
+        return result
+    except Exception as e:
+        logger.error(f"Error getting weekly stats: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting weekly stats: {str(e)}")
+
+
+@router.get("/monthly", response_model=List[PeriodStats])
+async def get_monthly_stats(
+    limit: int = Query(12, ge=1, le=24),
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get monthly performance stats - equivalent to /monthly command."""
+    try:
+        monthly_stats = stats.get_monthly_stats(limit=limit)
+        
+        result = []
+        for month_data in monthly_stats:
+            result.append(PeriodStats(
+                period=month_data['month'],
+                period_formatted=month_data['month_formatted'],
+                has_trades=month_data['has_trades'],
+                pnl=month_data['pnl'],
+                pnl_pct=month_data['pnl_pct'],
+                roe=month_data.get('roe', 0.0),
+                trades=month_data['trades'],
+                volume=month_data['volume']
+            ))
+        
+        return result
+    except Exception as e:
+        logger.error(f"Error getting monthly stats: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting monthly stats: {str(e)}")
+
+
+@router.get("/metrics", response_model=PerformanceMetrics)
+async def get_performance_metrics(
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get advanced performance metrics including risk analysis."""
+    try:
+        perf_stats = stats.get_performance_stats()
+        risk_metrics = stats.performance_calculator.get_risk_metrics()
+        
+        # Calculate additional metrics
+        best_roe = perf_stats.get('best_roe_trade', {})
+        worst_roe = perf_stats.get('worst_roe_trade', {})
+        
+        return PerformanceMetrics(
+            sharpe_ratio=risk_metrics.get('sharpe_ratio'),
+            max_consecutive_wins=0,  # TODO: Implement
+            max_consecutive_losses=0,  # TODO: Implement
+            avg_trade_duration="N/A",  # TODO: Calculate from stats
+            best_roe_trade=best_roe if best_roe else None,
+            worst_roe_trade=worst_roe if worst_roe else None,
+            volatility=0.0,  # TODO: Calculate
+            risk_metrics=risk_metrics
+        )
+    except Exception as e:
+        logger.error(f"Error getting performance metrics: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting performance metrics: {str(e)}")
+
+
+@router.get("/balance-history")
+async def get_balance_history(
+    days: int = Query(30, ge=1, le=365),
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get balance history for charting."""
+    try:
+        balance_data, balance_stats = stats.performance_calculator.get_balance_history(days=days)
+        
+        return {
+            "balance_history": balance_data,
+            "stats": balance_stats,
+            "days": days
+        }
+    except Exception as e:
+        logger.error(f"Error getting balance history: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting balance history: {str(e)}")
+
+
+@router.get("/summary")
+async def get_analytics_summary(
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get analytics summary for overview page."""
+    try:
+        # Get key metrics
+        perf_stats = stats.get_performance_stats()
+        daily_stats = stats.get_daily_stats(limit=7)
+        top_tokens = stats.get_token_performance(limit=5)
+        
+        # Calculate recent trend
+        recent_pnl = sum(day['pnl'] for day in daily_stats if day['has_trades'])
+        recent_trades = sum(day['trades'] for day in daily_stats if day['has_trades'])
+        
+        return {
+            "performance_overview": {
+                "total_pnl": perf_stats.get('total_pnl', 0.0),
+                "win_rate": perf_stats.get('win_rate', 0.0),
+                "profit_factor": perf_stats.get('profit_factor', 0.0),
+                "total_trades": perf_stats.get('total_trades', 0),
+                "max_drawdown_pct": perf_stats.get('max_drawdown_pct', 0.0)
+            },
+            "recent_performance": {
+                "pnl_7d": recent_pnl,
+                "trades_7d": recent_trades,
+                "daily_avg": recent_pnl / 7 if recent_pnl else 0.0
+            },
+            "top_tokens": top_tokens[:3],  # Top 3 performers
+            "last_updated": datetime.now().isoformat()
+        }
+    except Exception as e:
+        logger.error(f"Error getting analytics summary: {e}")
+        raise HTTPException(status_code=500, detail=f"Error getting analytics summary: {str(e)}") 

+ 606 - 0
src/web/routers/copy_trading.py

@@ -0,0 +1,606 @@
+"""
+Copy Trading API Router
+
+Provides endpoints for:
+- Copy trading status and control
+- Account analysis functionality
+- Target trader evaluation
+"""
+
+from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
+from pydantic import BaseModel, Field
+from typing import List, Optional, Dict, Any
+from datetime import datetime
+import asyncio
+import logging
+
+# Import our existing classes
+from ...monitoring.copy_trading_monitor import CopyTradingMonitor
+from ...monitoring.copy_trading_state import CopyTradingStateManager
+from ...clients.hyperliquid_client import HyperliquidClient
+from ...notifications.notification_manager import NotificationManager
+from ...config.config import Config
+
+# Import the account analyzer utility
+import sys
+import os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'utils'))
+from hyperliquid_account_analyzer import HyperliquidAccountAnalyzer, AccountStats
+
+router = APIRouter(prefix="/copy-trading", tags=["copy-trading"])
+logger = logging.getLogger(__name__)
+
+# Pydantic Models
+class CopyTradingStatus(BaseModel):
+    enabled: bool
+    target_address: Optional[str]
+    portfolio_percentage: float
+    copy_mode: str
+    max_leverage: float
+    target_positions: int
+    our_positions: int
+    tracked_positions: int
+    copied_trades: int
+    session_start_time: Optional[datetime]
+    session_duration_hours: Optional[float]
+    last_check: Optional[datetime]
+
+class CopyTradingConfig(BaseModel):
+    target_address: str = Field(..., description="Target trader address to copy")
+    portfolio_percentage: float = Field(default=0.1, ge=0.01, le=0.5, description="Portfolio percentage (1-50%)")
+    copy_mode: str = Field(default="FIXED", description="Copy mode: FIXED or PROPORTIONAL")
+    max_leverage: float = Field(default=10.0, ge=1.0, le=20.0, description="Maximum leverage (1-20x)")
+    min_position_size: float = Field(default=25.0, ge=10.0, description="Minimum position size in USD")
+    execution_delay: float = Field(default=2.0, ge=0.0, le=10.0, description="Execution delay in seconds")
+    notifications_enabled: bool = Field(default=True, description="Enable notifications")
+
+class AccountAnalysisRequest(BaseModel):
+    addresses: List[str] = Field(..., description="List of addresses to analyze")
+    limit: Optional[int] = Field(default=10, ge=1, le=50, description="Limit results")
+
+class AccountStatsResponse(BaseModel):
+    address: str
+    total_pnl: float
+    win_rate: float
+    total_trades: int
+    avg_trade_duration_hours: float
+    max_drawdown: float
+    avg_position_size: float
+    max_leverage_used: float
+    avg_leverage_used: float
+    trading_frequency_per_day: float
+    risk_reward_ratio: float
+    profit_factor: float
+    active_positions: int
+    current_drawdown: float
+    last_trade_timestamp: int
+    analysis_period_days: int
+    is_copyable: bool
+    copyability_reason: str
+    unique_tokens_traded: int
+    trading_type: str
+    top_tokens: List[str]
+    short_percentage: float
+    trading_style: str
+    buy_sell_ratio: float
+    relative_score: Optional[float] = None
+
+class LeaderboardRequest(BaseModel):
+    window: str = Field(default="7d", description="Time window: 1d, 7d, 30d, allTime")
+    limit: int = Field(default=10, ge=1, le=50, description="Number of top accounts")
+
+class BalanceTestResponse(BaseModel):
+    our_balance: float
+    target_balance: float
+    portfolio_percentage: float
+    test_price: float
+    test_leverage: float
+    margin_to_use: float
+    position_value: float
+    token_amount: float
+    min_position_size: float
+    would_execute: bool
+    config_enabled: bool
+    state_enabled: bool
+    error: Optional[str] = None
+
+class SingleAccountAnalysisRequest(BaseModel):
+    address: str = Field(..., description="Address to analyze in detail")
+
+# Global instances
+copy_trading_monitor: Optional[CopyTradingMonitor] = None
+state_manager = CopyTradingStateManager()
+
+def get_copy_trading_monitor():
+    """Get or create copy trading monitor instance"""
+    global copy_trading_monitor
+    if copy_trading_monitor is None:
+        try:
+            config = Config()
+            client = HyperliquidClient()
+            notification_manager = NotificationManager()
+            copy_trading_monitor = CopyTradingMonitor(client, notification_manager)
+        except Exception as e:
+            logger.error(f"Failed to create copy trading monitor: {e}")
+            raise HTTPException(status_code=500, detail="Failed to initialize copy trading system")
+    return copy_trading_monitor
+
+@router.get("/status", response_model=CopyTradingStatus)
+async def get_copy_trading_status():
+    """Get current copy trading status"""
+    try:
+        monitor = get_copy_trading_monitor()
+        status = monitor.get_status()
+        
+        return CopyTradingStatus(
+            enabled=status['enabled'],
+            target_address=status['target_address'],
+            portfolio_percentage=status['portfolio_percentage'],
+            copy_mode=status['copy_mode'],
+            max_leverage=status['max_leverage'],
+            target_positions=status['target_positions'],
+            our_positions=status['our_positions'],
+            tracked_positions=status['tracked_positions'],
+            copied_trades=status['copied_trades'],
+            session_start_time=status['session_start_time'],
+            session_duration_hours=status['session_duration_hours'],
+            last_check=status['last_check']
+        )
+    except Exception as e:
+        logger.error(f"Error getting copy trading status: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/start")
+async def start_copy_trading(config: CopyTradingConfig, background_tasks: BackgroundTasks):
+    """Start copy trading with the specified configuration"""
+    try:
+        monitor = get_copy_trading_monitor()
+        
+        # Update configuration
+        monitor.target_address = config.target_address
+        monitor.portfolio_percentage = config.portfolio_percentage
+        monitor.copy_mode = config.copy_mode
+        monitor.max_leverage = config.max_leverage
+        monitor.min_position_size = config.min_position_size
+        monitor.execution_delay = config.execution_delay
+        monitor.notifications_enabled = config.notifications_enabled
+        monitor.enabled = True
+        
+        # Start monitoring in background
+        background_tasks.add_task(monitor.start_monitoring)
+        
+        return {"message": "Copy trading started successfully", "target_address": config.target_address}
+    except Exception as e:
+        logger.error(f"Error starting copy trading: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/stop")
+async def stop_copy_trading():
+    """Stop copy trading"""
+    try:
+        monitor = get_copy_trading_monitor()
+        await monitor.stop_monitoring()
+        
+        return {"message": "Copy trading stopped successfully"}
+    except Exception as e:
+        logger.error(f"Error stopping copy trading: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/config")
+async def get_copy_trading_config():
+    """Get current copy trading configuration"""
+    try:
+        config = Config()
+        return {
+            "target_address": config.COPY_TRADING_TARGET_ADDRESS,
+            "portfolio_percentage": config.COPY_TRADING_PORTFOLIO_PERCENTAGE,
+            "copy_mode": config.COPY_TRADING_MODE,
+            "max_leverage": config.COPY_TRADING_MAX_LEVERAGE,
+            "min_position_size": config.COPY_TRADING_MIN_POSITION_SIZE,
+            "execution_delay": config.COPY_TRADING_EXECUTION_DELAY,
+            "notifications_enabled": config.COPY_TRADING_NOTIFICATIONS
+        }
+    except Exception as e:
+        logger.error(f"Error getting copy trading config: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/test-balance", response_model=BalanceTestResponse)
+async def test_balance_fetching():
+    """Test balance fetching and position sizing for debugging"""
+    try:
+        monitor = get_copy_trading_monitor()
+        result = await monitor.test_balance_fetching()
+        
+        if 'error' in result:
+            return BalanceTestResponse(
+                our_balance=0,
+                target_balance=0,
+                portfolio_percentage=0,
+                test_price=0,
+                test_leverage=0,
+                margin_to_use=0,
+                position_value=0,
+                token_amount=0,
+                min_position_size=0,
+                would_execute=False,
+                config_enabled=False,
+                state_enabled=False,
+                error=result['error']
+            )
+        
+        return BalanceTestResponse(**result)
+    except Exception as e:
+        logger.error(f"Error testing balance: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/analyze-accounts", response_model=List[AccountStatsResponse])
+async def analyze_accounts(request: AccountAnalysisRequest):
+    """Analyze multiple Hyperliquid accounts for copy trading suitability"""
+    try:
+        async with HyperliquidAccountAnalyzer() as analyzer:
+            results = await analyzer.analyze_multiple_accounts(request.addresses)
+            
+            if not results:
+                return []
+            
+            # Calculate relative scores
+            def calculate_relative_score(stats: AccountStats, all_stats: List[AccountStats]) -> float:
+                """Calculate relative score for ranking accounts"""
+                score = 0.0
+                
+                # Copyability (35% weight)
+                if stats.trading_frequency_per_day > 50:
+                    score += 0  # HFT bots get 0
+                elif stats.trading_frequency_per_day < 1:
+                    score += 5  # Inactive accounts get 5
+                elif 1 <= stats.trading_frequency_per_day <= 20:
+                    ideal_freq = 15
+                    freq_distance = abs(stats.trading_frequency_per_day - ideal_freq)
+                    score += max(0, 35 - (freq_distance * 1.5))
+                else:
+                    score += 15  # Questionable frequency
+                
+                # Profitability (30% weight)
+                if stats.total_pnl < 0:
+                    pnl_range = max([s.total_pnl for s in all_stats]) - min([s.total_pnl for s in all_stats])
+                    if pnl_range > 0:
+                        loss_severity = abs(stats.total_pnl) / pnl_range
+                        score += -15 * loss_severity
+                    else:
+                        score += -15
+                elif stats.total_pnl == 0:
+                    score += 0
+                else:
+                    max_pnl = max([s.total_pnl for s in all_stats if s.total_pnl > 0], default=1)
+                    score += (stats.total_pnl / max_pnl) * 30
+                
+                # Risk management (20% weight)
+                if stats.max_drawdown > 0.5:
+                    score += -10
+                elif stats.max_drawdown > 0.25:
+                    score += -5
+                elif stats.max_drawdown > 0.15:
+                    score += 5
+                elif stats.max_drawdown > 0.05:
+                    score += 15
+                else:
+                    score += 20
+                
+                # Account maturity (10% weight)
+                if stats.analysis_period_days < 7:
+                    score += 0
+                elif stats.analysis_period_days < 14:
+                    score += 2
+                elif stats.analysis_period_days < 30:
+                    score += 5
+                else:
+                    max_age = max([s.analysis_period_days for s in all_stats])
+                    age_ratio = min(1.0, (stats.analysis_period_days - 30) / max(1, max_age - 30))
+                    score += 7 + (age_ratio * 3)
+                
+                # Win rate (5% weight)
+                win_rates = [s.win_rate for s in all_stats]
+                if max(win_rates) > min(win_rates):
+                    winrate_normalized = (stats.win_rate - min(win_rates)) / (max(win_rates) - min(win_rates))
+                    score += winrate_normalized * 5
+                else:
+                    score += 2.5
+                
+                return score
+            
+            # Convert to response format with relative scores
+            response_list = []
+            for stats in results:
+                relative_score = calculate_relative_score(stats, results)
+                response_list.append(AccountStatsResponse(
+                    address=stats.address,
+                    total_pnl=stats.total_pnl,
+                    win_rate=stats.win_rate,
+                    total_trades=stats.total_trades,
+                    avg_trade_duration_hours=stats.avg_trade_duration_hours,
+                    max_drawdown=stats.max_drawdown,
+                    avg_position_size=stats.avg_position_size,
+                    max_leverage_used=stats.max_leverage_used,
+                    avg_leverage_used=stats.avg_leverage_used,
+                    trading_frequency_per_day=stats.trading_frequency_per_day,
+                    risk_reward_ratio=stats.risk_reward_ratio,
+                    profit_factor=stats.profit_factor,
+                    active_positions=stats.active_positions,
+                    current_drawdown=stats.current_drawdown,
+                    last_trade_timestamp=stats.last_trade_timestamp,
+                    analysis_period_days=stats.analysis_period_days,
+                    is_copyable=stats.is_copyable,
+                    copyability_reason=stats.copyability_reason,
+                    unique_tokens_traded=stats.unique_tokens_traded,
+                    trading_type=stats.trading_type,
+                    top_tokens=stats.top_tokens,
+                    short_percentage=stats.short_percentage,
+                    trading_style=stats.trading_style,
+                    buy_sell_ratio=stats.buy_sell_ratio,
+                    relative_score=relative_score
+                ))
+            
+            # Sort by relative score
+            response_list.sort(key=lambda x: x.relative_score or 0, reverse=True)
+            
+            return response_list[:request.limit]
+            
+    except Exception as e:
+        logger.error(f"Error analyzing accounts: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/get-leaderboard", response_model=List[str])
+async def get_leaderboard_accounts(request: LeaderboardRequest):
+    """Get top accounts from curated leaderboard"""
+    try:
+        async with HyperliquidAccountAnalyzer() as analyzer:
+            addresses = await analyzer.get_top_accounts_from_leaderboard(request.window, request.limit)
+            return addresses or []
+    except Exception as e:
+        logger.error(f"Error getting leaderboard: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/session-info")
+async def get_session_info():
+    """Get detailed session information"""
+    try:
+        session_info = state_manager.get_session_info()
+        return session_info
+    except Exception as e:
+        logger.error(f"Error getting session info: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/reset-state")
+async def reset_copy_trading_state():
+    """Reset copy trading state (use with caution)"""
+    try:
+        state_manager.reset_state()
+        return {"message": "Copy trading state reset successfully"}
+    except Exception as e:
+        logger.error(f"Error resetting state: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/analyze-single-account", response_model=Dict[str, Any])
+async def analyze_single_account(request: SingleAccountAnalysisRequest):
+    """Get detailed analysis for a single account including current positions and recent trades"""
+    try:
+        async with HyperliquidAccountAnalyzer() as analyzer:
+            # Get the account stats
+            stats = await analyzer.analyze_account(request.address)
+            
+            if not stats:
+                raise HTTPException(status_code=404, detail="Account not found or no trading data")
+            
+            # Get current positions and recent trades
+            account_state = await analyzer.get_account_state(request.address)
+            recent_fills = await analyzer.get_user_fills(request.address, limit=20)
+            
+            # Parse current positions
+            current_positions = []
+            if account_state:
+                positions = analyzer.parse_positions(account_state)
+                for pos in positions:
+                    side_emoji = "📈" if pos.side == "long" else "📉"
+                    pnl_emoji = "✅" if pos.unrealized_pnl >= 0 else "❌"
+                    
+                    current_positions.append({
+                        "coin": pos.coin,
+                        "side": pos.side,
+                        "side_emoji": side_emoji,
+                        "size": pos.size,
+                        "entry_price": pos.entry_price,
+                        "mark_price": pos.mark_price,
+                        "unrealized_pnl": pos.unrealized_pnl,
+                        "pnl_emoji": pnl_emoji,
+                        "leverage": pos.leverage,
+                        "margin_used": pos.margin_used,
+                        "position_value": abs(pos.size * pos.mark_price)
+                    })
+            
+            # Parse recent trades
+            recent_trades = []
+            if recent_fills:
+                trades = analyzer.parse_trades(recent_fills)
+                for trade in trades[-10:]:  # Last 10 trades
+                    side_emoji = "🟢" if trade.side == "buy" else "🔴"
+                    trade_time = datetime.fromtimestamp(trade.timestamp / 1000)
+                    
+                    recent_trades.append({
+                        "coin": trade.coin,
+                        "side": trade.side,
+                        "side_emoji": side_emoji,
+                        "size": trade.size,
+                        "price": trade.price,
+                        "value": trade.size * trade.price,
+                        "fee": trade.fee,
+                        "timestamp": trade_time.strftime('%Y-%m-%d %H:%M:%S'),
+                        "is_maker": trade.is_maker
+                    })
+            
+            # Calculate total position value and unrealized PnL
+            total_position_value = sum(pos["position_value"] for pos in current_positions)
+            total_unrealized_pnl = sum(pos["unrealized_pnl"] for pos in current_positions)
+            
+            # Convert stats to dict and add extra details
+            stats_dict = {
+                "address": stats.address,
+                "total_pnl": stats.total_pnl,
+                "win_rate": stats.win_rate,
+                "total_trades": stats.total_trades,
+                "avg_trade_duration_hours": stats.avg_trade_duration_hours,
+                "max_drawdown": stats.max_drawdown,
+                "avg_position_size": stats.avg_position_size,
+                "max_leverage_used": stats.max_leverage_used,
+                "avg_leverage_used": stats.avg_leverage_used,
+                "trading_frequency_per_day": stats.trading_frequency_per_day,
+                "risk_reward_ratio": stats.risk_reward_ratio,
+                "profit_factor": stats.profit_factor,
+                "active_positions": stats.active_positions,
+                "current_drawdown": stats.current_drawdown,
+                "last_trade_timestamp": stats.last_trade_timestamp,
+                "analysis_period_days": stats.analysis_period_days,
+                "is_copyable": stats.is_copyable,
+                "copyability_reason": stats.copyability_reason,
+                "unique_tokens_traded": stats.unique_tokens_traded,
+                "trading_type": stats.trading_type,
+                "top_tokens": stats.top_tokens,
+                "short_percentage": stats.short_percentage,
+                "trading_style": stats.trading_style,
+                "buy_sell_ratio": stats.buy_sell_ratio
+            }
+            
+            # Calculate relative score
+            def calculate_single_account_score(stats):
+                score = 0.0
+                
+                # Copyability (35% weight)
+                if stats.trading_frequency_per_day > 50:
+                    score += 0
+                elif stats.trading_frequency_per_day < 1:
+                    score += 5
+                elif 1 <= stats.trading_frequency_per_day <= 20:
+                    ideal_freq = 15
+                    freq_distance = abs(stats.trading_frequency_per_day - ideal_freq)
+                    score += max(0, 35 - (freq_distance * 1.5))
+                else:
+                    score += 15
+                
+                # Profitability (30% weight)
+                if stats.total_pnl < 0:
+                    score += -15
+                elif stats.total_pnl == 0:
+                    score += 0
+                else:
+                    # Score based on PnL magnitude (assuming $10k is excellent)
+                    score += min(30, (stats.total_pnl / 10000) * 30)
+                
+                # Risk management (20% weight)
+                if stats.max_drawdown > 0.5:
+                    score += -10
+                elif stats.max_drawdown > 0.25:
+                    score += -5
+                elif stats.max_drawdown > 0.15:
+                    score += 5
+                elif stats.max_drawdown > 0.05:
+                    score += 15
+                else:
+                    score += 20
+                
+                # Account maturity (10% weight)
+                if stats.analysis_period_days < 7:
+                    score += 0
+                elif stats.analysis_period_days < 14:
+                    score += 2
+                elif stats.analysis_period_days < 30:
+                    score += 5
+                else:
+                    score += 7 + min(3, (stats.analysis_period_days - 30) / 30)
+                
+                # Win rate (5% weight)
+                score += stats.win_rate * 5
+                
+                return score
+            
+            relative_score = calculate_single_account_score(stats)
+            stats_dict["relative_score"] = relative_score
+            
+            # Determine recommendation
+            if relative_score >= 60:
+                recommendation = "🟢 HIGHLY RECOMMENDED"
+                portfolio_allocation = "10-25% (confident allocation)"
+                max_leverage_limit = "5-10x"
+            elif relative_score >= 40:
+                recommendation = "🟡 MODERATELY RECOMMENDED"
+                portfolio_allocation = "5-15% (moderate allocation)"
+                max_leverage_limit = "3-5x"
+            elif relative_score >= 20:
+                recommendation = "🟠 PROCEED WITH CAUTION"
+                portfolio_allocation = "2-5% (very small allocation)"
+                max_leverage_limit = "2-3x"
+            elif relative_score >= 0:
+                recommendation = "🔴 NOT RECOMMENDED"
+                portfolio_allocation = "DO NOT COPY (Risky)"
+                max_leverage_limit = "N/A"
+            else:
+                recommendation = "⛔ DANGEROUS"
+                portfolio_allocation = "DO NOT COPY (Negative Score)"
+                max_leverage_limit = "N/A"
+            
+            # Evaluation points
+            evaluation = []
+            is_hft_pattern = stats.trading_frequency_per_day > 50
+            is_copyable = 1 <= stats.trading_frequency_per_day <= 20
+            
+            if is_hft_pattern:
+                evaluation.append("❌ HFT/Bot pattern detected")
+            elif stats.trading_frequency_per_day < 1:
+                evaluation.append("❌ Too inactive for copy trading")
+            elif is_copyable:
+                evaluation.append("✅ Human-like trading pattern")
+            
+            if stats.total_pnl > 0:
+                evaluation.append("✅ Profitable track record")
+            else:
+                evaluation.append("❌ Not profitable")
+            
+            if stats.max_drawdown < 0.15:
+                evaluation.append("✅ Good risk management")
+            elif stats.max_drawdown < 0.25:
+                evaluation.append("⚠️ Moderate risk")
+            else:
+                evaluation.append("❌ High risk (excessive drawdown)")
+            
+            if 2 <= stats.avg_trade_duration_hours <= 48:
+                evaluation.append("✅ Suitable trade duration")
+            elif stats.avg_trade_duration_hours < 2:
+                evaluation.append("⚠️ Very short trades (scalping)")
+            else:
+                evaluation.append("⚠️ Long hold times")
+            
+            return {
+                "stats": stats_dict,
+                "current_positions": current_positions,
+                "recent_trades": recent_trades,
+                "position_summary": {
+                    "total_position_value": total_position_value,
+                    "total_unrealized_pnl": total_unrealized_pnl,
+                    "position_count": len(current_positions)
+                },
+                "recommendation": {
+                    "overall": recommendation,
+                    "portfolio_allocation": portfolio_allocation,
+                    "max_leverage_limit": max_leverage_limit,
+                    "evaluation_points": evaluation
+                },
+                "trading_type_display": {
+                    "perps": "🔄 Perpetuals",
+                    "spot": "💱 Spot Trading", 
+                    "mixed": "🔀 Mixed (Spot + Perps)",
+                    "unknown": "❓ Unknown"
+                }.get(stats.trading_type, f"❓ {stats.trading_type}"),
+                "buy_sell_ratio_display": "∞ (only buys)" if stats.buy_sell_ratio == float('inf') else "0 (only sells)" if stats.buy_sell_ratio == 0 else f"{stats.buy_sell_ratio:.2f}"
+            }
+            
+    except Exception as e:
+        logger.error(f"Error analyzing single account {request.address}: {e}")
+        raise HTTPException(status_code=500, detail=str(e)) 

+ 252 - 0
src/web/routers/dashboard.py

@@ -0,0 +1,252 @@
+"""
+Dashboard API Router
+
+Provides endpoints for dashboard overview data including account summary,
+positions, recent trades, and key performance metrics.
+"""
+
+import logging
+from datetime import datetime, timezone
+from typing import Dict, List, Any, Optional
+from fastapi import APIRouter, HTTPException, Depends
+from pydantic import BaseModel
+
+from ..dependencies import AuthenticatedUser, TradingEngineInstance, MonitoringInstance, get_client, get_stats
+from src.clients.hyperliquid_client import HyperliquidClient
+from src.stats.trading_stats import TradingStats
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter(prefix="/dashboard", tags=["dashboard"])
+
+
+class DashboardSummary(BaseModel):
+    """Dashboard summary response model."""
+    balance: float
+    total_pnl: float
+    win_rate: float
+    open_positions: int
+    total_return_pct: float
+    total_trades: int
+    profit_factor: float
+    max_drawdown_pct: float
+
+
+class Position(BaseModel):
+    """Position response model."""
+    symbol: str
+    side: str
+    size: float
+    entry_price: float
+    mark_price: float
+    unrealized_pnl: float
+    unrealized_pnl_pct: float
+
+
+class RecentTrade(BaseModel):
+    """Recent trade response model."""
+    symbol: str
+    side: str
+    size: float
+    price: float
+    realized_pnl: float
+    timestamp: str
+
+
+class MarketData(BaseModel):
+    symbol: str
+    price: float
+    change_24h: float
+    high_24h: float
+    low_24h: float
+
+
+@router.get("/summary", response_model=DashboardSummary)
+async def get_dashboard_summary(
+    client: HyperliquidClient = Depends(get_client),
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get dashboard summary data."""
+    try:
+        # Get current balance (fix: use get_balance instead of get_account_info)
+        balance_data = client.get_balance()
+        current_balance = 0.0
+        if balance_data and 'USDC' in balance_data:
+            current_balance = float(balance_data['USDC']['total'])
+        
+        # Get performance stats
+        perf_stats = stats.get_performance_stats()
+        
+        return DashboardSummary(
+            balance=current_balance,
+            total_pnl=perf_stats.get('total_pnl', 0.0),
+            win_rate=perf_stats.get('win_rate', 0.0),
+            open_positions=stats._get_open_positions_count_from_db(),
+            total_return_pct=perf_stats.get('total_pnl', 0.0) / max(perf_stats.get('initial_balance', 1.0), 1.0) * 100,
+            total_trades=perf_stats.get('total_trades', 0),
+            profit_factor=perf_stats.get('profit_factor', 0.0),
+            max_drawdown_pct=abs(perf_stats.get('max_drawdown_pct', 0.0))
+        )
+    except Exception as e:
+        logger.warning(f"Could not get current balance: {e}")
+        # Return stats without balance
+        perf_stats = stats.get_performance_stats()
+        return DashboardSummary(
+            balance=0.0,
+            total_pnl=perf_stats.get('total_pnl', 0.0),
+            win_rate=perf_stats.get('win_rate', 0.0),
+            open_positions=stats._get_open_positions_count_from_db(),
+            total_return_pct=perf_stats.get('total_pnl', 0.0) / max(perf_stats.get('initial_balance', 1.0), 1.0) * 100,
+            total_trades=perf_stats.get('total_trades', 0),
+            profit_factor=perf_stats.get('profit_factor', 0.0),
+            max_drawdown_pct=abs(perf_stats.get('max_drawdown_pct', 0.0))
+        )
+
+
+@router.get("/positions", response_model=List[Position])
+async def get_positions(
+    client: HyperliquidClient = Depends(get_client),
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get current positions."""
+    try:
+        # Fix: get_positions is not async, don't await it
+        positions = client.get_positions()
+        
+        if not positions:
+            return []
+        
+        result = []
+        for pos in positions:
+            if float(pos.get('contracts', 0)) != 0:  # Only open positions
+                unrealized_pnl = float(pos.get('unrealizedPnl', 0))
+                position_value = abs(float(pos.get('notional', 0)))
+                unrealized_pnl_pct = (unrealized_pnl / position_value * 100) if position_value > 0 else 0.0
+                
+                result.append(Position(
+                    symbol=pos.get('symbol', ''),
+                    side='long' if float(pos.get('contracts', 0)) > 0 else 'short',
+                    size=abs(float(pos.get('contracts', 0))),
+                    entry_price=float(pos.get('entryPrice', 0)),
+                    mark_price=float(pos.get('markPrice', 0)),
+                    unrealized_pnl=unrealized_pnl,
+                    unrealized_pnl_pct=unrealized_pnl_pct
+                ))
+        
+        return result
+    
+    except Exception as e:
+        logger.error(f"Error getting positions: {e}")
+        return []
+
+
+@router.get("/recent-trades", response_model=List[RecentTrade])
+async def get_recent_trades(
+    stats: TradingStats = Depends(get_stats)
+):
+    """Get recent completed trades."""
+    try:
+        # Fix: Query trades that have been closed (position_closed status)
+        # and use correct column names from the trades table
+        query = """
+        SELECT symbol, side, amount, price, realized_pnl, position_closed_at
+        FROM trades 
+        WHERE status = 'position_closed' 
+        ORDER BY position_closed_at DESC 
+        LIMIT 10
+        """
+        
+        trades = stats.db_manager._fetch_query(query)
+        
+        result = []
+        for trade in trades:
+            result.append(RecentTrade(
+                symbol=trade['symbol'],
+                side=trade['side'],
+                size=float(trade['amount'] or 0),
+                price=float(trade['price'] or 0),
+                realized_pnl=float(trade['realized_pnl'] or 0),
+                timestamp=trade['position_closed_at'] or ''
+            ))
+        
+        return result
+    
+    except Exception as e:
+        logger.error(f"Error getting recent trades: {e}")
+        return []
+
+
+@router.get("/market-overview", response_model=List[MarketData])
+async def get_market_overview(
+    client: HyperliquidClient = Depends(get_client)
+):
+    """Get market overview for major symbols."""
+    symbols = ['BTC/USDC:USDC', 'ETH/USDC:USDC', 'SOL/USDC:USDC', 'DOGE/USDC:USDC']
+    result = []
+    
+    for symbol in symbols:
+        try:
+            # Fix: get_market_data is not async, don't await it
+            market_data = client.get_market_data(symbol)
+            
+            if market_data and 'ticker' in market_data:
+                ticker = market_data['ticker']
+                
+                # Debug: Log the ticker structure
+                logger.debug(f"Ticker data for {symbol}: {ticker}")
+                
+                # Extract price data with robust None handling
+                current_price = 0.0
+                high_24h = 0.0
+                low_24h = 0.0
+                open_24h = 0.0
+                
+                # Get current price (required)
+                if ticker.get('last') is not None:
+                    current_price = float(ticker['last'])
+                elif ticker.get('close') is not None:
+                    current_price = float(ticker['close'])
+                else:
+                    logger.warning(f"No current price found for {symbol}")
+                    continue
+                
+                # Get high/low with fallbacks
+                if ticker.get('high') is not None:
+                    high_24h = float(ticker['high'])
+                else:
+                    high_24h = current_price
+                    
+                if ticker.get('low') is not None:
+                    low_24h = float(ticker['low'])
+                else:
+                    low_24h = current_price
+                    
+                if ticker.get('open') is not None:
+                    open_24h = float(ticker['open'])
+                else:
+                    open_24h = current_price
+                
+                # Calculate 24h change percentage
+                change_24h = 0.0
+                if open_24h > 0 and current_price != open_24h:
+                    change_24h = ((current_price - open_24h) / open_24h * 100)
+                
+                # Use symbol display name (just the base asset)
+                display_symbol = symbol.split('/')[0]
+                
+                result.append(MarketData(
+                    symbol=display_symbol,
+                    price=current_price,
+                    change_24h=change_24h,
+                    high_24h=high_24h,
+                    low_24h=low_24h
+                ))
+                
+                logger.debug(f"Added market data for {display_symbol}: price={current_price}, change={change_24h:.2f}%")
+            else:
+                logger.warning(f"Could not get market data for {symbol}: no ticker data")
+                
+        except Exception as e:
+            logger.warning(f"Could not get market data for {symbol}: {e}")
+    
+    return result 

+ 57 - 0
src/web/static/css/app.css

@@ -0,0 +1,57 @@
+/**
+ * Custom CSS for Hyperliquid Trading Bot Web UI
+ * 
+ * Additional styles to complement Tailwind CSS
+ */
+
+/* Custom loading animations */
+.pulse-loading {
+    animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+    0%, 100% {
+        opacity: 1;
+    }
+    50% {
+        opacity: 0.5;
+    }
+}
+
+/* Custom scrollbar for webkit browsers */
+::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+}
+
+::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+/* Print styles */
+@media print {
+    .no-print {
+        display: none !important;
+    }
+}
+
+/* Mobile-specific improvements */
+@media (max-width: 640px) {
+    .metric-card {
+        padding: 1rem;
+    }
+    
+    .card {
+        padding: 1rem;
+    }
+} 

+ 376 - 0
src/web/static/js/app.js

@@ -0,0 +1,376 @@
+/**
+ * Main Application JavaScript
+ * 
+ * Contains global utilities, dashboard functionality, and initialization code.
+ */
+
+// Global utilities
+window.utils = {
+    formatCurrency: function(value, decimals = 2) {
+        return new Intl.NumberFormat('en-US', {
+            style: 'currency',
+            currency: 'USD',
+            minimumFractionDigits: decimals,
+            maximumFractionDigits: decimals
+        }).format(value);
+    },
+    
+    formatPercent: function(value, decimals = 2) {
+        return (value > 0 ? '+' : '') + value.toFixed(decimals) + '%';
+    },
+    
+    formatTime: function(isoString) {
+        if (!isoString) return '--';
+        return new Date(isoString).toLocaleTimeString();
+    },
+    
+    formatDate: function(isoString) {
+        if (!isoString) return '--';
+        return new Date(isoString).toLocaleDateString();
+    },
+    
+    showToast: function(message, type = 'info') {
+        const container = document.getElementById('toast-container');
+        const toast = document.createElement('div');
+        const bgColor = {
+            'success': 'bg-green-500',
+            'error': 'bg-red-500',
+            'warning': 'bg-yellow-500',
+            'info': 'bg-blue-500'
+        }[type] || 'bg-blue-500';
+        
+        toast.className = `${bgColor} text-white px-4 py-3 rounded-lg shadow-lg mb-3 transform transition-all duration-300`;
+        toast.innerHTML = `
+            <div class="flex items-center">
+                <span>${message}</span>
+                <button onclick="this.parentElement.parentElement.remove()" class="ml-4 text-white hover:text-gray-200">
+                    <i class="fas fa-times"></i>
+                </button>
+            </div>
+        `;
+        
+        container.appendChild(toast);
+        
+        // Auto remove after 5 seconds
+        setTimeout(() => {
+            if (toast.parentElement) {
+                toast.remove();
+            }
+        }, 5000);
+    },
+    
+    makeApiRequest: async function(endpoint, options = {}) {
+        const config = window.APP_CONFIG || {};
+        const url = (config.apiBase || '') + endpoint;
+        
+        const defaultOptions = {
+            headers: {
+                'Content-Type': 'application/json',
+                'Authorization': `Bearer ${localStorage.getItem('apiKey') || ''}`
+            }
+        };
+        
+        const finalOptions = { ...defaultOptions, ...options };
+        if (finalOptions.body && typeof finalOptions.body === 'object') {
+            finalOptions.body = JSON.stringify(finalOptions.body);
+        }
+        
+        try {
+            const response = await fetch(url, finalOptions);
+            
+            if (!response.ok) {
+                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+            }
+            
+            return await response.json();
+        } catch (error) {
+            console.error('API Request failed:', error);
+            this.showToast(`API Error: ${error.message}`, 'error');
+            throw error;
+        }
+    }
+};
+
+// Dashboard functionality
+window.dashboard = {
+    data: {
+        summary: null,
+        positions: [],
+        recentTrades: [],
+        marketOverview: null,
+        lastUpdate: null
+    },
+    
+    async loadAll() {
+        try {
+            const [summary, positions, recentTrades, marketOverview] = await Promise.all([
+                utils.makeApiRequest('/api/dashboard/summary'),
+                utils.makeApiRequest('/api/dashboard/positions'),
+                utils.makeApiRequest('/api/dashboard/recent-trades'),
+                utils.makeApiRequest('/api/dashboard/market-overview')
+            ]);
+            
+            this.data = {
+                summary,
+                positions,
+                recentTrades,
+                marketOverview,
+                lastUpdate: new Date().toISOString()
+            };
+            
+            this.render();
+            
+        } catch (error) {
+            console.error('Failed to load dashboard data:', error);
+            utils.showToast('Failed to load dashboard data', 'error');
+        }
+    },
+    
+    render() {
+        this.renderSummary();
+        this.renderPositions();
+        this.renderRecentTrades();
+        this.renderMarketOverview();
+    },
+    
+    renderSummary() {
+        const summary = this.data.summary;
+        if (!summary) return;
+        
+        const summaryCards = document.querySelectorAll('[data-summary]');
+        summaryCards.forEach(card => {
+            const field = card.dataset.summary;
+            const element = card.querySelector('[data-value]');
+            
+            if (element && summary[field] !== undefined) {
+                let value = summary[field];
+                
+                // Map field names to match new API response
+                const fieldMap = {
+                    'account_balance': 'balance',
+                    'open_positions_count': 'open_positions'
+                };
+                
+                const mappedField = fieldMap[field] || field;
+                if (summary[mappedField] !== undefined) {
+                    value = summary[mappedField];
+                }
+                
+                // Format based on field type
+                if (field.includes('pnl') || field.includes('balance')) {
+                    value = utils.formatCurrency(value);
+                    // Add color classes for P&L
+                    if (field.includes('pnl')) {
+                        element.className = value >= 0 ? 'text-profit' : 'text-loss';
+                    }
+                } else if (field.includes('pct') || field.includes('rate')) {
+                    value = utils.formatPercent(value);
+                    // Add color classes for percentages
+                    element.className = value >= 0 ? 'text-profit' : 'text-loss';
+                } else if (field.includes('count') || field.includes('trades') || field.includes('positions')) {
+                    value = value.toLocaleString();
+                }
+                
+                element.textContent = value;
+            }
+        });
+    },
+    
+    renderPositions() {
+        const container = document.getElementById('positions-container');
+        if (!container) return;
+        
+        const positions = this.data.positions;
+        
+        if (!positions || positions.length === 0) {
+            container.innerHTML = `
+                <div class="text-center py-8 text-gray-500">
+                    <i class="fas fa-chart-line text-4xl mb-4"></i>
+                    <p>No open positions</p>
+                </div>
+            `;
+            return;
+        }
+        
+        container.innerHTML = positions.map(position => `
+            <div class="metric-card">
+                <div class="flex justify-between items-start mb-2">
+                    <h3 class="font-semibold text-lg">${position.symbol}</h3>
+                    <span class="px-2 py-1 rounded text-xs font-medium ${
+                        position.side === 'long' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
+                    }">
+                        ${position.side.toUpperCase()}
+                    </span>
+                </div>
+                <div class="space-y-1 text-sm">
+                    <div class="flex justify-between">
+                        <span class="text-gray-600">Size:</span>
+                        <span class="font-medium">${position.size.toFixed(6)}</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span class="text-gray-600">Entry:</span>
+                        <span class="font-medium">${utils.formatCurrency(position.entry_price)}</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span class="text-gray-600">Mark:</span>
+                        <span class="font-medium">${utils.formatCurrency(position.mark_price)}</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span class="text-gray-600">P&L:</span>
+                        <span class="${position.unrealized_pnl >= 0 ? 'text-profit' : 'text-loss'}">
+                            ${utils.formatCurrency(position.unrealized_pnl)} (${utils.formatPercent(position.unrealized_pnl_pct)})
+                        </span>
+                    </div>
+                </div>
+            </div>
+        `).join('');
+    },
+    
+    renderRecentTrades() {
+        const container = document.getElementById('recent-trades-container');
+        if (!container) return;
+        
+        const trades = this.data.recentTrades;
+        
+        if (!trades || trades.length === 0) {
+            container.innerHTML = `
+                <div class="text-center py-8 text-gray-500">
+                    <i class="fas fa-history text-4xl mb-4"></i>
+                    <p>No recent trades</p>
+                </div>
+            `;
+            return;
+        }
+        
+        container.innerHTML = trades.map(trade => `
+            <div class="flex justify-between items-center py-3 border-b border-gray-100 last:border-b-0">
+                <div class="flex items-center space-x-3">
+                    <span class="px-2 py-1 rounded text-xs font-medium ${
+                        trade.side === 'buy' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
+                    }">
+                        ${trade.side.toUpperCase()}
+                    </span>
+                    <span class="font-medium">${trade.symbol}</span>
+                    <span class="text-sm text-gray-600">${trade.size.toFixed(6)}</span>
+                </div>
+                <div class="text-right">
+                    <div class="${trade.realized_pnl >= 0 ? 'text-profit' : 'text-loss'} font-medium">
+                        ${utils.formatCurrency(trade.realized_pnl)}
+                    </div>
+                    <div class="text-xs text-gray-500">
+                        ${utils.formatTime(trade.timestamp)}
+                    </div>
+                </div>
+            </div>
+        `).join('');
+    },
+    
+    renderMarketOverview() {
+        const container = document.getElementById('market-overview-container');
+        if (!container) return;
+        
+        const market = this.data.marketOverview;
+        if (!market || !Array.isArray(market)) return;
+        
+        container.innerHTML = market.map(data => `
+            <div class="metric-card">
+                <div class="text-center">
+                    <h3 class="font-semibold text-lg mb-1">${data.symbol}</h3>
+                    <div class="text-xl font-bold">${utils.formatCurrency(data.price)}</div>
+                    <div class="${data.change_24h >= 0 ? 'text-profit' : 'text-loss'} text-sm">
+                        ${utils.formatPercent(data.change_24h)} 24h
+                    </div>
+                    <div class="text-xs text-gray-500 mt-1">
+                        H: ${utils.formatCurrency(data.high_24h)} / L: ${utils.formatCurrency(data.low_24h)}
+                    </div>
+                </div>
+            </div>
+        `).join('');
+    }
+};
+
+// API Key management
+window.auth = {
+    setApiKey(key) {
+        localStorage.setItem('apiKey', key);
+    },
+    
+    getApiKey() {
+        return localStorage.getItem('apiKey');
+    },
+    
+    clearApiKey() {
+        localStorage.removeItem('apiKey');
+    },
+    
+    isAuthenticated() {
+        return !!this.getApiKey();
+    }
+};
+
+// Update last update time
+function updateLastUpdateTime() {
+    document.getElementById('last-update').textContent = 
+        'Last update: ' + new Date().toLocaleTimeString();
+}
+
+// Auto-refresh functionality
+let autoRefreshInterval = null;
+
+function startAutoRefresh(intervalMs = 30000) {
+    if (autoRefreshInterval) {
+        clearInterval(autoRefreshInterval);
+    }
+    
+    autoRefreshInterval = setInterval(() => {
+        if (window.dashboard && typeof window.dashboard.loadAll === 'function') {
+            dashboard.loadAll();
+        }
+        updateLastUpdateTime();
+    }, intervalMs);
+}
+
+function stopAutoRefresh() {
+    if (autoRefreshInterval) {
+        clearInterval(autoRefreshInterval);
+        autoRefreshInterval = null;
+    }
+}
+
+// Initialize application
+document.addEventListener('DOMContentLoaded', function() {
+    console.log('Hyperliquid Trading Bot Web UI initialized');
+    
+    // Update time display
+    updateLastUpdateTime();
+    
+    // Check if we're on the dashboard page
+    if (typeof window.dashboard !== 'undefined') {
+        // Load initial data
+        dashboard.loadAll();
+        
+        // Start auto-refresh
+        startAutoRefresh(30000); // 30 seconds
+        
+        // Add refresh button handlers
+        const refreshButton = document.getElementById('refresh-dashboard');
+        if (refreshButton) {
+            refreshButton.addEventListener('click', () => {
+                dashboard.loadAll();
+            });
+        }
+    }
+    
+    // Handle API key input
+    const apiKeyInput = document.getElementById('api-key-input');
+    if (apiKeyInput) {
+        const savedKey = auth.getApiKey();
+        if (savedKey) {
+            apiKeyInput.value = savedKey;
+        }
+        
+        apiKeyInput.addEventListener('change', (e) => {
+            auth.setApiKey(e.target.value);
+        });
+    }
+}); 

+ 503 - 0
src/web/templates/analytics.html

@@ -0,0 +1,503 @@
+{% extends "base.html" %}
+
+{% block title %}Analytics - Hyperliquid Trading Bot{% endblock %}
+
+{% block extra_head %}
+<style>
+    .tab-button.active {
+        @apply bg-blue-600 text-white;
+    }
+    .tab-content {
+        display: none;
+    }
+    .tab-content.active {
+        display: block;
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="max-w-7xl mx-auto px-6 py-8" x-data="analyticsApp()" x-init="init()">
+    
+    <!-- Analytics Header -->
+    <div class="mb-8">
+        <h1 class="text-3xl font-bold text-gray-900 mb-2">📊 Trading Analytics</h1>
+        <p class="text-gray-600">Comprehensive performance analysis and statistics</p>
+    </div>
+
+    <!-- Tab Navigation -->
+    <div class="mb-8">
+        <div class="flex flex-wrap gap-2 border-b border-gray-200">
+            <button @click="setActiveTab('overview')" 
+                    :class="activeTab === 'overview' ? 'active' : ''"
+                    class="tab-button px-6 py-3 font-medium rounded-t-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">
+                📈 Overview
+            </button>
+            <button @click="setActiveTab('performance')" 
+                    :class="activeTab === 'performance' ? 'active' : ''"
+                    class="tab-button px-6 py-3 font-medium rounded-t-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">
+                🏆 Performance
+            </button>
+            <button @click="setActiveTab('timeframes')" 
+                    :class="activeTab === 'timeframes' ? 'active' : ''"
+                    class="tab-button px-6 py-3 font-medium rounded-t-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">
+                📅 Timeframes
+            </button>
+            <button @click="setActiveTab('tokens')" 
+                    :class="activeTab === 'tokens' ? 'active' : ''"
+                    class="tab-button px-6 py-3 font-medium rounded-t-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">
+                🪙 Tokens
+            </button>
+            <button @click="setActiveTab('metrics')" 
+                    :class="activeTab === 'metrics' ? 'active' : ''"
+                    class="tab-button px-6 py-3 font-medium rounded-t-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">
+                📊 Risk Metrics
+            </button>
+        </div>
+    </div>
+
+    <!-- Loading State -->
+    <div x-show="loading" class="text-center py-8">
+        <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
+        <p class="mt-2 text-gray-600">Loading analytics...</p>
+    </div>
+
+    <!-- Overview Tab -->
+    <div x-show="activeTab === 'overview'" class="tab-content">
+        <div x-show="data.overview" class="space-y-6">
+            
+            <!-- Performance Summary Cards -->
+            <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-2">💰 Total P&L</h3>
+                    <div class="text-2xl font-bold" :class="data.overview?.total_pnl >= 0 ? 'text-profit' : 'text-loss'" 
+                         x-text="utils.formatCurrency(data.overview?.total_pnl || 0)"></div>
+                    <div class="text-sm text-gray-500" 
+                         x-text="'(' + utils.formatPercent(data.overview?.total_return_pct || 0) + ' return)'"></div>
+                </div>
+                
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-2">🎯 Win Rate</h3>
+                    <div class="text-2xl font-bold text-blue-600" 
+                         x-text="utils.formatPercent(data.overview?.win_rate || 0)"></div>
+                    <div class="text-sm text-gray-500" 
+                         x-text="(data.overview?.total_wins || 0) + 'W / ' + (data.overview?.total_losses || 0) + 'L'"></div>
+                </div>
+                
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-2">⚡ Profit Factor</h3>
+                    <div class="text-2xl font-bold text-purple-600" 
+                         x-text="(data.overview?.profit_factor || 0).toFixed(2)"></div>
+                    <div class="text-sm text-gray-500">Profit/Loss Ratio</div>
+                </div>
+                
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-2">📉 Max Drawdown</h3>
+                    <div class="text-2xl font-bold text-red-500" 
+                         x-text="utils.formatPercent(data.overview?.max_drawdown_pct || 0)"></div>
+                    <div class="text-sm text-gray-500" 
+                         x-text="utils.formatCurrency(data.overview?.max_drawdown || 0)"></div>
+                </div>
+            </div>
+
+            <!-- Additional Stats -->
+            <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-3">📊 Trade Summary</h3>
+                    <div class="space-y-2">
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Total Trades:</span>
+                            <span class="font-medium" x-text="data.overview?.total_trades || 0"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Avg Trade P&L:</span>
+                            <span class="font-medium" x-text="utils.formatCurrency(data.overview?.avg_trade_pnl || 0)"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Total Volume:</span>
+                            <span class="font-medium" x-text="utils.formatCurrency(data.overview?.total_volume || 0)"></span>
+                        </div>
+                    </div>
+                </div>
+                
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-3">🏆 Best Performance</h3>
+                    <div class="space-y-2">
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Largest Win:</span>
+                            <span class="font-medium text-profit" x-text="utils.formatCurrency(data.overview?.largest_win || 0)"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Best Token:</span>
+                            <span class="font-medium" x-text="data.overview?.best_token || 'N/A'"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Token P&L:</span>
+                            <span class="font-medium" x-text="utils.formatCurrency(data.overview?.best_token_pnl || 0)"></span>
+                        </div>
+                    </div>
+                </div>
+                
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-3">📉 Worst Performance</h3>
+                    <div class="space-y-2">
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Largest Loss:</span>
+                            <span class="font-medium text-loss" x-text="utils.formatCurrency(data.overview?.largest_loss || 0)"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Worst Token:</span>
+                            <span class="font-medium" x-text="data.overview?.worst_token || 'N/A'"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Token P&L:</span>
+                            <span class="font-medium" x-text="utils.formatCurrency(data.overview?.worst_token_pnl || 0)"></span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Performance Tab -->
+    <div x-show="activeTab === 'performance'" class="tab-content">
+        <div class="space-y-6">
+            <div class="flex justify-between items-center">
+                <h2 class="text-xl font-bold">🏆 Token Performance Ranking</h2>
+                <select @change="loadPerformance($event.target.value)" class="form-select">
+                    <option value="20">Top 20</option>
+                    <option value="10">Top 10</option>
+                    <option value="50">Top 50</option>
+                </select>
+            </div>
+            
+            <div x-show="data.performance && data.performance.length > 0" class="overflow-x-auto">
+                <table class="min-w-full bg-white rounded-lg shadow">
+                    <thead class="bg-gray-50">
+                        <tr>
+                            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Token</th>
+                            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">P&L</th>
+                            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ROE %</th>
+                            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Win Rate</th>
+                            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Trades</th>
+                            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Volume</th>
+                            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Avg Duration</th>
+                        </tr>
+                    </thead>
+                    <tbody class="divide-y divide-gray-200">
+                        <template x-for="(token, index) in data.performance" :key="token.token">
+                            <tr class="hover:bg-gray-50">
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <div class="flex items-center">
+                                        <span class="text-sm font-medium text-gray-900" x-text="token.token"></span>
+                                        <span class="ml-2 text-xs text-gray-500" x-text="'#' + (index + 1)"></span>
+                                    </div>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span :class="token.total_pnl >= 0 ? 'text-profit' : 'text-loss'" 
+                                          class="text-sm font-medium" x-text="utils.formatCurrency(token.total_pnl)"></span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span :class="token.roe_percentage >= 0 ? 'text-profit' : 'text-loss'" 
+                                          class="text-sm font-medium" x-text="utils.formatPercent(token.roe_percentage)"></span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="text-sm text-gray-900" x-text="utils.formatPercent(token.win_rate)"></span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="text-sm text-gray-900" x-text="token.total_trades"></span>
+                                    <div class="text-xs text-gray-500" x-text="token.winning_trades + 'W / ' + token.losing_trades + 'L'"></div>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="text-sm text-gray-900" x-text="utils.formatCurrency(token.entry_volume)"></span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="text-sm text-gray-900" x-text="token.avg_duration"></span>
+                                </td>
+                            </tr>
+                        </template>
+                    </tbody>
+                </table>
+            </div>
+            
+            <div x-show="!data.performance || data.performance.length === 0" class="text-center py-8">
+                <p class="text-gray-500">📭 No token performance data available yet.</p>
+            </div>
+        </div>
+    </div>
+
+    <!-- Timeframes Tab -->
+    <div x-show="activeTab === 'timeframes'" class="tab-content">
+        <div class="space-y-8">
+            
+            <!-- Timeframe Selector -->
+            <div class="flex space-x-4">
+                <button @click="setTimeframe('daily')" 
+                        :class="selectedTimeframe === 'daily' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'"
+                        class="px-4 py-2 rounded-lg font-medium">📅 Daily</button>
+                <button @click="setTimeframe('weekly')" 
+                        :class="selectedTimeframe === 'weekly' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'"
+                        class="px-4 py-2 rounded-lg font-medium">📊 Weekly</button>
+                <button @click="setTimeframe('monthly')" 
+                        :class="selectedTimeframe === 'monthly' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'"
+                        class="px-4 py-2 rounded-lg font-medium">📆 Monthly</button>
+            </div>
+
+            <!-- Timeframe Data -->
+            <div x-show="data.timeframes[selectedTimeframe]" class="space-y-4">
+                <h3 class="text-lg font-semibold capitalize" x-text="selectedTimeframe + ' Performance'"></h3>
+                
+                <div class="grid gap-4">
+                    <template x-for="period in data.timeframes[selectedTimeframe]" :key="period.period">
+                        <div class="bg-white rounded-lg shadow-sm border p-4">
+                            <div class="flex justify-between items-center">
+                                <div>
+                                    <h4 class="font-medium" x-text="period.period_formatted"></h4>
+                                    <div x-show="!period.has_trades" class="text-gray-500 text-sm">📭 No trading activity</div>
+                                </div>
+                                <div x-show="period.has_trades" class="text-right">
+                                    <div :class="period.pnl >= 0 ? 'text-profit' : 'text-loss'" 
+                                         class="font-bold" x-text="utils.formatCurrency(period.pnl)"></div>
+                                    <div class="text-sm text-gray-500">
+                                        <span x-text="utils.formatPercent(period.roe)"></span> ROE • 
+                                        <span x-text="period.trades"></span> trades
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </template>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Tokens Tab -->
+    <div x-show="activeTab === 'tokens'" class="tab-content">
+        <div class="space-y-6">
+            <div class="flex justify-between items-center">
+                <h2 class="text-xl font-bold">🪙 Token Analysis</h2>
+                <div class="flex space-x-2">
+                    <input type="text" x-model="tokenSearch" @input="searchToken()" 
+                           placeholder="Search token (e.g., BTC)" 
+                           class="px-3 py-2 border rounded-lg">
+                    <button @click="tokenSearch = ''; loadTokenStats()" class="px-4 py-2 bg-gray-200 rounded-lg">Clear</button>
+                </div>
+            </div>
+            
+            <!-- Individual Token Stats -->
+            <div x-show="data.selectedToken" class="bg-white rounded-lg shadow-sm border p-6">
+                <h3 class="text-lg font-bold mb-4" x-text="'📊 ' + (data.selectedToken?.token || '') + ' Detailed Statistics'"></h3>
+                
+                <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+                    <div class="space-y-2">
+                        <h4 class="font-medium text-gray-700">Performance</h4>
+                        <div class="text-lg font-bold" :class="data.selectedToken?.total_pnl >= 0 ? 'text-profit' : 'text-loss'" 
+                             x-text="utils.formatCurrency(data.selectedToken?.total_pnl || 0)"></div>
+                        <div class="text-sm text-gray-500" x-text="utils.formatPercent(data.selectedToken?.roe_percentage || 0) + ' ROE'"></div>
+                    </div>
+                    
+                    <div class="space-y-2">
+                        <h4 class="font-medium text-gray-700">Trading Activity</h4>
+                        <div class="text-lg font-bold" x-text="data.selectedToken?.total_trades || 0"></div>
+                        <div class="text-sm text-gray-500" x-text="(data.selectedToken?.winning_trades || 0) + 'W / ' + (data.selectedToken?.losing_trades || 0) + 'L'"></div>
+                    </div>
+                    
+                    <div class="space-y-2">
+                        <h4 class="font-medium text-gray-700">Win Rate</h4>
+                        <div class="text-lg font-bold text-blue-600" x-text="utils.formatPercent(data.selectedToken?.win_rate || 0)"></div>
+                        <div class="text-sm text-gray-500">Success Rate</div>
+                    </div>
+                </div>
+            </div>
+            
+            <div x-show="tokenSearchError" class="bg-red-50 border border-red-200 rounded-lg p-4">
+                <p class="text-red-600" x-text="tokenSearchError"></p>
+            </div>
+        </div>
+    </div>
+
+    <!-- Risk Metrics Tab -->
+    <div x-show="activeTab === 'metrics'" class="tab-content">
+        <div x-show="data.metrics" class="space-y-6">
+            <h2 class="text-xl font-bold">📊 Risk & Performance Metrics</h2>
+            
+            <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-3">📈 Risk Analysis</h3>
+                    <div class="space-y-3">
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Sharpe Ratio:</span>
+                            <span class="font-medium" x-text="data.metrics?.sharpe_ratio?.toFixed(2) || 'N/A'"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Max Drawdown:</span>
+                            <span class="font-medium text-red-500" 
+                                  x-text="utils.formatPercent(data.metrics?.risk_metrics?.max_drawdown_percentage || 0)"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Volatility:</span>
+                            <span class="font-medium" x-text="utils.formatPercent(data.metrics?.volatility || 0)"></span>
+                        </div>
+                    </div>
+                </div>
+                
+                <div class="metric-card">
+                    <h3 class="font-semibold text-gray-700 mb-3">🏆 Trade Quality</h3>
+                    <div class="space-y-3">
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Best ROE Trade:</span>
+                            <span class="font-medium text-profit" 
+                                  x-text="data.metrics?.best_roe_trade ? utils.formatPercent(data.metrics.best_roe_trade.percentage) + ' (' + data.metrics.best_roe_trade.token + ')' : 'N/A'"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Worst ROE Trade:</span>
+                            <span class="font-medium text-loss" 
+                                  x-text="data.metrics?.worst_roe_trade ? utils.formatPercent(data.metrics.worst_roe_trade.percentage) + ' (' + data.metrics.worst_roe_trade.token + ')' : 'N/A'"></span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span class="text-gray-600">Avg Duration:</span>
+                            <span class="font-medium" x-text="data.metrics?.avg_trade_duration || 'N/A'"></span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+</div>
+
+<script>
+function analyticsApp() {
+    return {
+        activeTab: 'overview',
+        selectedTimeframe: 'daily',
+        tokenSearch: '',
+        tokenSearchError: '',
+        loading: false,
+        data: {
+            overview: null,
+            performance: null,
+            timeframes: {
+                daily: null,
+                weekly: null,
+                monthly: null
+            },
+            selectedToken: null,
+            metrics: null
+        },
+
+        async init() {
+            this.loading = true;
+            try {
+                await this.loadOverview();
+            } catch (error) {
+                console.error('Failed to load analytics:', error);
+            } finally {
+                this.loading = false;
+            }
+        },
+
+        setActiveTab(tab) {
+            this.activeTab = tab;
+            
+            // Load data for the active tab if not already loaded
+            switch(tab) {
+                case 'overview':
+                    if (!this.data.overview) this.loadOverview();
+                    break;
+                case 'performance':
+                    if (!this.data.performance) this.loadPerformance();
+                    break;
+                case 'timeframes':
+                    if (!this.data.timeframes[this.selectedTimeframe]) this.loadTimeframes();
+                    break;
+                case 'metrics':
+                    if (!this.data.metrics) this.loadMetrics();
+                    break;
+            }
+        },
+
+        setTimeframe(timeframe) {
+            this.selectedTimeframe = timeframe;
+            if (!this.data.timeframes[timeframe]) {
+                this.loadTimeframes();
+            }
+        },
+
+        async loadOverview() {
+            try {
+                const response = await utils.authenticatedFetch('/api/analytics/stats');
+                if (response.ok) {
+                    this.data.overview = await response.json();
+                }
+            } catch (error) {
+                console.error('Error loading overview:', error);
+            }
+        },
+
+        async loadPerformance(limit = 20) {
+            try {
+                const response = await utils.authenticatedFetch(`/api/analytics/performance?limit=${limit}`);
+                if (response.ok) {
+                    this.data.performance = await response.json();
+                }
+            } catch (error) {
+                console.error('Error loading performance:', error);
+            }
+        },
+
+        async loadTimeframes() {
+            try {
+                const response = await utils.authenticatedFetch(`/api/analytics/${this.selectedTimeframe}`);
+                if (response.ok) {
+                    this.data.timeframes[this.selectedTimeframe] = await response.json();
+                }
+            } catch (error) {
+                console.error('Error loading timeframes:', error);
+            }
+        },
+
+        async loadMetrics() {
+            try {
+                const response = await utils.authenticatedFetch('/api/analytics/metrics');
+                if (response.ok) {
+                    this.data.metrics = await response.json();
+                }
+            } catch (error) {
+                console.error('Error loading metrics:', error);
+            }
+        },
+
+        async searchToken() {
+            this.tokenSearchError = '';
+            
+            if (!this.tokenSearch.trim()) {
+                this.data.selectedToken = null;
+                return;
+            }
+
+            try {
+                const response = await utils.authenticatedFetch(`/api/analytics/stats/${this.tokenSearch.trim()}`);
+                if (response.ok) {
+                    this.data.selectedToken = await response.json();
+                } else if (response.status === 404) {
+                    this.tokenSearchError = `No trading data found for ${this.tokenSearch.toUpperCase()}`;
+                    this.data.selectedToken = null;
+                } else {
+                    throw new Error('Failed to load token data');
+                }
+            } catch (error) {
+                console.error('Error searching token:', error);
+                this.tokenSearchError = 'Error loading token data';
+                this.data.selectedToken = null;
+            }
+        },
+
+        async loadTokenStats() {
+            this.data.selectedToken = null;
+            this.tokenSearchError = '';
+        }
+    }
+}
+</script>
+{% endblock %} 

+ 139 - 0
src/web/templates/base.html

@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html lang="en" class="h-full bg-gray-50">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{% block title %}Hyperliquid Trading Bot{% endblock %}</title>
+    
+    <!-- Tailwind CSS -->
+    <script src="https://cdn.tailwindcss.com"></script>
+    
+    <!-- Alpine.js -->
+    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
+    
+    <!-- Chart.js -->
+    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+    
+    <!-- Font Awesome -->
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
+    
+    <!-- Custom CSS -->
+    <link rel="stylesheet" href="{{ url_for('static', path='/css/app.css') }}">
+    
+    <style>
+        /* Custom styles for better UX */
+        .card {
+            @apply bg-white rounded-lg shadow-sm border border-gray-200 p-6;
+        }
+        
+        .metric-card {
+            @apply bg-white rounded-lg shadow-sm border border-gray-200 p-4;
+        }
+        
+        .btn-primary {
+            @apply bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors;
+        }
+        
+        .btn-secondary {
+            @apply bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-lg transition-colors;
+        }
+        
+        .btn-success {
+            @apply bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors;
+        }
+        
+        .btn-danger {
+            @apply bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-lg transition-colors;
+        }
+        
+        .text-profit {
+            @apply text-green-600 font-semibold;
+        }
+        
+        .text-loss {
+            @apply text-red-600 font-semibold;
+        }
+        
+        .loading {
+            @apply opacity-50 pointer-events-none;
+        }
+    </style>
+</head>
+<body class="h-full">
+    <div id="app" class="min-h-full">
+        <!-- Navigation -->
+        <nav class="bg-white shadow-sm border-b border-gray-200">
+            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+                <div class="flex justify-between h-16">
+                    <div class="flex">
+                        <div class="flex-shrink-0 flex items-center">
+                            <i class="fas fa-chart-line text-blue-600 text-2xl mr-3"></i>
+                            <h1 class="text-xl font-bold text-gray-900">Hyperliquid Bot</h1>
+                            {% if config.testnet %}
+                            <span class="ml-2 bg-yellow-100 text-yellow-800 text-xs font-medium px-2 py-1 rounded">TESTNET</span>
+                            {% endif %}
+                        </div>
+                        <div class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
+                            <a href="/" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
+                                <i class="fas fa-tachometer-alt mr-2"></i>Dashboard
+                            </a>
+                            <a href="#" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
+                                <i class="fas fa-chart-area mr-2"></i>Trading
+                            </a>
+                            <a href="/copy-trading" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
+                                <i class="fas fa-copy mr-2"></i>Copy Trading
+                            </a>
+                            <a href="/analytics" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
+                                <i class="fas fa-chart-bar mr-2"></i>Analytics
+                            </a>
+                        </div>
+                    </div>
+                    <div class="hidden sm:ml-6 sm:flex sm:items-center">
+                        <div class="text-sm text-gray-500">
+                            <span class="font-medium">Default Token:</span> {{ config.default_token }}
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </nav>
+
+        <!-- Main Content -->
+        <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
+            <!-- Alert/Toast Container -->
+            <div id="toast-container" class="fixed top-4 right-4 z-50"></div>
+            
+            {% block content %}{% endblock %}
+        </main>
+
+        <!-- Footer -->
+        <footer class="bg-white border-t border-gray-200 mt-12">
+            <div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
+                <div class="flex justify-between items-center text-sm text-gray-500">
+                    <div>
+                        <span class="font-medium">Hyperliquid Trading Bot</span> v1.0.0
+                    </div>
+                    <div class="flex items-center space-x-4">
+                        <span id="connection-status" class="flex items-center">
+                            <i class="fas fa-circle text-green-500 mr-1"></i>
+                            Connected
+                        </span>
+                        <span id="last-update">Last update: --</span>
+                    </div>
+                </div>
+            </div>
+        </footer>
+    </div>
+
+    <!-- Global JavaScript Configuration -->
+    <script>
+        window.APP_CONFIG = {
+            apiBase: '{{ config.api_base }}',
+            defaultToken: '{{ config.default_token }}',
+            testnet: {% if config.testnet %}true{% else %}false{% endif %}
+        };
+    </script>
+    
+    <script src="{{ url_for('static', path='/js/app.js') }}"></script>
+    {% block scripts %}{% endblock %}
+</body>
+</html> 

+ 1169 - 0
src/web/templates/copy_trading.html

@@ -0,0 +1,1169 @@
+{% extends "base.html" %}
+
+{% block title %}Copy Trading - Hyperliquid Bot{% endblock %}
+
+{% block content %}
+<div class="container mx-auto px-6 py-8">
+    <!-- Header -->
+    <div class="mb-8">
+        <h1 class="text-3xl font-bold text-gray-800 mb-2">Copy Trading Management</h1>
+        <p class="text-gray-600">Manage copy trading settings and analyze potential target traders</p>
+    </div>
+
+    <!-- Copy Trading Controls -->
+    <div class="bg-white rounded-lg shadow-md p-6 mb-8">
+        <div class="flex items-center justify-between mb-6">
+            <h2 class="text-2xl font-semibold text-gray-800 flex items-center">
+                <svg class="w-6 h-6 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
+                </svg>
+                Copy Trading Status
+            </h2>
+            <div id="copyTradingStatus" class="flex items-center">
+                <span class="status-indicator w-3 h-3 rounded-full bg-gray-400 mr-2"></span>
+                <span class="status-text text-gray-600">Loading...</span>
+            </div>
+        </div>
+
+        <!-- Status Cards -->
+        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
+            <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
+                <div class="text-blue-600 text-sm font-medium">Target Address</div>
+                <div id="targetAddress" class="text-blue-800 font-semibold text-sm mt-1 break-all">-</div>
+            </div>
+            <div class="bg-green-50 border border-green-200 rounded-lg p-4">
+                <div class="text-green-600 text-sm font-medium">Portfolio Allocation</div>
+                <div id="portfolioAllocation" class="text-green-800 font-semibold text-lg mt-1">-</div>
+            </div>
+            <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
+                <div class="text-yellow-600 text-sm font-medium">Active Positions</div>
+                <div id="activePositions" class="text-yellow-800 font-semibold text-lg mt-1">-</div>
+            </div>
+            <div class="bg-purple-50 border border-purple-200 rounded-lg p-4">
+                <div class="text-purple-600 text-sm font-medium">Copied Trades</div>
+                <div id="copiedTrades" class="text-purple-800 font-semibold text-lg mt-1">-</div>
+            </div>
+        </div>
+
+        <!-- Control Buttons -->
+        <div class="flex flex-wrap gap-4">
+            <button id="startCopyTradingBtn" class="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                Start Copy Trading
+            </button>
+            <button id="stopCopyTradingBtn" class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                Stop Copy Trading
+            </button>
+            <button id="testBalanceBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                Test Balance
+            </button>
+            <button id="refreshStatusBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                Refresh Status
+            </button>
+        </div>
+    </div>
+
+    <!-- Tab Navigation -->
+    <div class="bg-white rounded-lg shadow-md mb-8">
+        <div class="border-b border-gray-200">
+            <nav class="-mb-px flex">
+                <button class="tab-button active py-4 px-6 text-sm font-medium text-blue-600 border-b-2 border-blue-600" data-tab="config">
+                    Configuration
+                </button>
+                <button class="tab-button py-4 px-6 text-sm font-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent" data-tab="analyzer">
+                    Account Analyzer
+                </button>
+                <button class="tab-button py-4 px-6 text-sm font-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent" data-tab="leaderboard">
+                    Leaderboard
+                </button>
+                <button class="tab-button py-4 px-6 text-sm font-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent" data-tab="session">
+                    Session Info
+                </button>
+            </nav>
+        </div>
+
+        <!-- Configuration Tab -->
+        <div id="configTab" class="tab-content p-6">
+            <h3 class="text-xl font-semibold text-gray-800 mb-4">Copy Trading Configuration</h3>
+            
+            <form id="copyTradingForm" class="space-y-6">
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                    <div>
+                        <label for="targetAddress" class="block text-sm font-medium text-gray-700 mb-2">Target Trader Address</label>
+                        <input type="text" id="configTargetAddress" name="target_address" 
+                               class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                               placeholder="0x..." required>
+                        <p class="text-xs text-gray-500 mt-1">Ethereum address of the trader to copy</p>
+                    </div>
+                    
+                    <div>
+                        <label for="portfolioPercentage" class="block text-sm font-medium text-gray-700 mb-2">Portfolio Percentage</label>
+                        <div class="relative">
+                            <input type="number" id="portfolioPercentage" name="portfolio_percentage" 
+                                   class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                                   min="1" max="50" step="0.1" value="10">
+                            <span class="absolute right-3 top-2 text-gray-500">%</span>
+                        </div>
+                        <p class="text-xs text-gray-500 mt-1">Percentage of portfolio to allocate (1-50%)</p>
+                    </div>
+                    
+                    <div>
+                        <label for="copyMode" class="block text-sm font-medium text-gray-700 mb-2">Copy Mode</label>
+                        <select id="copyMode" name="copy_mode" 
+                                class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
+                            <option value="FIXED">Fixed Allocation</option>
+                            <option value="PROPORTIONAL">Proportional to Target</option>
+                        </select>
+                        <p class="text-xs text-gray-500 mt-1">How to size positions relative to target trader</p>
+                    </div>
+                    
+                    <div>
+                        <label for="maxLeverage" class="block text-sm font-medium text-gray-700 mb-2">Max Leverage</label>
+                        <div class="relative">
+                            <input type="number" id="maxLeverage" name="max_leverage" 
+                                   class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                                   min="1" max="20" step="0.1" value="10">
+                            <span class="absolute right-3 top-2 text-gray-500">x</span>
+                        </div>
+                        <p class="text-xs text-gray-500 mt-1">Maximum leverage to use (1-20x)</p>
+                    </div>
+                    
+                    <div>
+                        <label for="minPositionSize" class="block text-sm font-medium text-gray-700 mb-2">Min Position Size</label>
+                        <div class="relative">
+                            <span class="absolute left-3 top-2 text-gray-500">$</span>
+                            <input type="number" id="minPositionSize" name="min_position_size" 
+                                   class="w-full pl-8 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                                   min="10" step="1" value="25">
+                        </div>
+                        <p class="text-xs text-gray-500 mt-1">Minimum position size in USD</p>
+                    </div>
+                    
+                    <div>
+                        <label for="executionDelay" class="block text-sm font-medium text-gray-700 mb-2">Execution Delay</label>
+                        <div class="relative">
+                            <input type="number" id="executionDelay" name="execution_delay" 
+                                   class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                                   min="0" max="10" step="0.1" value="2">
+                            <span class="absolute right-3 top-2 text-gray-500">s</span>
+                        </div>
+                        <p class="text-xs text-gray-500 mt-1">Delay before executing copied trades</p>
+                    </div>
+                </div>
+                
+                <div class="flex items-center">
+                    <input type="checkbox" id="notificationsEnabled" name="notifications_enabled" 
+                           class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" checked>
+                    <label for="notificationsEnabled" class="ml-2 block text-sm text-gray-700">
+                        Enable notifications for copy trading events
+                    </label>
+                </div>
+                
+                <div class="pt-4">
+                    <button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                        Update Configuration
+                    </button>
+                </div>
+            </form>
+        </div>
+
+        <!-- Account Analyzer Tab -->
+        <div id="analyzerTab" class="tab-content p-6 hidden">
+            <h3 class="text-xl font-semibold text-gray-800 mb-4">Account Analyzer</h3>
+            
+            <div class="mb-6">
+                <div class="flex flex-col sm:flex-row gap-4">
+                    <div class="flex-1">
+                        <label for="addressesToAnalyze" class="block text-sm font-medium text-gray-700 mb-2">Addresses to Analyze</label>
+                        <textarea id="addressesToAnalyze" 
+                                  class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent h-24"
+                                  placeholder="Enter addresses separated by commas or newlines&#10;0x...&#10;0x..."></textarea>
+                        <p class="text-xs text-gray-500 mt-1">Enter multiple addresses to analyze for copy trading suitability</p>
+                    </div>
+                    <div class="sm:w-32">
+                        <label for="analysisLimit" class="block text-sm font-medium text-gray-700 mb-2">Limit</label>
+                        <input type="number" id="analysisLimit" 
+                               class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                               min="1" max="50" value="10">
+                    </div>
+                </div>
+                
+                <div class="flex gap-4 mt-4">
+                    <button id="analyzeAccountsBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                        Analyze Accounts
+                    </button>
+                    <button id="loadSampleAddressesBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                        Load Sample Addresses
+                    </button>
+                </div>
+            </div>
+
+            <!-- Analysis Results -->
+            <div id="analysisResults" class="hidden">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">Analysis Results</h4>
+                <div id="analysisTable"></div>
+            </div>
+
+            <!-- Analysis Loading -->
+            <div id="analysisLoading" class="hidden text-center py-8">
+                <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
+                <p class="text-gray-600 mt-2">Analyzing accounts...</p>
+            </div>
+        </div>
+
+        <!-- Leaderboard Tab -->
+        <div id="leaderboardTab" class="tab-content p-6 hidden">
+            <h3 class="text-xl font-semibold text-gray-800 mb-4">Top Performer Leaderboard</h3>
+            
+            <div class="mb-6">
+                <div class="flex flex-col sm:flex-row gap-4">
+                    <div>
+                        <label for="timeWindow" class="block text-sm font-medium text-gray-700 mb-2">Time Window</label>
+                        <select id="timeWindow" 
+                                class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
+                            <option value="1d">1 Day</option>
+                            <option value="7d" selected>7 Days</option>
+                            <option value="30d">30 Days</option>
+                            <option value="allTime">All Time</option>
+                        </select>
+                    </div>
+                    <div>
+                        <label for="leaderboardLimit" class="block text-sm font-medium text-gray-700 mb-2">Number of Accounts</label>
+                        <input type="number" id="leaderboardLimit" 
+                               class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                               min="1" max="50" value="15">
+                    </div>
+                    <div class="flex items-end">
+                        <button id="getLeaderboardBtn" class="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                            Get Top Performers
+                        </button>
+                    </div>
+                </div>
+                
+                <button id="analyzeLeaderboardBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200 mt-4 hidden">
+                    Analyze Leaderboard Accounts
+                </button>
+            </div>
+
+            <!-- Leaderboard Results -->
+            <div id="leaderboardResults" class="hidden">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">Top Performer Addresses</h4>
+                <div id="leaderboardAddresses" class="space-y-2"></div>
+            </div>
+
+            <!-- Leaderboard Loading -->
+            <div id="leaderboardLoading" class="hidden text-center py-8">
+                <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
+                <p class="text-gray-600 mt-2">Fetching top performers...</p>
+            </div>
+        </div>
+
+        <!-- Session Info Tab -->
+        <div id="sessionTab" class="tab-content p-6 hidden">
+            <h3 class="text-xl font-semibold text-gray-800 mb-4">Session Information</h3>
+            
+            <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                <div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
+                    <div class="text-gray-600 text-sm font-medium">Session Status</div>
+                    <div id="sessionEnabled" class="text-gray-800 font-semibold text-lg mt-1">-</div>
+                </div>
+                <div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
+                    <div class="text-gray-600 text-sm font-medium">Session Start</div>
+                    <div id="sessionStartTime" class="text-gray-800 font-semibold text-sm mt-1">-</div>
+                </div>
+                <div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
+                    <div class="text-gray-600 text-sm font-medium">Session Duration</div>
+                    <div id="sessionDuration" class="text-gray-800 font-semibold text-lg mt-1">-</div>
+                </div>
+                <div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
+                    <div class="text-gray-600 text-sm font-medium">Last Check</div>
+                    <div id="sessionLastCheck" class="text-gray-800 font-semibold text-sm mt-1">-</div>
+                </div>
+            </div>
+            
+            <div class="mt-6 flex gap-4">
+                <button id="refreshSessionBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                    Refresh Session Info
+                </button>
+                <button id="resetStateBtn" class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg font-medium transition duration-200">
+                    Reset State
+                </button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- Balance Test Modal -->
+<div id="balanceTestModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
+    <div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
+        <h3 class="text-lg font-semibold text-gray-800 mb-4">Balance Test Results</h3>
+        <div id="balanceTestContent" class="space-y-3">
+            <!-- Content will be populated by JavaScript -->
+        </div>
+        <div class="mt-6 flex justify-end">
+            <button id="closeBalanceTestBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg font-medium transition duration-200">
+                Close
+            </button>
+        </div>
+    </div>
+</div>
+
+<!-- Detailed Account Analysis Modal -->
+<div id="accountDetailModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
+    <div class="bg-white rounded-lg p-6 max-w-6xl w-full mx-4 max-h-[90vh] overflow-y-auto">
+        <div class="flex justify-between items-center mb-6">
+            <h3 class="text-2xl font-semibold text-gray-800">Deep Dive Account Analysis</h3>
+            <button id="closeAccountDetailBtn" class="text-gray-400 hover:text-gray-600 text-2xl font-bold">
+                &times;
+            </button>
+        </div>
+        
+        <!-- Loading State -->
+        <div id="accountDetailLoading" class="text-center py-8">
+            <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
+            <p class="text-gray-600 mt-2">Analyzing account...</p>
+        </div>
+        
+        <!-- Content -->
+        <div id="accountDetailContent" class="hidden">
+            <!-- Content will be populated by JavaScript -->
+        </div>
+    </div>
+</div>
+
+<script>
+// Copy Trading Management JavaScript
+document.addEventListener('DOMContentLoaded', function() {
+    // Tab management
+    const tabButtons = document.querySelectorAll('.tab-button');
+    const tabContents = document.querySelectorAll('.tab-content');
+    
+    tabButtons.forEach(button => {
+        button.addEventListener('click', () => {
+            const tabName = button.dataset.tab;
+            
+            // Update active tab button
+            tabButtons.forEach(b => {
+                b.classList.remove('active', 'text-blue-600', 'border-blue-600');
+                b.classList.add('text-gray-500', 'border-transparent');
+            });
+            button.classList.add('active', 'text-blue-600', 'border-blue-600');
+            button.classList.remove('text-gray-500', 'border-transparent');
+            
+            // Show corresponding tab content
+            tabContents.forEach(content => {
+                content.classList.add('hidden');
+            });
+            document.getElementById(tabName + 'Tab').classList.remove('hidden');
+        });
+    });
+    
+    // Load initial data
+    loadCopyTradingStatus();
+    loadCopyTradingConfig();
+    
+    // Event listeners
+    document.getElementById('startCopyTradingBtn').addEventListener('click', startCopyTrading);
+    document.getElementById('stopCopyTradingBtn').addEventListener('click', stopCopyTrading);
+    document.getElementById('testBalanceBtn').addEventListener('click', testBalance);
+    document.getElementById('refreshStatusBtn').addEventListener('click', loadCopyTradingStatus);
+    document.getElementById('copyTradingForm').addEventListener('submit', updateConfiguration);
+    document.getElementById('analyzeAccountsBtn').addEventListener('click', analyzeAccounts);
+    document.getElementById('loadSampleAddressesBtn').addEventListener('click', loadSampleAddresses);
+    document.getElementById('getLeaderboardBtn').addEventListener('click', getLeaderboard);
+    document.getElementById('analyzeLeaderboardBtn').addEventListener('click', analyzeLeaderboard);
+    document.getElementById('refreshSessionBtn').addEventListener('click', loadSessionInfo);
+    document.getElementById('resetStateBtn').addEventListener('click', resetState);
+    document.getElementById('closeBalanceTestBtn').addEventListener('click', closeBalanceTestModal);
+    document.getElementById('closeAccountDetailBtn').addEventListener('click', closeAccountDetailModal);
+    
+    // Auto-refresh status every 30 seconds
+    setInterval(loadCopyTradingStatus, 30000);
+});
+
+async function loadCopyTradingStatus() {
+    try {
+        const response = await fetch('/api/copy-trading/status');
+        const status = await response.json();
+        
+        // Update status indicator
+        const statusIndicator = document.querySelector('#copyTradingStatus .status-indicator');
+        const statusText = document.querySelector('#copyTradingStatus .status-text');
+        
+        if (status.enabled) {
+            statusIndicator.className = 'status-indicator w-3 h-3 rounded-full bg-green-500 mr-2';
+            statusText.textContent = 'Active';
+            statusText.className = 'status-text text-green-600 font-medium';
+        } else {
+            statusIndicator.className = 'status-indicator w-3 h-3 rounded-full bg-red-500 mr-2';
+            statusText.textContent = 'Inactive';
+            statusText.className = 'status-text text-red-600 font-medium';
+        }
+        
+        // Update status cards
+        document.getElementById('targetAddress').textContent = status.target_address || '-';
+        document.getElementById('portfolioAllocation').textContent = status.portfolio_percentage ? 
+            (status.portfolio_percentage * 100).toFixed(1) + '%' : '-';
+        document.getElementById('activePositions').textContent = 
+            `${status.our_positions}/${status.target_positions}`;
+        document.getElementById('copiedTrades').textContent = status.copied_trades || '0';
+        
+    } catch (error) {
+        console.error('Error loading copy trading status:', error);
+        showToast('Failed to load copy trading status', 'error');
+    }
+}
+
+async function loadCopyTradingConfig() {
+    try {
+        const response = await fetch('/api/copy-trading/config');
+        const config = await response.json();
+        
+        // Populate form with current config
+        document.getElementById('configTargetAddress').value = config.target_address || '';
+        document.getElementById('portfolioPercentage').value = (config.portfolio_percentage * 100).toFixed(1);
+        document.getElementById('copyMode').value = config.copy_mode || 'FIXED';
+        document.getElementById('maxLeverage').value = config.max_leverage || 10;
+        document.getElementById('minPositionSize').value = config.min_position_size || 25;
+        document.getElementById('executionDelay').value = config.execution_delay || 2;
+        document.getElementById('notificationsEnabled').checked = config.notifications_enabled !== false;
+        
+    } catch (error) {
+        console.error('Error loading copy trading config:', error);
+    }
+}
+
+async function startCopyTrading() {
+    try {
+        const formData = new FormData(document.getElementById('copyTradingForm'));
+        const config = {
+            target_address: formData.get('target_address'),
+            portfolio_percentage: parseFloat(formData.get('portfolio_percentage')) / 100,
+            copy_mode: formData.get('copy_mode'),
+            max_leverage: parseFloat(formData.get('max_leverage')),
+            min_position_size: parseFloat(formData.get('min_position_size')),
+            execution_delay: parseFloat(formData.get('execution_delay')),
+            notifications_enabled: formData.get('notifications_enabled') === 'on'
+        };
+        
+        if (!config.target_address) {
+            showToast('Please enter a target trader address', 'error');
+            return;
+        }
+        
+        const response = await fetch('/api/copy-trading/start', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify(config)
+        });
+        
+        const result = await response.json();
+        
+        if (response.ok) {
+            showToast('Copy trading started successfully', 'success');
+            loadCopyTradingStatus();
+        } else {
+            showToast(result.detail || 'Failed to start copy trading', 'error');
+        }
+        
+    } catch (error) {
+        console.error('Error starting copy trading:', error);
+        showToast('Failed to start copy trading', 'error');
+    }
+}
+
+async function stopCopyTrading() {
+    try {
+        const response = await fetch('/api/copy-trading/stop', {
+            method: 'POST'
+        });
+        
+        const result = await response.json();
+        
+        if (response.ok) {
+            showToast('Copy trading stopped successfully', 'success');
+            loadCopyTradingStatus();
+        } else {
+            showToast(result.detail || 'Failed to stop copy trading', 'error');
+        }
+        
+    } catch (error) {
+        console.error('Error stopping copy trading:', error);
+        showToast('Failed to stop copy trading', 'error');
+    }
+}
+
+async function testBalance() {
+    try {
+        const response = await fetch('/api/copy-trading/test-balance');
+        const result = await response.json();
+        
+        if (response.ok) {
+            showBalanceTestModal(result);
+        } else {
+            showToast(result.detail || 'Failed to test balance', 'error');
+        }
+        
+    } catch (error) {
+        console.error('Error testing balance:', error);
+        showToast('Failed to test balance', 'error');
+    }
+}
+
+function showBalanceTestModal(result) {
+    const content = document.getElementById('balanceTestContent');
+    
+    if (result.error) {
+        content.innerHTML = `
+            <div class="bg-red-50 border border-red-200 rounded-lg p-4">
+                <div class="text-red-600 text-sm font-medium">Error</div>
+                <div class="text-red-800 text-sm mt-1">${result.error}</div>
+            </div>
+        `;
+    } else {
+        content.innerHTML = `
+            <div class="grid grid-cols-2 gap-4 text-sm">
+                <div><span class="font-medium">Our Balance:</span> $${result.our_balance.toFixed(2)}</div>
+                <div><span class="font-medium">Target Balance:</span> $${result.target_balance.toFixed(2)}</div>
+                <div><span class="font-medium">Portfolio %:</span> ${(result.portfolio_percentage * 100).toFixed(1)}%</div>
+                <div><span class="font-medium">Test Leverage:</span> ${result.test_leverage.toFixed(1)}x</div>
+                <div><span class="font-medium">Margin to Use:</span> $${result.margin_to_use.toFixed(2)}</div>
+                <div><span class="font-medium">Position Value:</span> $${result.position_value.toFixed(2)}</div>
+                <div><span class="font-medium">Token Amount:</span> ${result.token_amount.toFixed(6)}</div>
+                <div><span class="font-medium">Would Execute:</span> ${result.would_execute ? 'Yes' : 'No'}</div>
+            </div>
+            <div class="mt-4 p-3 rounded-lg ${result.would_execute ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200'}">
+                <div class="text-sm font-medium ${result.would_execute ? 'text-green-600' : 'text-yellow-600'}">
+                    ${result.would_execute ? '✅ Configuration looks good!' : '⚠️ Position size too small'}
+                </div>
+            </div>
+        `;
+    }
+    
+    document.getElementById('balanceTestModal').classList.remove('hidden');
+    document.getElementById('balanceTestModal').classList.add('flex');
+}
+
+function closeBalanceTestModal() {
+    document.getElementById('balanceTestModal').classList.add('hidden');
+    document.getElementById('balanceTestModal').classList.remove('flex');
+}
+
+async function updateConfiguration(event) {
+    event.preventDefault();
+    
+    try {
+        const formData = new FormData(event.target);
+        const config = {
+            target_address: formData.get('target_address'),
+            portfolio_percentage: parseFloat(formData.get('portfolio_percentage')) / 100,
+            copy_mode: formData.get('copy_mode'),
+            max_leverage: parseFloat(formData.get('max_leverage')),
+            min_position_size: parseFloat(formData.get('min_position_size')),
+            execution_delay: parseFloat(formData.get('execution_delay')),
+            notifications_enabled: formData.get('notifications_enabled') === 'on'
+        };
+        
+        // For now, just show success message since we don't have a separate update endpoint
+        showToast('Configuration updated (restart copy trading to apply)', 'success');
+        
+    } catch (error) {
+        console.error('Error updating configuration:', error);
+        showToast('Failed to update configuration', 'error');
+    }
+}
+
+async function analyzeAccounts() {
+    const addressesText = document.getElementById('addressesToAnalyze').value.trim();
+    const limit = parseInt(document.getElementById('analysisLimit').value);
+    
+    if (!addressesText) {
+        showToast('Please enter at least one address to analyze', 'error');
+        return;
+    }
+    
+    // Parse addresses
+    const addresses = addressesText
+        .split(/[,\n]/)
+        .map(addr => addr.trim())
+        .filter(addr => addr.length > 0);
+    
+    if (addresses.length === 0) {
+        showToast('No valid addresses found', 'error');
+        return;
+    }
+    
+    // Show loading
+    document.getElementById('analysisLoading').classList.remove('hidden');
+    document.getElementById('analysisResults').classList.add('hidden');
+    
+    try {
+        const response = await fetch('/api/copy-trading/analyze-accounts', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({
+                addresses: addresses,
+                limit: limit
+            })
+        });
+        
+        const results = await response.json();
+        
+        if (response.ok) {
+            displayAnalysisResults(results);
+            document.getElementById('analysisResults').classList.remove('hidden');
+        } else {
+            showToast(results.detail || 'Failed to analyze accounts', 'error');
+        }
+        
+    } catch (error) {
+        console.error('Error analyzing accounts:', error);
+        showToast('Failed to analyze accounts', 'error');
+    } finally {
+        document.getElementById('analysisLoading').classList.add('hidden');
+    }
+}
+
+function displayAnalysisResults(results) {
+    const tableContainer = document.getElementById('analysisTable');
+    
+    if (results.length === 0) {
+        tableContainer.innerHTML = '<p class="text-gray-500 text-center py-4">No results to display</p>';
+        return;
+    }
+    
+    const table = `
+        <div class="overflow-x-auto">
+            <table class="min-w-full bg-white border border-gray-300">
+                <thead class="bg-gray-50">
+                    <tr>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Address</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Score</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Copyable</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">P&L</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Win Rate</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Trades/Day</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Drawdown</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Style</th>
+                        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
+                    </tr>
+                </thead>
+                <tbody class="divide-y divide-gray-200">
+                    ${results.map((account, index) => `
+                                            <tr class="${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'} hover:bg-blue-50 transition-colors">
+                        <td class="px-4 py-2 text-xs font-mono">
+                            <button class="text-blue-600 hover:text-blue-800 hover:underline" 
+                                    onclick="showAccountDetail('${account.address}')">
+                                ${account.address.substring(0, 10)}...
+                            </button>
+                        </td>
+                            <td class="px-4 py-2 text-sm">
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
+                                    ${account.relative_score >= 60 ? 'bg-green-100 text-green-800' : 
+                                      account.relative_score >= 40 ? 'bg-yellow-100 text-yellow-800' : 
+                                      account.relative_score >= 20 ? 'bg-orange-100 text-orange-800' : 
+                                      'bg-red-100 text-red-800'}">
+                                    ${account.relative_score ? account.relative_score.toFixed(1) : '0'}
+                                </span>
+                            </td>
+                            <td class="px-4 py-2 text-sm">
+                                ${account.is_copyable ? 
+                                    '<span class="text-green-600">✅ Yes</span>' : 
+                                    '<span class="text-red-600">❌ No</span>'}
+                            </td>
+                            <td class="px-4 py-2 text-sm ${account.total_pnl >= 0 ? 'text-green-600' : 'text-red-600'}">
+                                $${account.total_pnl.toFixed(0)}
+                            </td>
+                            <td class="px-4 py-2 text-sm">${(account.win_rate * 100).toFixed(1)}%</td>
+                            <td class="px-4 py-2 text-sm">${account.trading_frequency_per_day.toFixed(1)}</td>
+                            <td class="px-4 py-2 text-sm">${(account.max_drawdown * 100).toFixed(1)}%</td>
+                            <td class="px-4 py-2 text-xs">${account.trading_style.substring(0, 20)}...</td>
+                            <td class="px-4 py-2">
+                                ${account.is_copyable && account.relative_score >= 40 ? 
+                                    `<button class="bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs"
+                                            onclick="setTargetAddress('${account.address}')">Set as Target</button>` : 
+                                    '<span class="text-gray-400 text-xs">Not suitable</span>'}
+                            </td>
+                        </tr>
+                    `).join('')}
+                </tbody>
+            </table>
+        </div>
+    `;
+    
+    tableContainer.innerHTML = table;
+}
+
+function setTargetAddress(address) {
+    document.getElementById('configTargetAddress').value = address;
+    showToast('Target address set successfully', 'success');
+    
+    // Switch to configuration tab
+    document.querySelector('[data-tab="config"]').click();
+}
+
+function loadSampleAddresses() {
+    const sampleAddresses = [
+        '0xa10ec245b3483f83e350a9165a52ae23dbab01bc',
+        '0x59a15c79a007cd6e9965b949fcf04125c2212524',
+        '0x0487b5e806ac781508cb3272ebd83ad603ddcc0f',
+        '0x72fad4e75748b65566a3ebb555b6f6ee18ce08d1',
+        '0xa70434af5778038245d53da1b4d360a30307a827'
+    ];
+    
+    document.getElementById('addressesToAnalyze').value = sampleAddresses.join('\n');
+    showToast('Sample addresses loaded', 'success');
+}
+
+async function getLeaderboard() {
+    const window = document.getElementById('timeWindow').value;
+    const limit = parseInt(document.getElementById('leaderboardLimit').value);
+    
+    // Show loading
+    document.getElementById('leaderboardLoading').classList.remove('hidden');
+    document.getElementById('leaderboardResults').classList.add('hidden');
+    
+    try {
+        const response = await fetch('/api/copy-trading/get-leaderboard', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({
+                window: window,
+                limit: limit
+            })
+        });
+        
+        const addresses = await response.json();
+        
+        if (response.ok) {
+            displayLeaderboardResults(addresses);
+            document.getElementById('leaderboardResults').classList.remove('hidden');
+            document.getElementById('analyzeLeaderboardBtn').classList.remove('hidden');
+        } else {
+            showToast('Failed to get leaderboard', 'error');
+        }
+        
+    } catch (error) {
+        console.error('Error getting leaderboard:', error);
+        showToast('Failed to get leaderboard', 'error');
+    } finally {
+        document.getElementById('leaderboardLoading').classList.add('hidden');
+    }
+}
+
+function displayLeaderboardResults(addresses) {
+    const container = document.getElementById('leaderboardAddresses');
+    
+    if (addresses.length === 0) {
+        container.innerHTML = '<p class="text-gray-500 text-center py-4">No addresses found</p>';
+        return;
+    }
+    
+    container.innerHTML = addresses.map((address, index) => `
+        <div class="flex items-center justify-between bg-white border border-gray-200 rounded-lg p-3 hover:bg-blue-50 transition-colors">
+            <div class="flex items-center">
+                <span class="bg-blue-100 text-blue-800 text-sm font-medium px-2.5 py-0.5 rounded-full mr-3">
+                    #${index + 1}
+                </span>
+                <button class="font-mono text-sm text-blue-600 hover:text-blue-800 hover:underline" 
+                        onclick="showAccountDetail('${address}')">
+                    ${address}
+                </button>
+            </div>
+            <div class="flex gap-2">
+                <button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm"
+                        onclick="showAccountDetail('${address}')">Analyze</button>
+                <button class="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm"
+                        onclick="setTargetAddress('${address}')">Set as Target</button>
+            </div>
+        </div>
+    `).join('');
+}
+
+async function analyzeLeaderboard() {
+    const addresses = Array.from(document.querySelectorAll('#leaderboardAddresses .font-mono'))
+        .map(el => el.textContent.trim());
+    
+    if (addresses.length === 0) {
+        showToast('No leaderboard addresses to analyze', 'error');
+        return;
+    }
+    
+    // Set addresses in analyzer and switch to that tab
+    document.getElementById('addressesToAnalyze').value = addresses.join('\n');
+    document.querySelector('[data-tab="analyzer"]').click();
+    
+    // Start analysis
+    await analyzeAccounts();
+}
+
+async function loadSessionInfo() {
+    try {
+        const response = await fetch('/api/copy-trading/session-info');
+        const session = await response.json();
+        
+        document.getElementById('sessionEnabled').textContent = session.enabled ? 'Active' : 'Inactive';
+        document.getElementById('sessionStartTime').textContent = session.start_time ? 
+            new Date(session.start_time).toLocaleString() : '-';
+        document.getElementById('sessionDuration').textContent = session.session_duration_seconds ? 
+            (session.session_duration_seconds / 3600).toFixed(1) + ' hours' : '-';
+        document.getElementById('sessionLastCheck').textContent = session.last_check_time ? 
+            new Date(session.last_check_time).toLocaleString() : '-';
+        
+    } catch (error) {
+        console.error('Error loading session info:', error);
+        showToast('Failed to load session info', 'error');
+    }
+}
+
+async function resetState() {
+    if (!confirm('Are you sure you want to reset the copy trading state? This will clear all tracked positions and trade history.')) {
+        return;
+    }
+    
+    try {
+        const response = await fetch('/api/copy-trading/reset-state', {
+            method: 'POST'
+        });
+        
+        const result = await response.json();
+        
+        if (response.ok) {
+            showToast('Copy trading state reset successfully', 'success');
+            loadCopyTradingStatus();
+            loadSessionInfo();
+        } else {
+            showToast(result.detail || 'Failed to reset state', 'error');
+        }
+        
+    } catch (error) {
+        console.error('Error resetting state:', error);
+        showToast('Failed to reset state', 'error');
+    }
+}
+
+async function showAccountDetail(address) {
+    // Show modal and loading state
+    document.getElementById('accountDetailModal').classList.remove('hidden');
+    document.getElementById('accountDetailModal').classList.add('flex');
+    document.getElementById('accountDetailLoading').classList.remove('hidden');
+    document.getElementById('accountDetailContent').classList.add('hidden');
+    
+    try {
+        const response = await fetch('/api/copy-trading/analyze-single-account', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({address: address})
+        });
+        
+        if (!response.ok) {
+            const error = await response.json();
+            throw new Error(error.detail || 'Failed to analyze account');
+        }
+        
+        const data = await response.json();
+        displayAccountDetail(data);
+        
+    } catch (error) {
+        console.error('Error fetching account details:', error);
+        document.getElementById('accountDetailContent').innerHTML = `
+            <div class="bg-red-50 border border-red-200 rounded-lg p-4">
+                <div class="text-red-600 text-sm font-medium">Error</div>
+                <div class="text-red-800 text-sm mt-1">${error.message}</div>
+            </div>
+        `;
+        document.getElementById('accountDetailContent').classList.remove('hidden');
+    } finally {
+        document.getElementById('accountDetailLoading').classList.add('hidden');
+    }
+}
+
+function displayAccountDetail(data) {
+    const stats = data.stats;
+    const positions = data.current_positions;
+    const trades = data.recent_trades;
+    const positionSummary = data.position_summary;
+    const recommendation = data.recommendation;
+    
+    const lastTradeDate = stats.last_trade_timestamp > 0 ? 
+        new Date(stats.last_trade_timestamp).toLocaleString() : 'N/A';
+    
+    const content = `
+        <!-- Account Overview -->
+        <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
+            <div class="lg:col-span-2">
+                <div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
+                    <h4 class="text-lg font-semibold text-gray-800 mb-3">📋 Account Overview</h4>
+                    <div class="grid grid-cols-2 gap-4 text-sm">
+                        <div><span class="font-medium">Address:</span> ${stats.address}</div>
+                        <div><span class="font-medium">Analysis Period:</span> ${stats.analysis_period_days} days</div>
+                        <div><span class="font-medium">Last Trade:</span> ${lastTradeDate}</div>
+                        <div><span class="font-medium">Trading Type:</span> ${data.trading_type_display}</div>
+                    </div>
+                </div>
+            </div>
+            <div>
+                <div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
+                    <h4 class="text-lg font-semibold text-gray-800 mb-3">🏆 Score & Recommendation</h4>
+                    <div class="text-center">
+                        <div class="text-2xl font-bold mb-2 ${stats.relative_score >= 60 ? 'text-green-600' : 
+                            stats.relative_score >= 40 ? 'text-yellow-600' : 
+                            stats.relative_score >= 20 ? 'text-orange-600' : 'text-red-600'}">
+                            ${stats.relative_score?.toFixed(1) || '0'}/100
+                        </div>
+                        <div class="text-sm font-medium">${recommendation.overall}</div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Current Positions -->
+        <div class="mb-6">
+            <h4 class="text-lg font-semibold text-gray-800 mb-4">📊 Current Positions (${positions.length} active)</h4>
+            ${positions.length > 0 ? `
+                <div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 mb-4">
+                    ${positions.map(pos => `
+                        <div class="bg-white border border-gray-200 rounded-lg p-4">
+                            <div class="flex items-center justify-between mb-2">
+                                <div class="flex items-center">
+                                    <span class="text-lg mr-2">${pos.side_emoji}</span>
+                                    <span class="font-medium">${pos.coin}</span>
+                                    <span class="ml-2 text-xs px-2 py-1 rounded-full ${pos.side === 'long' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">
+                                        ${pos.side.toUpperCase()}
+                                    </span>
+                                </div>
+                                <span class="text-lg">${pos.pnl_emoji}</span>
+                            </div>
+                            <div class="text-sm text-gray-600 space-y-1">
+                                <div>Size: ${pos.size.toFixed(6)} ${pos.coin}</div>
+                                <div>Entry: $${pos.entry_price.toFixed(4)}</div>
+                                <div>Mark: $${pos.mark_price.toFixed(4)}</div>
+                                <div class="${pos.unrealized_pnl >= 0 ? 'text-green-600' : 'text-red-600'}">
+                                    Unrealized: $${pos.unrealized_pnl.toFixed(2)}
+                                </div>
+                                <div>Leverage: ${pos.leverage.toFixed(1)}x</div>
+                            </div>
+                        </div>
+                    `).join('')}
+                </div>
+                <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
+                    <div class="grid grid-cols-3 gap-4 text-center">
+                        <div>
+                            <div class="text-sm text-blue-600 font-medium">Total Value</div>
+                            <div class="text-lg font-semibold text-blue-800">$${positionSummary.total_position_value.toFixed(2)}</div>
+                        </div>
+                        <div>
+                            <div class="text-sm text-blue-600 font-medium">Unrealized PnL</div>
+                            <div class="text-lg font-semibold ${positionSummary.total_unrealized_pnl >= 0 ? 'text-green-600' : 'text-red-600'}">
+                                $${positionSummary.total_unrealized_pnl.toFixed(2)}
+                            </div>
+                        </div>
+                        <div>
+                            <div class="text-sm text-blue-600 font-medium">Positions</div>
+                            <div class="text-lg font-semibold text-blue-800">${positionSummary.position_count}</div>
+                        </div>
+                    </div>
+                </div>
+            ` : `
+                <div class="text-center py-8 text-gray-500">
+                    💤 No active positions
+                </div>
+            `}
+        </div>
+
+        <!-- Performance Analysis -->
+        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
+            <div class="bg-white border border-gray-200 rounded-lg p-4">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">📈 Performance Metrics</h4>
+                <div class="space-y-3 text-sm">
+                    <div class="flex justify-between">
+                        <span>Total P&L:</span>
+                        <span class="${stats.total_pnl >= 0 ? 'text-green-600' : 'text-red-600'} font-semibold">
+                            $${stats.total_pnl.toFixed(2)}
+                        </span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Win Rate:</span>
+                        <span class="font-semibold">${(stats.win_rate * 100).toFixed(1)}%</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Total Trades:</span>
+                        <span class="font-semibold">${stats.total_trades}</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Avg Trade Duration:</span>
+                        <span class="font-semibold">${stats.avg_trade_duration_hours.toFixed(1)} hours</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Max Drawdown:</span>
+                        <span class="font-semibold">${(stats.max_drawdown * 100).toFixed(1)}%</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Profit Factor:</span>
+                        <span class="font-semibold">${stats.profit_factor.toFixed(2)}</span>
+                    </div>
+                </div>
+            </div>
+            <div class="bg-white border border-gray-200 rounded-lg p-4">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">🎲 Trading Patterns</h4>
+                <div class="space-y-3 text-sm">
+                    <div class="flex justify-between">
+                        <span>Trading Frequency:</span>
+                        <span class="font-semibold">${stats.trading_frequency_per_day.toFixed(1)} trades/day</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Avg Leverage:</span>
+                        <span class="font-semibold">${stats.avg_leverage_used.toFixed(1)}x</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Max Leverage:</span>
+                        <span class="font-semibold">${stats.max_leverage_used.toFixed(1)}x</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Unique Tokens:</span>
+                        <span class="font-semibold">${stats.unique_tokens_traded}</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Short Percentage:</span>
+                        <span class="font-semibold">${stats.short_percentage.toFixed(1)}%</span>
+                    </div>
+                    <div class="flex justify-between">
+                        <span>Buy/Sell Ratio:</span>
+                        <span class="font-semibold">${data.buy_sell_ratio_display}</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Trading Style & Tokens -->
+        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
+            <div class="bg-white border border-gray-200 rounded-lg p-4">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">📊 Trading Style</h4>
+                <div class="text-sm">
+                    <div class="mb-2"><span class="font-medium">Style:</span> ${stats.trading_style}</div>
+                    <div class="mb-2"><span class="font-medium">Type:</span> ${data.trading_type_display}</div>
+                    <div><span class="font-medium">Directional Capability:</span> 
+                        ${stats.short_percentage > 30 ? '✅ Excellent (can profit both ways)' : 
+                          stats.short_percentage > 10 ? '⚠️ Limited short capability' : '❌ Long-only trading'}
+                    </div>
+                </div>
+            </div>
+            <div class="bg-white border border-gray-200 rounded-lg p-4">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">🏆 Top Traded Tokens</h4>
+                <div class="flex flex-wrap gap-2">
+                    ${stats.top_tokens.map((token, i) => `
+                        <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
+                            #${i + 1} ${token}
+                        </span>
+                    `).join('')}
+                </div>
+            </div>
+        </div>
+
+        <!-- Recent Trading Activity -->
+        <div class="mb-6">
+            <h4 class="text-lg font-semibold text-gray-800 mb-4">🕒 Recent Trading Activity (last ${trades.length} trades)</h4>
+            ${trades.length > 0 ? `
+                <div class="overflow-x-auto">
+                    <table class="min-w-full bg-white border border-gray-300">
+                        <thead class="bg-gray-50">
+                            <tr>
+                                <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Asset</th>
+                                <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Side</th>
+                                <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Size</th>
+                                <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Price</th>
+                                <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Value</th>
+                                <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Time</th>
+                            </tr>
+                        </thead>
+                        <tbody class="divide-y divide-gray-200">
+                            ${trades.map((trade, index) => `
+                                <tr class="${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}">
+                                    <td class="px-4 py-2 text-sm font-medium">${trade.coin}</td>
+                                    <td class="px-4 py-2 text-sm">
+                                        <span class="flex items-center">
+                                            ${trade.side_emoji} ${trade.side.toUpperCase()}
+                                        </span>
+                                    </td>
+                                    <td class="px-4 py-2 text-sm">${trade.size.toFixed(6)}</td>
+                                    <td class="px-4 py-2 text-sm">$${trade.price.toFixed(4)}</td>
+                                    <td class="px-4 py-2 text-sm">$${trade.value.toFixed(2)}</td>
+                                    <td class="px-4 py-2 text-xs text-gray-500">${trade.timestamp}</td>
+                                </tr>
+                            `).join('')}
+                        </tbody>
+                    </table>
+                </div>
+            ` : `
+                <div class="text-center py-8 text-gray-500">
+                    No recent trading activity found
+                </div>
+            `}
+        </div>
+
+        <!-- Copy Trading Evaluation -->
+        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+            <div class="bg-white border border-gray-200 rounded-lg p-4">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">🎯 Copy Trading Evaluation</h4>
+                <div class="space-y-2">
+                    ${recommendation.evaluation_points.map(point => `
+                        <div class="text-sm">${point}</div>
+                    `).join('')}
+                </div>
+            </div>
+            <div class="bg-white border border-gray-200 rounded-lg p-4">
+                <h4 class="text-lg font-semibold text-gray-800 mb-4">💡 Copy Trading Recommendations</h4>
+                ${stats.is_copyable && stats.relative_score >= 40 ? `
+                    <div class="space-y-3 text-sm">
+                        <div class="flex justify-between">
+                            <span>Portfolio Allocation:</span>
+                            <span class="font-semibold">${recommendation.portfolio_allocation}</span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span>Max Leverage Limit:</span>
+                            <span class="font-semibold">${recommendation.max_leverage_limit}</span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span>Expected Activity:</span>
+                            <span class="font-semibold">~${stats.trading_frequency_per_day.toFixed(1)} trades/day</span>
+                        </div>
+                        <div class="flex justify-between">
+                            <span>Avg Trade Duration:</span>
+                            <span class="font-semibold">~${stats.avg_trade_duration_hours.toFixed(1)} hours</span>
+                        </div>
+                        <div class="mt-4 pt-4 border-t border-gray-200">
+                            <button class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition duration-200"
+                                    onclick="setTargetAddress('${stats.address}'); closeAccountDetailModal();">
+                                Set as Copy Target
+                            </button>
+                        </div>
+                    </div>
+                ` : `
+                    <div class="bg-red-50 border border-red-200 rounded-lg p-3">
+                        <div class="text-red-600 text-sm font-medium">❌ NOT RECOMMENDED FOR COPY TRADING</div>
+                        <div class="text-red-800 text-sm mt-1">${stats.copyability_reason}</div>
+                    </div>
+                `}
+            </div>
+        </div>
+    `;
+    
+    document.getElementById('accountDetailContent').innerHTML = content;
+    document.getElementById('accountDetailContent').classList.remove('hidden');
+}
+
+function closeAccountDetailModal() {
+    document.getElementById('accountDetailModal').classList.add('hidden');
+    document.getElementById('accountDetailModal').classList.remove('flex');
+}
+
+function setTargetAddress(address) {
+    document.getElementById('targetAddress').value = address;
+    showToast(`Target address set to ${address.substring(0, 10)}...`, 'success');
+}
+
+function showToast(message, type = 'info') {
+    // Use existing toast system
+    window.showToast && window.showToast(message, type);
+}
+</script>
+{% endblock %} 

+ 244 - 0
src/web/templates/dashboard.html

@@ -0,0 +1,244 @@
+{% extends "base.html" %}
+
+{% block title %}Dashboard - Hyperliquid Trading Bot{% endblock %}
+
+{% block content %}
+<div class="px-4 sm:px-0">
+    <!-- Page Header -->
+    <div class="flex justify-between items-center mb-6">
+        <div>
+            <h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
+            <p class="text-gray-600">Overview of your trading performance and positions</p>
+        </div>
+        <div class="flex items-center space-x-3">
+            <!-- API Key Input (for testing) -->
+            <div class="hidden sm:flex items-center space-x-2">
+                <label for="api-key-input" class="text-sm text-gray-600">API Key:</label>
+                <input 
+                    type="password" 
+                    id="api-key-input" 
+                    placeholder="Enter API key"
+                    class="px-3 py-1 border border-gray-300 rounded text-sm w-40"
+                >
+            </div>
+            <button 
+                id="refresh-dashboard" 
+                class="btn-primary"
+                title="Refresh Dashboard"
+            >
+                <i class="fas fa-sync-alt mr-2"></i>
+                Refresh
+            </button>
+        </div>
+    </div>
+
+    <!-- Summary Cards -->
+    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
+        <!-- Account Balance -->
+        <div class="metric-card" data-summary="balance">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-wallet text-blue-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Account Balance</p>
+                    <p class="text-2xl font-bold text-gray-900" data-value>$0.00</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- Total P&L -->
+        <div class="metric-card" data-summary="total_pnl">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-chart-line text-green-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Total P&L</p>
+                    <p class="text-2xl font-bold" data-value>$0.00</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- Win Rate -->
+        <div class="metric-card" data-summary="win_rate">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-target text-purple-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Win Rate</p>
+                    <p class="text-2xl font-bold text-gray-900" data-value>0.0%</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- Open Positions -->
+        <div class="metric-card" data-summary="open_positions">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-chart-area text-orange-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Open Positions</p>
+                    <p class="text-2xl font-bold text-gray-900" data-value>0</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Additional Metrics Row -->
+    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
+        <!-- Total Return % -->
+        <div class="metric-card" data-summary="total_return_pct">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-percentage text-indigo-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Total Return</p>
+                    <p class="text-2xl font-bold" data-value>0.0%</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- Total Trades -->
+        <div class="metric-card" data-summary="total_trades">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-exchange-alt text-teal-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Total Trades</p>
+                    <p class="text-2xl font-bold text-gray-900" data-value>0</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- Profit Factor -->
+        <div class="metric-card" data-summary="profit_factor">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-balance-scale text-yellow-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Profit Factor</p>
+                    <p class="text-2xl font-bold text-gray-900" data-value>0.00</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- Max Drawdown -->
+        <div class="metric-card" data-summary="max_drawdown_pct">
+            <div class="flex items-center">
+                <div class="flex-shrink-0">
+                    <i class="fas fa-arrow-down text-red-500 text-2xl"></i>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm font-medium text-gray-600">Max Drawdown</p>
+                    <p class="text-2xl font-bold text-red-600" data-value>0.0%</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Main Content Grid -->
+    <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
+        <!-- Positions Card -->
+        <div class="lg:col-span-2">
+            <div class="card">
+                <div class="flex justify-between items-center mb-4">
+                    <h2 class="text-lg font-semibold text-gray-900">
+                        <i class="fas fa-chart-area mr-2 text-blue-600"></i>
+                        Open Positions
+                    </h2>
+                    <span class="text-sm text-gray-500">Live P&L</span>
+                </div>
+                <div id="positions-container" class="space-y-4">
+                    <!-- Positions will be loaded here -->
+                    <div class="text-center py-8 text-gray-500">
+                        <i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
+                        <p>Loading positions...</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Market Overview -->
+        <div>
+            <div class="card">
+                <h2 class="text-lg font-semibold text-gray-900 mb-4">
+                    <i class="fas fa-globe mr-2 text-green-600"></i>
+                    Market Overview
+                </h2>
+                <div id="market-overview-container" class="space-y-3">
+                    <!-- Market data will be loaded here -->
+                    <div class="text-center py-8 text-gray-500">
+                        <i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
+                        <p>Loading market data...</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Recent Trades -->
+    <div class="mt-8">
+        <div class="card">
+            <div class="flex justify-between items-center mb-4">
+                <h2 class="text-lg font-semibold text-gray-900">
+                    <i class="fas fa-history mr-2 text-purple-600"></i>
+                    Recent Trades
+                </h2>
+                <span class="text-sm text-gray-500">Last 10 completed trades</span>
+            </div>
+            <div id="recent-trades-container" class="space-y-2">
+                <!-- Recent trades will be loaded here -->
+                <div class="text-center py-8 text-gray-500">
+                    <i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
+                    <p>Loading recent trades...</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Quick Actions (for future phases) -->
+    <div class="mt-8">
+        <div class="card">
+            <h2 class="text-lg font-semibold text-gray-900 mb-4">
+                <i class="fas fa-bolt mr-2 text-yellow-600"></i>
+                Quick Actions
+            </h2>
+            <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
+                <button class="btn-primary" disabled>
+                    <i class="fas fa-arrow-up mr-2"></i>
+                    Long {{ config.default_token }}
+                </button>
+                <button class="btn-danger" disabled>
+                    <i class="fas fa-arrow-down mr-2"></i>
+                    Short {{ config.default_token }}
+                </button>
+                <button class="btn-secondary" disabled>
+                    <i class="fas fa-times mr-2"></i>
+                    Close All
+                </button>
+                <button class="btn-secondary" disabled>
+                    <i class="fas fa-copy mr-2"></i>
+                    Copy Trading
+                </button>
+            </div>
+            <p class="text-sm text-gray-500 mt-3">
+                <i class="fas fa-info-circle mr-1"></i>
+                Trading functionality will be available in Phase 2
+            </p>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<script>
+    // Dashboard-specific JavaScript can go here
+    console.log('Dashboard page loaded');
+</script>
+{% endblock %} 

+ 1 - 1
trading_bot.py

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

+ 481 - 0
uv.lock

@@ -109,6 +109,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" },
 ]
 
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
 [[package]]
 name = "anyio"
 version = "4.9.0"
@@ -234,6 +243,27 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
 ]
 
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
 [[package]]
 name = "cryptography"
 version = "45.0.5"
@@ -275,6 +305,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" },
 ]
 
+[[package]]
+name = "fastapi"
+version = "0.115.14"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pydantic" },
+    { name = "starlette" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" },
+]
+
 [[package]]
 name = "frozenlist"
 version = "1.7.0"
@@ -374,6 +418,35 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
 ]
 
+[[package]]
+name = "httptools"
+version = "0.6.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" },
+    { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" },
+    { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" },
+    { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" },
+    { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" },
+    { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" },
+    { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
+]
+
 [[package]]
 name = "httpx"
 version = "0.28.1"
@@ -417,6 +490,66 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
 ]
 
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
+    { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
+    { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
+    { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
+    { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
+    { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
+    { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+    { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+    { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+    { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+    { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+    { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+    { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+]
+
 [[package]]
 name = "multidict"
 version = "6.6.3"
@@ -750,6 +883,86 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
 ]
 
+[[package]]
+name = "pydantic"
+version = "2.11.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "annotated-types" },
+    { name = "pydantic-core" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
+    { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
+    { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
+    { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
+    { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
+    { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
+    { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
+    { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
+    { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
+    { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
+    { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
+    { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
+    { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
+    { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
+    { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
+    { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
+    { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
+    { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
+    { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
+]
+
 [[package]]
 name = "python-dateutil"
 version = "2.9.0.post0"
@@ -771,6 +984,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
 ]
 
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
+]
+
 [[package]]
 name = "python-telegram-bot"
 version = "22.2"
@@ -792,6 +1014,41 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
 ]
 
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
+    { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
+    { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
+    { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
+    { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
+    { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+    { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+    { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+    { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+    { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+    { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
 [[package]]
 name = "requests"
 version = "2.32.4"
@@ -834,6 +1091,30 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
 ]
 
+[[package]]
+name = "sse-starlette"
+version = "2.3.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.46.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
+]
+
 [[package]]
 name = "trader-hyperliquid"
 version = "1.0.0"
@@ -841,26 +1122,38 @@ source = { editable = "." }
 dependencies = [
     { name = "aiofiles" },
     { name = "aiohttp" },
+    { name = "fastapi" },
     { name = "hyperliquid" },
+    { name = "jinja2" },
     { name = "numpy" },
     { name = "pandas" },
     { name = "psutil" },
     { name = "python-dotenv" },
+    { name = "python-multipart" },
     { name = "python-telegram-bot" },
     { name = "requests" },
+    { name = "sse-starlette" },
+    { name = "uvicorn", extra = ["standard"] },
+    { name = "websockets" },
 ]
 
 [package.metadata]
 requires-dist = [
     { name = "aiofiles" },
     { name = "aiohttp" },
+    { name = "fastapi", specifier = ">=0.104.0" },
     { name = "hyperliquid" },
+    { name = "jinja2", specifier = ">=3.1.0" },
     { name = "numpy" },
     { name = "pandas" },
     { name = "psutil" },
     { name = "python-dotenv" },
+    { name = "python-multipart", specifier = ">=0.0.6" },
     { name = "python-telegram-bot" },
     { name = "requests" },
+    { name = "sse-starlette", specifier = ">=1.6.0" },
+    { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
+    { name = "websockets", specifier = ">=12.0" },
 ]
 
 [package.metadata.requires-dev]
@@ -875,6 +1168,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
 ]
 
+[[package]]
+name = "typing-inspection"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
+]
+
 [[package]]
 name = "tzdata"
 version = "2025.2"
@@ -893,6 +1198,182 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
 ]
 
+[[package]]
+name = "uvicorn"
+version = "0.35.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
+]
+
+[package.optional-dependencies]
+standard = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "httptools" },
+    { name = "python-dotenv" },
+    { name = "pyyaml" },
+    { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+    { name = "watchfiles" },
+    { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" },
+    { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" },
+    { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" },
+    { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" },
+    { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" },
+    { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" },
+    { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" },
+    { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" },
+    { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8b/78/7401154b78ab484ccaaeef970dc2af0cb88b5ba8a1b415383da444cdd8d3/watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2", size = 405751, upload-time = "2025-06-15T19:05:07.679Z" },
+    { url = "https://files.pythonhosted.org/packages/76/63/e6c3dbc1f78d001589b75e56a288c47723de28c580ad715eb116639152b5/watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c", size = 397313, upload-time = "2025-06-15T19:05:08.764Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/a2/8afa359ff52e99af1632f90cbf359da46184207e893a5f179301b0c8d6df/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d", size = 450792, upload-time = "2025-06-15T19:05:09.869Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/bf/7446b401667f5c64972a57a0233be1104157fc3abf72c4ef2666c1bd09b2/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7", size = 458196, upload-time = "2025-06-15T19:05:11.91Z" },
+    { url = "https://files.pythonhosted.org/packages/58/2f/501ddbdfa3fa874ea5597c77eeea3d413579c29af26c1091b08d0c792280/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c", size = 484788, upload-time = "2025-06-15T19:05:13.373Z" },
+    { url = "https://files.pythonhosted.org/packages/61/1e/9c18eb2eb5c953c96bc0e5f626f0e53cfef4bd19bd50d71d1a049c63a575/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575", size = 597879, upload-time = "2025-06-15T19:05:14.725Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/6c/1467402e5185d89388b4486745af1e0325007af0017c3384cc786fff0542/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8", size = 477447, upload-time = "2025-06-15T19:05:15.775Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/a1/ec0a606bde4853d6c4a578f9391eeb3684a9aea736a8eb217e3e00aa89a1/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f", size = 453145, upload-time = "2025-06-15T19:05:17.17Z" },
+    { url = "https://files.pythonhosted.org/packages/90/b9/ef6f0c247a6a35d689fc970dc7f6734f9257451aefb30def5d100d6246a5/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4", size = 626539, upload-time = "2025-06-15T19:05:18.557Z" },
+    { url = "https://files.pythonhosted.org/packages/34/44/6ffda5537085106ff5aaa762b0d130ac6c75a08015dd1621376f708c94de/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d", size = 624472, upload-time = "2025-06-15T19:05:19.588Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/e3/71170985c48028fa3f0a50946916a14055e741db11c2e7bc2f3b61f4d0e3/watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2", size = 279348, upload-time = "2025-06-15T19:05:20.856Z" },
+    { url = "https://files.pythonhosted.org/packages/89/1b/3e39c68b68a7a171070f81fc2561d23ce8d6859659406842a0e4bebf3bba/watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12", size = 292607, upload-time = "2025-06-15T19:05:21.937Z" },
+    { url = "https://files.pythonhosted.org/packages/61/9f/2973b7539f2bdb6ea86d2c87f70f615a71a1fc2dba2911795cea25968aea/watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a", size = 285056, upload-time = "2025-06-15T19:05:23.12Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" },
+    { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" },
+    { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" },
+    { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" },
+    { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" },
+    { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" },
+    { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" },
+    { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" },
+    { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" },
+    { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" },
+    { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" },
+    { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" },
+    { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" },
+    { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" },
+    { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" },
+    { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" },
+    { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" },
+    { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" },
+    { url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" },
+    { url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" },
+    { url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" },
+    { url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" },
+    { url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" },
+    { url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" },
+    { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" },
+    { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" },
+    { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/6b/686dcf5d3525ad17b384fd94708e95193529b460a1b7bf40851f1328ec6e/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3", size = 406910, upload-time = "2025-06-15T19:06:49.335Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816, upload-time = "2025-06-15T19:06:50.433Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584, upload-time = "2025-06-15T19:06:51.834Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/d3/254cea30f918f489db09d6a8435a7de7047f8cb68584477a515f160541d6/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792", size = 454009, upload-time = "2025-06-15T19:06:52.896Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
+    { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
+    { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
+    { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
+    { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
+    { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
+    { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
+    { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
+    { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
+    { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
+    { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
+    { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
+    { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
+    { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
+    { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
+    { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
+]
+
 [[package]]
 name = "yarl"
 version = "1.20.1"

+ 129 - 0
web_start.py

@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+"""
+Web Server Entry Point for Hyperliquid Trading Bot
+
+This script starts the FastAPI web server that provides a web interface
+for the trading bot. It can be run alongside the existing Telegram bot.
+"""
+
+import asyncio
+import logging
+import uvicorn
+from contextlib import asynccontextmanager
+
+from src.config.config import Config
+from src.config.logging_config import setup_logging
+from src.trading.trading_engine import TradingEngine
+from src.monitoring.monitoring_coordinator import MonitoringCoordinator
+from src.notifications.notification_manager import NotificationManager
+from src.web.app import create_app, initialize_app_dependencies
+
+# Setup logging
+setup_logging()
+logger = logging.getLogger(__name__)
+
+
+@asynccontextmanager
+async def lifespan(app):
+    """Application lifespan context manager."""
+    logger.info("🚀 Starting Hyperliquid Trading Bot Web UI...")
+    
+    # Validate configuration
+    if not Config.validate():
+        logger.error("❌ Configuration validation failed")
+        raise SystemExit(1)
+    
+    if not Config.WEB_ENABLED:
+        logger.error("❌ Web UI is disabled in configuration")
+        raise SystemExit(1)
+    
+    # Initialize core components
+    logger.info("🔧 Initializing trading engine...")
+    trading_engine = TradingEngine()
+    await trading_engine.async_init()
+    
+    logger.info("🔧 Initializing monitoring coordinator...")
+    notification_manager = NotificationManager()
+    monitoring_coordinator = MonitoringCoordinator(
+        trading_engine.client,
+        notification_manager,
+        Config
+    )
+    
+    # Initialize web app dependencies
+    initialize_app_dependencies(trading_engine, monitoring_coordinator)
+    
+    logger.info("✅ Web UI startup complete")
+    
+    yield
+    
+    # Cleanup
+    logger.info("🛑 Shutting down Web UI...")
+    
+    # Stop monitoring if running
+    try:
+        await monitoring_coordinator.stop()
+    except Exception as e:
+        logger.warning(f"Error stopping monitoring coordinator: {e}")
+    
+    # Close trading engine
+    try:
+        if hasattr(trading_engine, 'close'):
+            trading_engine.close()
+    except Exception as e:
+        logger.warning(f"Error closing trading engine: {e}")
+    
+    logger.info("✅ Web UI shutdown complete")
+
+
+def main():
+    """Main function to start the web server."""
+    
+    # Validate configuration first
+    if not Config.WEB_ENABLED:
+        print("❌ Web UI is disabled. Set WEB_ENABLED=true in your .env file")
+        return
+    
+    if not Config.WEB_API_KEY:
+        print("❌ WEB_API_KEY is required. Please set it in your .env file")
+        return
+    
+    # Create FastAPI app with lifespan
+    app = create_app()
+    app.router.lifespan_context = lifespan
+    
+    # Print startup info
+    print("\n" + "="*50)
+    print("🚀 HYPERLIQUID TRADING BOT WEB UI")
+    print("="*50)
+    print(f"📍 URL: http://{Config.WEB_HOST}:{Config.WEB_PORT}")
+    print(f"🔑 API Key: {Config.WEB_API_KEY[:8]}..." if Config.WEB_API_KEY else "🔑 API Key: Not set")
+    print(f"🌐 Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}")
+    print(f"📊 Default Token: {Config.DEFAULT_TRADING_TOKEN}")
+    print("="*50)
+    print("💡 Quick Start:")
+    print("1. Open the URL above in your browser")
+    print("2. Enter your API key in the dashboard")
+    print("3. View your trading performance and positions")
+    print("="*50)
+    print("\n🔄 Starting web server...\n")
+    
+    # Run the server
+    uvicorn.run(
+        app,
+        host=Config.WEB_HOST,
+        port=Config.WEB_PORT,
+        log_level="info",
+        access_log=True,
+        lifespan="on"
+    )
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print("\n👋 Web server stopped by user")
+    except Exception as e:
+        print(f"\n❌ Error starting web server: {e}")
+        logger.error(f"Error starting web server: {e}", exc_info=True)