#!/usr/bin/env python3
"""
ML Inference Server V9 - Dual Model System
===========================================

4-hour timeframe trading system with:
1. Volatility Gate: Predict if there's enough movement
2. Trend Detector: Long/Short classifier
3. Exit Optimizer: RL model for optimal exit timing

API Endpoints:
- POST /predict - Get entry signal
- POST /exit-check - Check if should exit position
- GET /health - Health check
- GET /model-info - Model information
- POST /update-price - Update price for 4h candle building
"""

import os
import sys
import json
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from collections import deque
import logging

import torch
import torch.nn as nn
import torch.nn.functional as F
import lightgbm as lgb
import joblib

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn

# ============================================================
# CONFIGURATION
# ============================================================

CONFIG = {
    "host": "0.0.0.0",
    "port": 3060,  # New port for V9
    "model_dir": "/var/www/html/bestrading.cuttalo.com/models/btc_v9",
    "historical_data": "/var/www/html/bestrading.cuttalo.com/scripts/prices_BTC_EUR_2025_full.csv",
    "candle_minutes": 240,  # 4 hours
    "lookback_periods": 30,
    "confidence_threshold": 0.70,
    "fee_rate": 0.005,
    "preload_candles": 60,  # Pre-load 60 4h candles (~10 days)
    "stop_loss": 0.02,      # 2% stop loss
    "take_profit": 0.03,    # 3% take profit
}

# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ml-inference-v9")


# ============================================================
# EXIT OPTIMIZER MODEL (must match training)
# ============================================================

class ExitOptimizer(nn.Module):
    def __init__(self, input_dim: int = 8, hidden_dim: int = 128):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.LayerNorm(hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.1),
            nn.Linear(hidden_dim, hidden_dim),
            nn.LayerNorm(hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.1),
        )
        self.policy = nn.Linear(hidden_dim, 2)
        self.value = nn.Linear(hidden_dim, 1)

    def forward(self, x):
        features = self.net(x)
        action_logits = self.policy(features)
        value = self.value(features)
        return action_logits, value

    def get_action(self, x, deterministic=True):
        action_logits, value = self.forward(x)
        probs = F.softmax(action_logits, dim=-1)
        if deterministic:
            action = probs.argmax(dim=-1)
        else:
            dist = torch.distributions.Categorical(probs)
            action = dist.sample()
        return action, probs, value


# ============================================================
# CANDLE BUILDER
# ============================================================

class CandleBuilder:
    """Build 4h candles from minute prices."""

    def __init__(self, candle_minutes: int = 240):
        self.candle_minutes = candle_minutes
        self.current_candle = None
        self.completed_candles = deque(maxlen=100)
        self.minute_prices = []
        self.candle_start_time = None

    def add_price(self, price: float, timestamp: datetime = None) -> Optional[Dict]:
        """Add a price tick. Returns completed candle if any."""
        if timestamp is None:
            timestamp = datetime.utcnow()

        # Initialize candle start time
        if self.candle_start_time is None:
            # Align to 4h boundary
            minutes = timestamp.hour * 60 + timestamp.minute
            boundary = (minutes // self.candle_minutes) * self.candle_minutes
            self.candle_start_time = timestamp.replace(
                hour=boundary // 60,
                minute=boundary % 60,
                second=0,
                microsecond=0
            )

        # Check if we've crossed into a new candle
        time_in_candle = (timestamp - self.candle_start_time).total_seconds() / 60
        if time_in_candle >= self.candle_minutes:
            # Complete current candle
            completed = self._complete_candle()

            # Start new candle
            self.candle_start_time += timedelta(minutes=self.candle_minutes)
            self.minute_prices = [price]
            self.current_candle = {
                'open': price,
                'high': price,
                'low': price,
                'close': price,
                'timestamp': self.candle_start_time,
            }

            return completed

        # Update current candle
        self.minute_prices.append(price)
        if self.current_candle is None:
            self.current_candle = {
                'open': price,
                'high': price,
                'low': price,
                'close': price,
                'timestamp': self.candle_start_time,
            }
        else:
            self.current_candle['high'] = max(self.current_candle['high'], price)
            self.current_candle['low'] = min(self.current_candle['low'], price)
            self.current_candle['close'] = price

        return None

    def _complete_candle(self) -> Optional[Dict]:
        if self.current_candle is None or len(self.minute_prices) == 0:
            return None

        candle = self.current_candle.copy()
        candle['volume'] = len(self.minute_prices)  # Proxy
        self.completed_candles.append(candle)
        return candle

    def get_dataframe(self) -> pd.DataFrame:
        """Get completed candles as DataFrame."""
        if len(self.completed_candles) == 0:
            return pd.DataFrame()

        df = pd.DataFrame(list(self.completed_candles))
        df.set_index('timestamp', inplace=True)
        return df

    def load_historical(self, csv_path: str, num_candles: int = 60) -> int:
        """Load historical 4h candles from CSV to bootstrap the system."""
        try:
            # Load raw minute data
            df = pd.read_csv(csv_path)
            df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s')
            df.set_index('timestamp', inplace=True)

            # Resample to 4h
            df_4h = df.resample(f'{self.candle_minutes}min').agg({
                'open': 'first',
                'high': 'max',
                'low': 'min',
                'close': 'last',
                'volume': 'sum'
            }).dropna()

            # Take last N candles
            df_4h = df_4h.tail(num_candles)

            # Add to completed_candles
            for idx, row in df_4h.iterrows():
                candle = {
                    'open': float(row['open']),
                    'high': float(row['high']),
                    'low': float(row['low']),
                    'close': float(row['close']),
                    'volume': float(row['volume']),
                    'timestamp': idx,
                }
                self.completed_candles.append(candle)

            # Set candle_start_time to the last candle's time + 4h
            if len(df_4h) > 0:
                last_candle_time = df_4h.index[-1]
                self.candle_start_time = last_candle_time + timedelta(minutes=self.candle_minutes)
                self.current_candle = {
                    'open': float(df_4h.iloc[-1]['close']),
                    'high': float(df_4h.iloc[-1]['close']),
                    'low': float(df_4h.iloc[-1]['close']),
                    'close': float(df_4h.iloc[-1]['close']),
                    'timestamp': self.candle_start_time,
                }

            logger.info(f"Loaded {len(df_4h)} historical 4h candles from {df_4h.index[0]} to {df_4h.index[-1]}")
            return len(df_4h)

        except Exception as e:
            logger.error(f"Failed to load historical candles: {e}")
            import traceback
            traceback.print_exc()
            return 0


# ============================================================
# FEATURE COMPUTER
# ============================================================

class FeatureComputer:
    """Compute features for V9 models."""

    def __init__(self):
        self.vol_features = [
            'vol_3', 'vol_6', 'vol_12', 'vol_24',
            'atr_3', 'atr_6', 'atr_12',
            'vol_regime', 'range_pct', 'range_sma',
            'volume_ratio',
            'hour_sin', 'hour_cos',
            'ret_1', 'ret_3', 'ret_6',
        ]

        self.trend_features = [
            'ret_1', 'ret_2', 'ret_3', 'ret_6', 'ret_12', 'ret_24',
            'sma_cross_6_12', 'sma_cross_12_24', 'sma_cross_24_48',
            'price_vs_sma12', 'price_vs_sma24',
            'rsi_norm', 'roc_3', 'roc_6', 'roc_12',
            'vol_6', 'vol_12', 'vol_regime',
            'atr_6', 'atr_12',
            'volume_ratio',
            'hour_sin', 'hour_cos',
        ]

    def compute(self, df: pd.DataFrame) -> pd.DataFrame:
        """Compute all features."""
        df = df.copy()

        # Returns
        for periods in [1, 2, 3, 6, 12, 24]:
            df[f'ret_{periods}'] = df['close'].pct_change(periods)

        # Volatility
        for periods in [3, 6, 12, 24]:
            df[f'vol_{periods}'] = df['ret_1'].rolling(periods).std() * np.sqrt(6 * 365)

        # ATR
        df['tr'] = np.maximum(
            df['high'] - df['low'],
            np.maximum(
                abs(df['high'] - df['close'].shift(1)),
                abs(df['low'] - df['close'].shift(1))
            )
        )
        for periods in [3, 6, 12]:
            df[f'atr_{periods}'] = df['tr'].rolling(periods).mean() / df['close']

        # Trend
        for periods in [6, 12, 24, 48]:
            df[f'sma_{periods}'] = df['close'].rolling(periods).mean()

        df['sma_cross_6_12'] = (df['sma_6'] - df['sma_12']) / df['close']
        df['sma_cross_12_24'] = (df['sma_12'] - df['sma_24']) / df['close']
        df['sma_cross_24_48'] = (df['sma_24'] - df['sma_48']) / df['close']
        df['price_vs_sma12'] = (df['close'] - df['sma_12']) / df['sma_12']
        df['price_vs_sma24'] = (df['close'] - df['sma_24']) / df['sma_24']

        # RSI
        delta = df['close'].diff()
        gain = delta.where(delta > 0, 0).rolling(14).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
        rs = gain / (loss + 1e-10)
        df['rsi'] = 100 - (100 / (1 + rs))
        df['rsi_norm'] = (df['rsi'] - 50) / 50

        # ROC
        for periods in [3, 6, 12]:
            df[f'roc_{periods}'] = df['close'].pct_change(periods)

        # Volume
        df['volume_sma'] = df['volume'].rolling(12).mean()
        df['volume_ratio'] = df['volume'] / (df['volume_sma'] + 1e-10)

        # Volatility regime
        df['vol_regime'] = df['vol_6'] / (df['vol_24'] + 1e-10)

        # Range
        df['range_pct'] = (df['high'] - df['low']) / df['close']
        df['range_sma'] = df['range_pct'].rolling(6).mean()

        # Hour (cyclical)
        df['hour'] = df.index.hour
        df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
        df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)

        return df


# ============================================================
# INFERENCE ENGINE
# ============================================================

class InferenceEngineV9:
    """Main inference engine for V9 Dual Model system."""

    def __init__(self, config: dict):
        self.config = config
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

        # Models
        self.vol_gate_model = None
        self.long_model = None
        self.short_model = None
        self.exit_model = None

        # Scalers
        self.vol_scaler = None
        self.trend_scaler = None

        # Feature lists
        self.vol_features = None
        self.trend_features = None

        # State
        self.candle_builder = CandleBuilder(config['candle_minutes'])
        self.feature_computer = FeatureComputer()

        # Position tracking
        self.position = 0  # 0=flat, 1=long, -1=short
        self.entry_price = 0
        self.entry_time = None
        self.max_profit_seen = 0
        self.steps_in_position = 0

        self.model_info = {}

        logger.info(f"InferenceEngineV9 initialized on {self.device}")

    def load_models(self) -> bool:
        """Load all models."""
        try:
            model_dir = self.config['model_dir']

            # Volatility Gate
            self.vol_gate_model = lgb.Booster(model_file=os.path.join(model_dir, 'vol_gate_model.txt'))
            self.vol_scaler = joblib.load(os.path.join(model_dir, 'vol_scaler.pkl'))
            self.vol_features = joblib.load(os.path.join(model_dir, 'vol_features.pkl'))

            # Trend Detector
            self.long_model = lgb.Booster(model_file=os.path.join(model_dir, 'long_model.txt'))
            self.short_model = lgb.Booster(model_file=os.path.join(model_dir, 'short_model.txt'))
            self.trend_scaler = joblib.load(os.path.join(model_dir, 'trend_scaler.pkl'))
            self.trend_features = joblib.load(os.path.join(model_dir, 'trend_features.pkl'))

            # Exit Optimizer
            exit_checkpoint = torch.load(
                os.path.join(model_dir, 'exit_optimizer_best.pt'),
                map_location=self.device,
                weights_only=False
            )
            self.exit_model = ExitOptimizer(input_dim=8, hidden_dim=128)
            self.exit_model.load_state_dict(exit_checkpoint['model_state_dict'])
            self.exit_model.to(self.device)
            self.exit_model.eval()

            self.model_info = {
                'version': 'V9_DUAL_MODEL',
                'loaded_at': datetime.now().isoformat(),
                'exit_avg_pnl': exit_checkpoint.get('avg_pnl', 'unknown'),
                'exit_win_rate': exit_checkpoint.get('win_rate', 'unknown'),
                'timeframe': '4h',
            }

            logger.info("All V9 models loaded successfully")
            return True

        except Exception as e:
            logger.error(f"Failed to load models: {e}")
            import traceback
            traceback.print_exc()
            return False

    def load_historical_data(self) -> int:
        """Load historical candles to bootstrap the system (no waiting!)."""
        csv_path = self.config.get('historical_data')
        num_candles = self.config.get('preload_candles', 60)

        if csv_path and os.path.exists(csv_path):
            loaded = self.candle_builder.load_historical(csv_path, num_candles)
            logger.info(f"Pre-loaded {loaded} historical candles - SYSTEM READY TO TRADE!")
            return loaded
        else:
            logger.warning(f"Historical data not found at {csv_path}")
            return 0

    def update_price(self, price: float, timestamp: datetime = None) -> Optional[Dict]:
        """Update with new price. Returns completed candle if any."""
        return self.candle_builder.add_price(price, timestamp)

    def get_entry_signal(self) -> Dict[str, Any]:
        """Get entry signal based on current state."""
        if self.vol_gate_model is None:
            return {'error': 'Models not loaded'}

        df = self.candle_builder.get_dataframe()
        if len(df) < 50:
            return {
                'signal': 'WAIT',
                'reason': f'Accumulating data: {len(df)}/50 candles',
                'candles': len(df),
            }

        # Compute features
        df = self.feature_computer.compute(df)
        df = df.dropna()

        if len(df) == 0:
            return {'signal': 'WAIT', 'reason': 'No valid features'}

        latest = df.iloc[-1]

        # Step 1: Volatility Gate
        try:
            vol_X = self.vol_scaler.transform(latest[self.vol_features].values.reshape(1, -1))
            vol_proba = self.vol_gate_model.predict(vol_X)[0]
        except Exception as e:
            return {'signal': 'ERROR', 'reason': f'Vol gate error: {e}'}

        if vol_proba < 0.5:
            return {
                'signal': 'WAIT',
                'reason': 'Low volatility expected',
                'vol_probability': float(vol_proba),
            }

        # Step 2: Trend Detection
        try:
            trend_X = self.trend_scaler.transform(latest[self.trend_features].values.reshape(1, -1))
            long_proba = self.long_model.predict(trend_X)[0]
            short_proba = self.short_model.predict(trend_X)[0]
        except Exception as e:
            return {'signal': 'ERROR', 'reason': f'Trend detection error: {e}'}

        confidence_threshold = self.config['confidence_threshold']

        if long_proba > confidence_threshold and long_proba > short_proba:
            return {
                'signal': 'LONG',
                'confidence': float(long_proba),
                'vol_probability': float(vol_proba),
                'price': float(latest['close']),
            }
        elif short_proba > confidence_threshold and short_proba > long_proba:
            return {
                'signal': 'SHORT',
                'confidence': float(short_proba),
                'vol_probability': float(vol_proba),
                'price': float(latest['close']),
            }
        else:
            return {
                'signal': 'HOLD',
                'reason': 'No confident signal',
                'long_proba': float(long_proba),
                'short_proba': float(short_proba),
                'vol_probability': float(vol_proba),
            }

    def open_position(self, direction: int, price: float):
        """Open a position."""
        self.position = direction
        self.entry_price = price
        self.entry_time = datetime.utcnow()
        self.max_profit_seen = 0
        self.steps_in_position = 0
        logger.info(f"Position opened: {'LONG' if direction == 1 else 'SHORT'} @ {price}")

    def check_exit(self, current_price: float) -> Dict[str, Any]:
        """Check if should exit current position."""
        if self.position == 0:
            return {'action': 'NONE', 'reason': 'No position'}

        if self.exit_model is None:
            return {'action': 'ERROR', 'reason': 'Exit model not loaded'}

        # Calculate P&L
        if self.position == 1:  # Long
            pnl = (current_price - self.entry_price) / self.entry_price
        else:  # Short
            pnl = (self.entry_price - current_price) / self.entry_price

        self.max_profit_seen = max(self.max_profit_seen, pnl)
        self.steps_in_position += 1

        # === STOP LOSS CHECK (2%) ===
        stop_loss = self.config.get('stop_loss', 0.02)
        if pnl <= -stop_loss:
            logger.warning(f"STOP LOSS triggered: PnL {pnl*100:.2f}% <= -{stop_loss*100:.0f}%")
            return {
                'action': 'EXIT',
                'reason': 'stop_loss',
                'pnl': float(pnl),
                'net_pnl': float(pnl - self.config['fee_rate']),
                'exit_probability': 1.0,
                'steps_in_position': self.steps_in_position,
                'max_profit_seen': float(self.max_profit_seen),
            }

        # === TAKE PROFIT CHECK (3%) ===
        take_profit = self.config.get('take_profit', 0.03)
        if pnl >= take_profit:
            logger.info(f"TAKE PROFIT triggered: PnL {pnl*100:.2f}% >= {take_profit*100:.0f}%")
            return {
                'action': 'EXIT',
                'reason': 'take_profit',
                'pnl': float(pnl),
                'net_pnl': float(pnl - self.config['fee_rate']),
                'exit_probability': 1.0,
                'steps_in_position': self.steps_in_position,
                'max_profit_seen': float(self.max_profit_seen),
            }

        # Get latest features for volatility
        df = self.candle_builder.get_dataframe()
        if len(df) > 0:
            df = self.feature_computer.compute(df)
            latest = df.iloc[-1]
            vol_6 = latest.get('vol_6', 0.5)
            ret_1 = latest.get('ret_1', 0)
            rsi_norm = latest.get('rsi_norm', 0)
        else:
            vol_6, ret_1, rsi_norm = 0.5, 0, 0

        # Build exit state
        exit_state = np.array([
            pnl,
            self.max_profit_seen,
            pnl - self.max_profit_seen,  # Drawdown from max
            self.steps_in_position / 12,  # Normalized time
            vol_6,
            ret_1,
            rsi_norm,
            self.position,
        ], dtype=np.float32)

        # Get exit decision
        state_tensor = torch.FloatTensor(exit_state).unsqueeze(0).to(self.device)
        with torch.no_grad():
            action, probs, value = self.exit_model.get_action(state_tensor, deterministic=True)

        exit_proba = probs[0, 1].item()  # Probability of EXIT action

        # Force exit after 12 candles (48h)
        if action.item() == 1 or self.steps_in_position >= 12:
            reason = 'model_decision' if action.item() == 1 else 'timeout'
            return {
                'action': 'EXIT',
                'reason': reason,
                'pnl': float(pnl),
                'net_pnl': float(pnl - self.config['fee_rate']),
                'exit_probability': float(exit_proba),
                'steps_in_position': self.steps_in_position,
                'max_profit_seen': float(self.max_profit_seen),
            }

        return {
            'action': 'HOLD',
            'pnl': float(pnl),
            'exit_probability': float(exit_proba),
            'steps_in_position': self.steps_in_position,
            'max_profit_seen': float(self.max_profit_seen),
        }

    def close_position(self):
        """Close current position."""
        logger.info(f"Position closed. Was {'LONG' if self.position == 1 else 'SHORT'}")
        self.position = 0
        self.entry_price = 0
        self.entry_time = None
        self.max_profit_seen = 0
        self.steps_in_position = 0


# ============================================================
# FASTAPI APP
# ============================================================

app = FastAPI(
    title="ML Inference Server V9",
    description="Dual Model Trading System (4h timeframe)",
    version="9.0.0"
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Global engine
engine: Optional[InferenceEngineV9] = None
startup_time = datetime.now()


# Request/Response models
class UpdatePriceRequest(BaseModel):
    price: float
    timestamp: Optional[str] = None


class ExitCheckRequest(BaseModel):
    current_price: float


class OpenPositionRequest(BaseModel):
    direction: int  # 1=long, -1=short
    price: float


@app.on_event("startup")
async def startup_event():
    global engine
    engine = InferenceEngineV9(CONFIG)
    engine.load_models()

    # Pre-load historical candles - READY TO TRADE IMMEDIATELY!
    loaded = engine.load_historical_data()
    logger.info(f"V9 Inference Server started with {loaded} pre-loaded candles")


@app.get("/health")
async def health():
    candle_count = len(engine.candle_builder.completed_candles) if engine else 0
    return {
        "status": "healthy",
        "models_loaded": engine.vol_gate_model is not None if engine else False,
        "device": str(engine.device) if engine else "N/A",
        "uptime": str(datetime.now() - startup_time),
        "version": "V9_DUAL_MODEL",
        "timeframe": "4h",
        "candles_ready": candle_count,
        "ready_to_trade": candle_count >= 50,
    }


@app.get("/model-info")
async def model_info():
    if not engine:
        return {"error": "Engine not initialized"}
    return engine.model_info


@app.post("/update-price")
async def update_price(request: UpdatePriceRequest):
    """Update price for candle building."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    timestamp = None
    if request.timestamp:
        timestamp = datetime.fromisoformat(request.timestamp)

    completed = engine.update_price(request.price, timestamp)

    return {
        "success": True,
        "price": request.price,
        "candle_completed": completed is not None,
        "total_candles": len(engine.candle_builder.completed_candles),
    }


@app.get("/entry-signal")
async def get_entry_signal():
    """Get entry signal."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    return engine.get_entry_signal()


@app.post("/open-position")
async def open_position(request: OpenPositionRequest):
    """Open a new position."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    engine.open_position(request.direction, request.price)
    return {
        "success": True,
        "direction": "LONG" if request.direction == 1 else "SHORT",
        "price": request.price,
    }


@app.post("/check-exit")
async def check_exit(request: ExitCheckRequest):
    """Check if should exit current position."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    return engine.check_exit(request.current_price)


@app.post("/close-position")
async def close_position():
    """Close current position."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    engine.close_position()
    return {"success": True}


@app.get("/position")
async def get_position():
    """Get current position info."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    return {
        "position": engine.position,
        "direction": "LONG" if engine.position == 1 else ("SHORT" if engine.position == -1 else "FLAT"),
        "entry_price": engine.entry_price,
        "entry_time": engine.entry_time.isoformat() if engine.entry_time else None,
        "steps_in_position": engine.steps_in_position,
        "max_profit_seen": engine.max_profit_seen,
    }


@app.get("/candles")
async def get_candles():
    """Get accumulated candles."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    candles = list(engine.candle_builder.completed_candles)
    return {
        "count": len(candles),
        "current": engine.candle_builder.current_candle,
        "latest_5": candles[-5:] if len(candles) >= 5 else candles,
    }


@app.post("/reload-historical")
async def reload_historical():
    """Reload historical candles (reset and pre-load fresh data)."""
    if not engine:
        raise HTTPException(status_code=500, detail="Engine not initialized")

    # Clear existing candles
    engine.candle_builder.completed_candles.clear()
    engine.candle_builder.current_candle = None
    engine.candle_builder.candle_start_time = None

    # Reload
    loaded = engine.load_historical_data()
    return {
        "success": True,
        "candles_loaded": loaded,
        "ready_to_trade": loaded >= 50,
    }


# ============================================================
# MAIN
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("ML INFERENCE SERVER V9 - DUAL MODEL SYSTEM")
    print("4-hour timeframe | Volatility Gate + Trend + Exit")
    print("=" * 60)
    print(f"Host: {CONFIG['host']}")
    print(f"Port: {CONFIG['port']}")
    print(f"Model dir: {CONFIG['model_dir']}")
    print("=" * 60)

    uvicorn.run(
        "inference_server_v9:app",
        host=CONFIG["host"],
        port=CONFIG["port"],
        reload=False,
        log_level="info"
    )
