"""Vizzor ML Sidecar — FastAPI server for ML inference."""

import hmac
import json
import os
import time
from contextlib import asynccontextmanager
from pathlib import Path
from uuid import uuid4

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import AliasChoices, BaseModel, Field, ConfigDict

from .model_catalog import get_model_catalog, get_training_roadmap
from .models.lstm_predictor import LSTMPredictor
from .models.signal_classifier import SignalClassifier
from .models.anomaly_detector import AnomalyDetector
from .models.rug_detector import RugDetector
from .models.wallet_classifier import WalletClassifier
from .models.sentiment_analyzer import SentimentAnalyzer
from .models.trend_scorer import TrendScorer
from .models.ta_interpreter import TAInterpreter
from .models.regime_detector import RegimeDetector
from .models.strategy_bandit import StrategyBandit
from .models.project_risk_scorer import ProjectRiskScorer
from .models.portfolio_optimizer import PortfolioOptimizer
from .models.intent_classifier import IntentClassifier
from .models.pump_detector import PumpDetectorModel
from .models.narrative_detector import NarrativeDetectorModel
from .models.divergence_detector import DivergenceDetectorModel
from .models.blockchain_cycle_analyzer import BlockchainCycleAnalyzer
from .models.target_quantile import TargetQuantileModel
from .models.conformal_interval import ConformalIntervalModel
from .models.stacking_meta import StackingMetaModel
from .models.drift_monitor import DriftMonitor
from .models.microstructure_specialist import MicrostructureSpecialist
from .models.catalyst_event_model import CatalystEventModel
from .training.train_rug import RugTrainer
from .training.train_trend import TrendTrainer
from .training.train_regime import RegimeTrainer
from .training.train_sentiment import SentimentTrainer
from .training.train_pump import PumpTrainer
from .training.train_narrative import NarrativeTrainer
from .training.train_direction import DirectionTrainer
from .training.train_target_quantile import TargetQuantileTrainer
from .training.train_conformal import ConformalIntervalTrainer
from .training.train_stacking_meta import StackingMetaTrainer
from .training.train_isotonic import IsotonicTrainer
from .training.train_drift import DriftMonitorTrainer
from .training.train_microstructure import MicrostructureTrainer
from .training.train_catalyst import CatalystTrainer
from .training.data_loader import load_direction_outcomes, load_meta_prediction_frame

# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------


VALID_HORIZONS = {"1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1mo", "1y"}
FLEX_HORIZON_PATTERN = r"^(?:\d+(?:mo|y|w|d|h|m))+$"


class FeatureVector(BaseModel):
    model_config = ConfigDict(str_max_length=32)

    rsi: float = Field(50.0, ge=0, le=100)
    macdHistogram: float = Field(0.0, ge=-1e6, le=1e6)
    bollingerPercentB: float = Field(0.5, ge=-5, le=5)
    ema12: float = Field(0.0, ge=-1e9, le=1e9)
    ema26: float = Field(0.0, ge=-1e9, le=1e9)
    atr: float = Field(0.0, ge=0, le=1e9)
    obv: float = Field(0.0, ge=-1e15, le=1e15)
    fundingRate: float = Field(0.0, ge=-1, le=1)
    fearGreed: float = Field(50.0, ge=0, le=100)
    priceChange24h: float = Field(0.0, ge=-100, le=10000)
    rsiSlope: float = Field(0.0, ge=-100, le=100)
    volumeRatio: float = Field(1.0, ge=0, le=1000)
    emaCrossoverPct: float = Field(0.0, ge=-100, le=100)
    atrPct: float = Field(0.0, ge=0, le=100)
    symbol: str = Field("BTC", max_length=20, pattern=r"^[A-Za-z0-9]{1,20}$")
    timestamp: int = Field(0, ge=0)
    horizon: str = Field(
        "4h",
        max_length=32,
        pattern=FLEX_HORIZON_PATTERN,
        description="Arbitrary minute/hour/day/week/month/year horizon, e.g. 15m, 20m, 2h, 6h, 2w, 3mo, 1y6mo",
    )


class PredictionResponse(BaseModel):
    symbol: str
    direction: str  # up | down | sideways
    probability: float
    model: str
    horizon: str
    confidence: int


class BatchRequest(BaseModel):
    features: list[FeatureVector] = Field(max_length=100)


class BatchResponse(BaseModel):
    predictions: list[PredictionResponse]


class TokenFlow(BaseModel):
    model_config = ConfigDict(str_max_length=100)

    symbol: str = Field(max_length=20)
    amount: float = Field(ge=0, le=1e18)
    from_addr: str = Field("", max_length=100)
    to_addr: str = Field("", max_length=100)
    timestamp: int = Field(0, ge=0)
    type: str = Field("transfer", max_length=30)


class FlowRequest(BaseModel):
    flows: list[TokenFlow] = Field(max_length=500)


class AnomalyResponse(BaseModel):
    symbol: str
    score: float
    isAnomaly: bool
    type: str
    details: str


class AnomaliesResponse(BaseModel):
    anomalies: list[AnomalyResponse]


# --- Rug Detection ---


class RugFeatures(BaseModel):
    bytecode_size: int = Field(0, ge=0, le=10_000_000)
    is_verified: int = Field(0, ge=0, le=1)
    holder_concentration: float = Field(0.0, ge=0, le=100)
    has_proxy: int = Field(0, ge=0, le=1)
    has_mint: int = Field(0, ge=0, le=1)
    has_pause: int = Field(0, ge=0, le=1)
    has_blacklist: int = Field(0, ge=0, le=1)
    liquidity_locked: int = Field(0, ge=0, le=1)
    buy_tax: float = Field(0.0, ge=0, le=100)
    sell_tax: float = Field(0.0, ge=0, le=100)
    contract_age_days: int = Field(0, ge=0, le=100_000)
    total_transfers: int = Field(0, ge=0, le=1_000_000_000)
    owner_balance_pct: float = Field(0.0, ge=0, le=100)
    is_open_source: int = Field(0, ge=0, le=1)
    top10_holder_pct: float = Field(0.0, ge=0, le=100)


class RugRiskFactor(BaseModel):
    factor: str
    importance: float
    value: float


class RugResponse(BaseModel):
    rug_probability: float
    risk_level: str
    risk_factors: list[RugRiskFactor]
    model: str


# --- Wallet Classification ---


class WalletFeatures(BaseModel):
    tx_count: int = Field(0, ge=0, le=10_000_000)
    avg_value_eth: float = Field(0.0, ge=0, le=1e12)
    max_value_eth: float = Field(0.0, ge=0, le=1e12)
    avg_gas_used: float = Field(0.0, ge=0, le=1e12)
    unique_recipients: int = Field(0, ge=0, le=10_000_000)
    unique_methods: int = Field(0, ge=0, le=100_000)
    time_span_hours: float = Field(0.0, ge=0, le=1e8)
    avg_interval_seconds: float = Field(3600.0, ge=0, le=1e9)
    min_interval_seconds: float = Field(60.0, ge=0, le=1e9)
    contract_interaction_pct: float = Field(0.0, ge=0, le=100)
    self_transfer_pct: float = Field(0.0, ge=0, le=100)
    high_value_tx_pct: float = Field(0.0, ge=0, le=100)
    failed_tx_pct: float = Field(0.0, ge=0, le=100)
    token_diversity: int = Field(0, ge=0, le=1_000_000)


class WalletResponse(BaseModel):
    behavior_type: str
    confidence: float
    risk_score: float
    secondary_type: str | None
    indicators: list[str]
    model: str


# --- Sentiment NLP ---


class SentimentRequest(BaseModel):
    text: str = Field(max_length=10_000)


class SentimentBatchRequest(BaseModel):
    texts: list[str] = Field(max_length=100)
    # Each text max 10k chars enforced at handler level


class SentimentResponse(BaseModel):
    sentiment: str  # bullish | bearish | neutral
    confidence: float
    score: float  # -1 to +1
    key_topics: list[str]
    model: str


class SentimentBatchResponse(BaseModel):
    results: list[SentimentResponse]


# --- Trend Scoring ---


class TrendFeatures(BaseModel):
    price_change_24h: float = Field(0.0, ge=-100, le=100_000)
    price_change_7d: float = Field(0.0, ge=-100, le=100_000)
    volume_24h: float = Field(0.0, ge=0, le=1e15)
    market_cap: float = Field(0.0, ge=0, le=1e15)
    volume_to_mcap_ratio: float = Field(0.0, ge=0, le=1000)
    rank: float = Field(0.0, ge=0, le=100_000)


class TrendScoreResponse(BaseModel):
    score: float
    direction: str
    confidence: float
    feature_importances: dict[str, float]
    model: str


# --- TA Interpretation ---


class TAFeatures(BaseModel):
    rsi: float = Field(50.0, ge=0, le=100)
    macd_histogram: float = Field(0.0, ge=-1e6, le=1e6)
    macd_line: float = Field(0.0, ge=-1e6, le=1e6)
    macd_signal: float = Field(0.0, ge=-1e6, le=1e6)
    bb_percent_b: float = Field(0.5, ge=-5, le=5)
    bb_bandwidth: float = Field(0.0, ge=0, le=1000)
    ema12: float = Field(0.0, ge=-1e9, le=1e9)
    ema26: float = Field(0.0, ge=-1e9, le=1e9)
    ema_cross_pct: float = Field(0.0, ge=-100, le=100)
    atr: float = Field(0.0, ge=0, le=1e9)
    atr_pct: float = Field(0.0, ge=0, le=100)
    obv: float = Field(0.0, ge=-1e15, le=1e15)
    price_change: float = Field(0.0, ge=-100, le=100_000)


class TASignal(BaseModel):
    name: str
    direction: str
    strength: float
    description: str


class TAResponse(BaseModel):
    signals: list[TASignal]
    weights: dict[str, float]
    composite: dict[str, object]
    model: str


# --- Strategy Bandit ---


class StrategyFeatures(BaseModel):
    model_config = ConfigDict(str_max_length=30)

    rsi: float = Field(50.0, ge=0, le=100)
    macd_histogram: float = Field(0.0, ge=-1e6, le=1e6)
    ema12: float = Field(0.0, ge=-1e9, le=1e9)
    ema26: float = Field(0.0, ge=-1e9, le=1e9)
    bollinger_pct_b: float = Field(0.5, ge=-5, le=5)
    atr: float = Field(0.0, ge=0, le=1e9)
    obv: float = Field(0.0, ge=-1e15, le=1e15)
    funding_rate: float = Field(0.0, ge=-1, le=1)
    fear_greed: float = Field(50.0, ge=0, le=100)
    price_change_24h: float = Field(0.0, ge=-100, le=100_000)
    price: float = Field(0.0, ge=0, le=1e9)
    regime: str = Field("ranging", max_length=30)


class StrategyResponse(BaseModel):
    action: str
    confidence: float
    position_size_pct: float
    reasoning: list[str]
    model: str


# --- Regime Detection ---


class RegimeFeatures(BaseModel):
    returns_1d: float = Field(0.0, ge=-100, le=10000)
    returns_7d: float = Field(0.0, ge=-100, le=10000)
    volatility_14d: float = Field(3.0, ge=0, le=1000)
    volume_ratio: float = Field(1.0, ge=0, le=10000)
    rsi: float = Field(50.0, ge=0, le=100)
    bb_width: float = Field(0.0, ge=0, le=1000)
    fear_greed: float = Field(50.0, ge=0, le=100)
    funding_rate: float = Field(0.0, ge=-1, le=1)
    price_vs_sma200: float = Field(0.0, ge=-100, le=10000)


class RegimeResponse(BaseModel):
    regime: str
    confidence: float
    probabilities: dict[str, float]
    model: str


# --- Project Risk ---


class ProjectRiskFeatures(BaseModel):
    bytecode_size: int = Field(0, ge=0, le=10_000_000)
    is_verified: int = Field(0, ge=0, le=1)
    holder_concentration: float = Field(0.0, ge=0, le=100)
    has_proxy: int = Field(0, ge=0, le=1)
    has_mint: int = Field(0, ge=0, le=1)
    has_pause: int = Field(0, ge=0, le=1)
    has_blacklist: int = Field(0, ge=0, le=1)
    liquidity_locked: int = Field(0, ge=0, le=1)
    buy_tax: float = Field(0.0, ge=0, le=100)
    sell_tax: float = Field(0.0, ge=0, le=100)
    contract_age_days: int = Field(0, ge=0, le=100_000)
    total_transfers: int = Field(0, ge=0, le=1_000_000_000)
    owner_balance_pct: float = Field(0.0, ge=0, le=100)
    is_open_source: int = Field(0, ge=0, le=1)
    top10_holder_pct: float = Field(0.0, ge=0, le=100)
    has_token_info: int = Field(1, ge=0, le=1)


class ProjectRiskResponse(BaseModel):
    risk_probability: float
    risk_level: str
    risk_factors: list[RugRiskFactor]
    model: str


# --- Portfolio Optimization ---


class PortfolioFeatures(BaseModel):
    model_config = ConfigDict(str_max_length=30)

    total_value: float = Field(10000.0, ge=0, le=1e15)
    cash: float = Field(10000.0, ge=0, le=1e15)
    win_rate: float = Field(0.5, ge=0, le=1)
    max_drawdown: float = Field(0.0, ge=0, le=100)
    avg_win: float = Field(0.05, ge=0, le=1e6)
    avg_loss: float = Field(0.03, ge=0, le=1e6)
    regime: str = Field("ranging", max_length=30)
    atr_pct: float = Field(3.0, ge=0, le=100)


class PortfolioOptResponse(BaseModel):
    position_size_pct: float
    stop_loss_multiplier: float
    take_profit_multiplier: float
    max_allocation_pct: int
    reasoning: list[str]
    model: str


# --- Intent Classification ---


class IntentRequest(BaseModel):
    text: str = Field(max_length=10_000)


class IntentResponse(BaseModel):
    intent: str
    confidence: float
    secondary_intent: str | None
    detected_tokens: list[str]
    detected_addresses: list[str]
    model: str


# --- Bytecode Risk ---


class BytecodeFeatures(BaseModel):
    bytecode_size: int = Field(0, ge=0, le=10_000_000)
    is_verified: int = Field(0, ge=0, le=1)
    has_selfdestruct: int = Field(0, ge=0, le=1)
    has_delegatecall: int = Field(0, ge=0, le=1)
    selector_count: int = Field(0, ge=0, le=100_000)
    opcode_entropy: float = Field(0.0, ge=0, le=10)
    has_mint: int = Field(0, ge=0, le=1)
    has_pause: int = Field(0, ge=0, le=1)
    has_blacklist: int = Field(0, ge=0, le=1)
    has_proxy: int = Field(0, ge=0, le=1)


class BytecodeRiskResponse(BaseModel):
    rug_probability: float
    risk_level: str
    risk_factors: list[RugRiskFactor]
    model: str


# --- Portfolio Forward Prediction ---


class PortfolioPredFeatures(BaseModel):
    returns_history: list[float] = Field(default_factory=list, max_length=1000)
    sharpe_history: list[float] = Field(default_factory=list, max_length=1000)
    drawdown_history: list[float] = Field(default_factory=list, max_length=1000)


class PortfolioPredResponse(BaseModel):
    predicted_return: float
    predicted_sharpe: float
    predicted_max_drawdown: float
    confidence: float
    model: str


# --- Target Quantile Prediction ---


class TargetFeatures(FeatureVector):
    current_price: float = Field(0.0, ge=0, le=1e12)
    probability_hint: float = Field(0.5, ge=0, le=1)


class TargetResponse(BaseModel):
    low_change_pct: float
    base_change_pct: float
    high_change_pct: float
    low_price: float | None = None
    base_price: float | None = None
    high_price: float | None = None
    direction_bias: str
    interval_width_pct: float
    model: str
    conformal_model: str | None = None


# --- Meta Confidence ---


class MetaConfidenceFeatures(BaseModel):
    model: str = Field("direction_ensemble", max_length=50)
    horizon: str = Field("4h", max_length=20)
    probability: float = Field(0.5, ge=0, le=1)
    rsi: float = Field(50.0, ge=0, le=100)
    macdHistogram: float = Field(0.0, ge=-1e6, le=1e6)
    volumeRatio: float = Field(1.0, ge=0, le=1000)
    atrPct: float = Field(0.0, ge=0, le=100)
    fearGreed: float = Field(50.0, ge=0, le=100)


class MetaConfidenceResponse(BaseModel):
    correctness_probability: float
    verdict: str
    model: str


# --- Microstructure Specialist ---


class MicrostructureFeatures(BaseModel):
    return_1: float = Field(0.0, ge=-100, le=1000)
    volume_ratio: float = Field(1.0, ge=0, le=1000)
    range_pct: float = Field(0.0, ge=0, le=1000)
    wick_imbalance: float = Field(0.0, ge=-1000, le=1000)
    trade_intensity: float = Field(0.0, ge=0, le=1000)
    price_vs_sma20: float = Field(0.0, ge=-1000, le=1000)
    volatility_5: float = Field(0.0, ge=0, le=1000)


class MicrostructureResponse(BaseModel):
    direction: str
    probability: float
    confidence: float
    model: str


# --- Catalyst Event Model ---


class CatalystFeatures(BaseModel):
    days_to_event: float = Field(30.0, ge=0, le=365)
    event_risk: float = Field(0.0, ge=0, le=1)
    within_24h: float = Field(0.0, ge=0, le=1)
    within_72h: float = Field(0.0, ge=0, le=1)
    within_7d: float = Field(0.0, ge=0, le=1)
    is_fomc: float = Field(0.0, ge=0, le=1)
    is_cpi: float = Field(0.0, ge=0, le=1)
    is_nfp: float = Field(0.0, ge=0, le=1)
    returns_1d: float = Field(0.0, ge=-1000, le=1000)
    returns_7d: float = Field(0.0, ge=-1000, le=1000)
    volatility_14d: float = Field(0.0, ge=0, le=1000)
    fear_greed: float = Field(50.0, ge=0, le=100)
    funding_rate: float = Field(0.0, ge=-1, le=1)


class CatalystResponse(BaseModel):
    direction: str
    probability: float
    event_risk: float
    model: str


# --- Training Pipeline ---

TRAINABLE_MODEL_PATTERN = (
    r"^(rug_detector|trend_scorer|regime_detector|sentiment_nlp|pump_detector|"
    r"narrative_detector|direction_ensemble|ensemble|target_xgb_quantile|"
    r"interval_conformal_calibrator|meta_stacking|meta_isotonic|meta_drift_detector|"
    r"microstructure_specialist|catalyst_event)$"
)


class TrainRequest(BaseModel):
    model_config = ConfigDict(populate_by_name=True, str_max_length=50)

    model_name: str = Field(
        ...,
        max_length=50,
        pattern=TRAINABLE_MODEL_PATTERN,
        validation_alias=AliasChoices("model_name", "model"),
        description=(
            "Model to train: rug_detector, trend_scorer, regime_detector, "
            "sentiment_nlp, pump_detector, narrative_detector, direction_ensemble, ensemble, "
            "target_xgb_quantile, interval_conformal_calibrator, meta_stacking, meta_isotonic, "
            "meta_drift_detector, microstructure_specialist, catalyst_event"
        ),
    )
    lookback_days: int = Field(90, ge=7, le=3650)
    reason: str | None = Field(None, max_length=200)


class TrainResponse(BaseModel):
    model: str
    status: str
    metrics: dict | None = None
    duration_seconds: float
    artifact_path: str
    error: str | None = None


class EvaluateRequest(BaseModel):
    model_config = ConfigDict(populate_by_name=True, str_max_length=50)

    model_name: str = Field(
        ...,
        max_length=50,
        pattern=TRAINABLE_MODEL_PATTERN,
        validation_alias=AliasChoices("model_name", "model"),
        description=(
            "Model to evaluate: rug_detector, trend_scorer, regime_detector, "
            "sentiment_nlp, pump_detector, narrative_detector, direction_ensemble, ensemble, "
            "target_xgb_quantile, interval_conformal_calibrator, meta_stacking, meta_isotonic, "
            "meta_drift_detector, microstructure_specialist, catalyst_event"
        ),
    )
    lookback_days: int = Field(90, ge=7, le=3650)


class RetrainRequest(BaseModel):
    model_config = ConfigDict(str_max_length=50)

    models: list[str] = Field(default_factory=lambda: ["direction_ensemble"], max_length=20)
    lookback_days: int = Field(90, ge=7, le=3650)
    reason: str | None = Field(None, max_length=200)


class RetrainResponse(BaseModel):
    job_id: str
    status: str
    models: list[str]
    results: list[dict]
    duration_seconds: float


class EvaluateResponse(BaseModel):
    model: str
    status: str
    metrics: dict | None = None
    error: str | None = None


# ---------------------------------------------------------------------------
# Model instances
# ---------------------------------------------------------------------------

lstm = LSTMPredictor()
classifier = SignalClassifier()
anomaly_detector = AnomalyDetector()
rug_detector = RugDetector()
wallet_classifier = WalletClassifier()
sentiment_analyzer = SentimentAnalyzer()
trend_scorer = TrendScorer()
ta_interpreter = TAInterpreter()
regime_detector = RegimeDetector()
strategy_bandit = StrategyBandit()
project_risk_scorer = ProjectRiskScorer()
portfolio_optimizer = PortfolioOptimizer()
intent_classifier = IntentClassifier()
pump_detector = PumpDetectorModel()
narrative_detector = NarrativeDetectorModel()
divergence_detector = DivergenceDetectorModel()
blockchain_cycle_analyzer = BlockchainCycleAnalyzer()
target_quantile_model = TargetQuantileModel()
conformal_interval_model = ConformalIntervalModel()
stacking_meta_model = StackingMetaModel()
drift_monitor = DriftMonitor()
microstructure_specialist = MicrostructureSpecialist()
catalyst_event_model = CatalystEventModel()
start_time = time.time()
predictions_served = 0
MODEL_DIR = Path(os.getenv("MODEL_DIR", "models"))
training_status = "idle"
training_job_id: str | None = None


@asynccontextmanager
async def lifespan(_app: FastAPI):
    """Load models on startup."""
    lstm.load()
    classifier.load()
    anomaly_detector.load()
    rug_detector.load()
    wallet_classifier.load()
    sentiment_analyzer.load()
    trend_scorer.load()
    ta_interpreter.load()
    regime_detector.load()
    strategy_bandit.load()
    project_risk_scorer.load()
    portfolio_optimizer.load()
    intent_classifier.load()
    pump_detector.load()
    narrative_detector.load()
    divergence_detector.load()
    blockchain_cycle_analyzer.load()
    target_quantile_model.load()
    conformal_interval_model.load()
    stacking_meta_model.load()
    drift_monitor.load()
    microstructure_specialist.load()
    catalyst_event_model.load()
    _load_isotonic_artifact()
    yield


app = FastAPI(
    title="Vizzor ML Sidecar",
    version="0.15.0",
    lifespan=lifespan,
)

# ---------------------------------------------------------------------------
# Security middleware — require ML_API_SECRET if set
# ---------------------------------------------------------------------------

ML_API_SECRET = os.getenv("ML_API_SECRET", "")


@app.middleware("http")
async def check_api_secret(request: Request, call_next):
    if ML_API_SECRET and request.url.path != "/health":
        token = request.headers.get("x-api-secret", "")
        if not hmac.compare_digest(token.encode(), ML_API_SECRET.encode()):
            return JSONResponse(status_code=403, content={"detail": "Forbidden"})
    return await call_next(request)


# ---------------------------------------------------------------------------
# Global exception handler — suppress stack traces in responses
# ---------------------------------------------------------------------------


@app.exception_handler(Exception)
async def global_exception_handler(_request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={"detail": "Internal inference error"},
    )


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@app.post("/predict", response_model=PredictionResponse)
async def predict(features: FeatureVector) -> PredictionResponse:
    global predictions_served
    predictions_served += 1

    # Use signal classifier for primary prediction
    result = classifier.predict(features.model_dump())

    # Scalping horizons (5m/15m/30m) get dampened — short timeframes are noisier
    horizon = features.horizon if features.horizon in VALID_HORIZONS else "4h"
    probability = result["probability"]
    if horizon in ("5m", "15m", "30m"):
        probability = 0.5 + (probability - 0.5) * 0.85

    return PredictionResponse(
        symbol=features.symbol,
        direction=result["direction"],
        probability=probability,
        model=result["model"],
        horizon=horizon,
        confidence=int(probability * 100),
    )


@app.post("/predict/batch", response_model=BatchResponse)
async def predict_batch(req: BatchRequest) -> BatchResponse:
    global predictions_served
    predictions_served += len(req.features)

    preds = []
    for fv in req.features:
        result = classifier.predict(fv.model_dump())
        horizon = fv.horizon if fv.horizon in VALID_HORIZONS else "4h"
        probability = result["probability"]
        if horizon in ("5m", "15m", "30m"):
            probability = 0.5 + (probability - 0.5) * 0.85
        preds.append(
            PredictionResponse(
                symbol=fv.symbol,
                direction=result["direction"],
                probability=probability,
                model=result["model"],
                horizon=horizon,
                confidence=int(probability * 100),
            )
        )
    return BatchResponse(predictions=preds)


@app.post("/anomalies", response_model=AnomaliesResponse)
async def detect_anomalies(req: FlowRequest) -> AnomaliesResponse:
    results = []
    for flow in req.flows:
        result = anomaly_detector.detect(flow.model_dump())
        results.append(
            AnomalyResponse(
                symbol=flow.symbol,
                score=result["score"],
                isAnomaly=result["is_anomaly"],
                type=result["type"],
                details=result["details"],
            )
        )
    return AnomaliesResponse(anomalies=results)


@app.post("/predict/rug", response_model=RugResponse)
async def predict_rug(features: RugFeatures) -> RugResponse:
    global predictions_served
    predictions_served += 1

    result = rug_detector.predict(features.model_dump())

    return RugResponse(
        rug_probability=result["rug_probability"],
        risk_level=result["risk_level"],
        risk_factors=[RugRiskFactor(**f) for f in result["risk_factors"]],
        model=result["model"],
    )


@app.post("/predict/wallet", response_model=WalletResponse)
async def predict_wallet(features: WalletFeatures) -> WalletResponse:
    global predictions_served
    predictions_served += 1

    result = wallet_classifier.classify(features.model_dump())

    return WalletResponse(
        behavior_type=result["behavior_type"],
        confidence=result["confidence"],
        risk_score=result["risk_score"],
        secondary_type=result["secondary_type"],
        indicators=result["indicators"],
        model=result["model"],
    )


@app.post("/predict/sentiment", response_model=SentimentResponse)
async def predict_sentiment(req: SentimentRequest) -> SentimentResponse:
    global predictions_served
    predictions_served += 1

    result = sentiment_analyzer.analyze(req.text)

    return SentimentResponse(
        sentiment=result["sentiment"],
        confidence=result["confidence"],
        score=result["score"],
        key_topics=result["key_topics"],
        model=result["model"],
    )


@app.post("/predict/sentiment/batch", response_model=SentimentBatchResponse)
async def predict_sentiment_batch(req: SentimentBatchRequest) -> SentimentBatchResponse:
    global predictions_served
    # Truncate individual texts to 10k chars
    texts = [t[:10_000] for t in req.texts]
    predictions_served += len(texts)

    results = sentiment_analyzer.analyze_batch(texts)

    return SentimentBatchResponse(
        results=[
            SentimentResponse(
                sentiment=r["sentiment"],
                confidence=r["confidence"],
                score=r["score"],
                key_topics=r["key_topics"],
                model=r["model"],
            )
            for r in results
        ]
    )


# --- New v0.11.0 Endpoints ---


@app.post("/predict/trend", response_model=TrendScoreResponse)
async def predict_trend(features: TrendFeatures) -> TrendScoreResponse:
    global predictions_served
    predictions_served += 1
    result = trend_scorer.predict(features.model_dump())
    return TrendScoreResponse(**result)


@app.post("/predict/ta", response_model=TAResponse)
async def predict_ta(features: TAFeatures) -> TAResponse:
    global predictions_served
    predictions_served += 1
    result = ta_interpreter.predict(features.model_dump())
    return TAResponse(
        signals=[TASignal(**s) for s in result["signals"]],
        weights=result["weights"],
        composite=result["composite"],
        model=result["model"],
    )


@app.post("/predict/regime", response_model=RegimeResponse)
async def predict_regime(features: RegimeFeatures) -> RegimeResponse:
    global predictions_served
    predictions_served += 1
    result = regime_detector.predict(features.model_dump())
    return RegimeResponse(**result)


@app.post("/predict/strategy", response_model=StrategyResponse)
async def predict_strategy(features: StrategyFeatures) -> StrategyResponse:
    global predictions_served
    predictions_served += 1
    result = strategy_bandit.predict(features.model_dump())
    return StrategyResponse(**result)


@app.post("/predict/project-risk", response_model=ProjectRiskResponse)
async def predict_project_risk(features: ProjectRiskFeatures) -> ProjectRiskResponse:
    global predictions_served
    predictions_served += 1
    result = project_risk_scorer.predict(features.model_dump())
    return ProjectRiskResponse(
        risk_probability=result["risk_probability"],
        risk_level=result["risk_level"],
        risk_factors=[RugRiskFactor(**f) for f in result["risk_factors"]],
        model=result["model"],
    )


@app.post("/predict/portfolio-opt", response_model=PortfolioOptResponse)
async def predict_portfolio_opt(features: PortfolioFeatures) -> PortfolioOptResponse:
    global predictions_served
    predictions_served += 1
    result = portfolio_optimizer.optimize(features.model_dump())
    return PortfolioOptResponse(**result)


@app.post("/predict/intent", response_model=IntentResponse)
async def predict_intent(req: IntentRequest) -> IntentResponse:
    global predictions_served
    predictions_served += 1
    result = intent_classifier.classify(req.text)
    return IntentResponse(**result)


@app.post("/predict/bytecode-risk", response_model=BytecodeRiskResponse)
async def predict_bytecode_risk(features: BytecodeFeatures) -> BytecodeRiskResponse:
    global predictions_served
    predictions_served += 1
    # Extend rug detector with bytecode-specific features
    rug_features = {
        "bytecode_size": features.bytecode_size,
        "is_verified": features.is_verified,
        "holder_concentration": 0,
        "has_proxy": features.has_proxy,
        "has_mint": features.has_mint,
        "has_pause": features.has_pause,
        "has_blacklist": features.has_blacklist,
        "liquidity_locked": 0,
        "buy_tax": 0,
        "sell_tax": 0,
        "contract_age_days": 0,
        "total_transfers": 0,
        "owner_balance_pct": 0,
        "is_open_source": features.is_verified,
        "top10_holder_pct": 0,
    }
    result = rug_detector.predict(rug_features)

    # Adjust based on bytecode-specific features
    prob = result["rug_probability"]
    if features.has_selfdestruct:
        prob = min(1.0, prob + 0.2)
    if features.has_delegatecall:
        prob = min(1.0, prob + 0.1)
    if features.opcode_entropy < 3.0 and features.bytecode_size > 100:
        prob = min(1.0, prob + 0.05)

    risk_level = (
        "critical" if prob >= 0.75
        else "high" if prob >= 0.5
        else "medium" if prob >= 0.25
        else "low"
    )

    factors = result["risk_factors"]
    if features.has_selfdestruct:
        factors.append({"factor": "has_selfdestruct", "importance": 0.25, "value": 1})
    if features.has_delegatecall:
        factors.append({"factor": "has_delegatecall", "importance": 0.15, "value": 1})

    return BytecodeRiskResponse(
        rug_probability=round(prob, 4),
        risk_level=risk_level,
        risk_factors=[RugRiskFactor(**f) for f in factors[:5]],
        model=result["model"] + "+bytecode",
    )


@app.post("/predict/portfolio-forward", response_model=PortfolioPredResponse)
async def predict_portfolio_forward(features: PortfolioPredFeatures) -> PortfolioPredResponse:
    global predictions_served
    predictions_served += 1
    result = portfolio_optimizer.forecast(features.model_dump())
    return PortfolioPredResponse(**result)


@app.post("/predict/target", response_model=TargetResponse)
async def predict_target(features: TargetFeatures) -> TargetResponse:
    global predictions_served
    predictions_served += 1

    raw = target_quantile_model.predict(features.model_dump())
    calibrated = conformal_interval_model.apply(
        raw["low_change_pct"],
        raw["base_change_pct"],
        raw["high_change_pct"],
    )

    current_price = features.current_price if features.current_price > 0 else None
    low_price = (
        round(current_price * (1 + calibrated["low_change_pct"] / 100), 8)
        if current_price is not None
        else None
    )
    base_price = (
        round(current_price * (1 + calibrated["base_change_pct"] / 100), 8)
        if current_price is not None
        else None
    )
    high_price = (
        round(current_price * (1 + calibrated["high_change_pct"] / 100), 8)
        if current_price is not None
        else None
    )

    return TargetResponse(
        low_change_pct=calibrated["low_change_pct"],
        base_change_pct=calibrated["base_change_pct"],
        high_change_pct=calibrated["high_change_pct"],
        low_price=low_price,
        base_price=base_price,
        high_price=high_price,
        direction_bias=raw["direction_bias"],
        interval_width_pct=round(
            calibrated["high_change_pct"] - calibrated["low_change_pct"], 4
        ),
        model=raw["model"],
        conformal_model=calibrated["model"],
    )


@app.post("/predict/meta-confidence", response_model=MetaConfidenceResponse)
async def predict_meta_confidence(
    features: MetaConfidenceFeatures,
) -> MetaConfidenceResponse:
    global predictions_served
    predictions_served += 1
    result = stacking_meta_model.predict(features.model_dump())
    return MetaConfidenceResponse(**result)


@app.post("/predict/microstructure", response_model=MicrostructureResponse)
async def predict_microstructure(
    features: MicrostructureFeatures,
) -> MicrostructureResponse:
    global predictions_served
    predictions_served += 1
    result = microstructure_specialist.predict(features.model_dump())
    return MicrostructureResponse(**result)


@app.post("/predict/catalyst", response_model=CatalystResponse)
async def predict_catalyst(features: CatalystFeatures) -> CatalystResponse:
    global predictions_served
    predictions_served += 1
    result = catalyst_event_model.predict(features.model_dump())
    return CatalystResponse(**result)


@app.post("/drift/check")
async def check_drift(features: FeatureVector):
    return drift_monitor.check(features.model_dump())


# --- v0.12.0 Endpoints ---


@app.post("/detect-pump")
async def detect_pump(request: Request):
    """Detect pump/dump activity using CUSUM."""
    global predictions_served
    predictions_served += 1

    data = await request.json()
    token = data.get("token", "unknown")
    prices = data.get("prices", [])
    volumes = data.get("volumes", [])

    # Single feed mode
    if not prices and "price" in data:
        result = pump_detector.feed(
            token, float(data["price"]), float(data.get("volume", 0))
        )
        if result is None:
            return {
                "detected": False,
                "type": "none",
                "severity": "low",
                "cusum_value": 0.0,
                "threshold": pump_detector.threshold * 0.01,
                "price_change_pct": 0.0,
                "volume_spike": 0.0,
                "confidence": 0.0,
            }
        return {
            "detected": result.detected,
            "type": result.type,
            "severity": result.severity,
            "cusum_value": result.cusum_value,
            "threshold": result.threshold,
            "price_change_pct": result.price_change_pct,
            "volume_spike": result.volume_spike,
            "confidence": result.confidence,
        }

    # Batch mode
    result = pump_detector.predict(
        {"token": token, "prices": prices, "volumes": volumes}
    )
    return {
        "detected": result.detected,
        "type": result.type,
        "severity": result.severity,
        "cusum_value": result.cusum_value,
        "threshold": result.threshold,
        "price_change_pct": result.price_change_pct,
        "volume_spike": result.volume_spike,
        "confidence": result.confidence,
    }


@app.post("/detect-narrative")
async def detect_narrative(request: Request):
    """Detect trending narratives from text corpus."""
    global predictions_served
    predictions_served += 1

    data = await request.json()
    texts = data.get("texts", [])
    top_k = data.get("top_k", 5)

    results = narrative_detector.get_trending_narratives(texts, top_k=top_k)
    return {
        "narratives": [
            {
                "narrative": r.narrative,
                "confidence": r.confidence,
                "related_tokens": r.related_tokens,
                "keywords": r.keywords,
                "trend_direction": r.trend_direction,
                "mention_count": r.mention_count,
            }
            for r in results
        ]
    }


@app.post("/detect-divergence")
async def detect_divergence(request: Request):
    """Detect prediction market vs price divergence."""
    global predictions_served
    predictions_served += 1

    data = await request.json()
    market_odds = data.get("market_odds", [])
    prices = data.get("prices", [])

    result = divergence_detector.detect(market_odds, prices)
    return {
        "divergence_score": result.divergence_score,
        "type": result.type,
        "prediction_market_signal": result.prediction_market_signal,
        "price_action_signal": result.price_action_signal,
        "confidence": result.confidence,
        "interpretation": result.interpretation,
    }


# --- v0.12.5 Blockchain Cycle Endpoint ---


class BlockchainCycleFeatures(BaseModel):
    halving_cycle_progress: float = Field(0.0, ge=0, le=100)
    days_since_halving: float = Field(0.0, ge=0, le=2000)
    days_to_next_halving: float = Field(0.0, ge=0, le=2000)
    block_reward: float = Field(3.125, ge=0, le=50)
    hashrate_change_30d: float = Field(0.0, ge=-100, le=1000)
    difficulty_change_14d: float = Field(0.0, ge=-100, le=1000)
    nvt_ratio: float = Field(55.0, ge=0, le=1000)
    mvrv_z_score: float = Field(1.5, ge=-10, le=20)
    inflation_rate: float = Field(0.83, ge=0, le=100)
    fee_revenue_share: float = Field(5.0, ge=0, le=100)
    mempool_size_mb: float = Field(0.0, ge=0, le=10000)
    avg_fee_rate: float = Field(0.0, ge=0, le=10000)
    hash_ribbon_signal: float = Field(0.0, ge=-1, le=1)


class BlockchainCycleRiskFactor(BaseModel):
    factor: str
    importance: float
    value: float


class BlockchainCycleResponse(BaseModel):
    cycle_phase: str
    phase_confidence: float
    fair_value_estimate: float
    deviation_from_fair: float
    risk_factors: list[BlockchainCycleRiskFactor]
    model: str


@app.post("/predict/blockchain-cycle", response_model=BlockchainCycleResponse)
async def predict_blockchain_cycle(features: BlockchainCycleFeatures) -> BlockchainCycleResponse:
    global predictions_served
    predictions_served += 1

    result = blockchain_cycle_analyzer.predict(features.model_dump())

    return BlockchainCycleResponse(
        cycle_phase=result["cycle_phase"],
        phase_confidence=result["phase_confidence"],
        fair_value_estimate=result["fair_value_estimate"],
        deviation_from_fair=result["deviation_from_fair"],
        risk_factors=[BlockchainCycleRiskFactor(**f) for f in result["risk_factors"]],
        model=result["model"],
    )


# --- Isotonic Calibration Endpoints ---


class CalibrationTrainRequest(BaseModel):
    predicted: list[float] = Field(max_length=10000)
    actual: list[float] = Field(max_length=10000)


class CalibrationPredictRequest(BaseModel):
    probabilities: list[float] = Field(max_length=1000)


# Global calibrator — trained lazily from prediction outcomes
_calibrator = None
_calibrator_lock = False


def _load_isotonic_artifact() -> None:
    global _calibrator
    try:
        import joblib

        path = MODEL_DIR / "meta_isotonic.joblib"
        if path.exists():
            data = joblib.load(path)
            _calibrator = data.get("model")
    except Exception:
        _calibrator = None


@app.post("/calibrate/train")
async def calibrate_train(req: CalibrationTrainRequest):
    """Train isotonic regression calibrator from prediction outcomes."""
    global _calibrator, _calibrator_lock
    if _calibrator_lock:
        return {"status": "busy", "message": "Training already in progress"}
    if len(req.predicted) != len(req.actual):
        return {"status": "error", "message": "predicted and actual must have the same length"}
    if len(req.predicted) < 10:
        return {"status": "error", "message": "Need at least 10 samples"}

    _calibrator_lock = True
    try:
        import joblib
        from sklearn.isotonic import IsotonicRegression
        ir = IsotonicRegression(y_min=0.05, y_max=0.95, out_of_bounds="clip")
        ir.fit(req.predicted, req.actual)
        _calibrator = ir
        MODEL_DIR.mkdir(parents=True, exist_ok=True)
        joblib.dump(
            {
                "model": ir,
                "trained_at": str(int(time.time())),
                "accuracy": None,
                "samples": len(req.predicted),
            },
            MODEL_DIR / "meta_isotonic.joblib",
        )
        return {"status": "ok", "samples": len(req.predicted)}
    except Exception as e:
        return {"status": "error", "message": str(e)}
    finally:
        _calibrator_lock = False


@app.post("/calibrate/predict")
async def calibrate_predict(req: CalibrationPredictRequest):
    """Return calibrated probabilities using trained isotonic regression."""
    if _calibrator is None:
        return {"calibrated": req.probabilities, "model": "identity"}

    import numpy as np
    arr = np.array(req.probabilities, dtype=np.float64)
    calibrated = _calibrator.predict(arr).tolist()
    return {"calibrated": calibrated, "model": "isotonic"}


# --- Training Pipeline Endpoints ---


TRAINERS = {
    "rug_detector": RugTrainer,
    "trend_scorer": TrendTrainer,
    "regime_detector": RegimeTrainer,
    "sentiment_nlp": SentimentTrainer,
    "pump_detector": PumpTrainer,
    "narrative_detector": NarrativeTrainer,
    "target_xgb_quantile": TargetQuantileTrainer,
    "interval_conformal_calibrator": ConformalIntervalTrainer,
    "meta_stacking": StackingMetaTrainer,
    "meta_isotonic": IsotonicTrainer,
    "meta_drift_detector": DriftMonitorTrainer,
    "microstructure_specialist": MicrostructureTrainer,
    "catalyst_event": CatalystTrainer,
}

MODEL_RELOADERS = {
    "rug_detector": rug_detector,
    "trend_scorer": trend_scorer,
    "regime_detector": regime_detector,
    "sentiment_nlp": sentiment_analyzer,
    "pump_detector": pump_detector,
    "narrative_detector": narrative_detector,
    "target_xgb_quantile": target_quantile_model,
    "interval_conformal_calibrator": conformal_interval_model,
    "meta_stacking": stacking_meta_model,
    "meta_drift_detector": drift_monitor,
    "microstructure_specialist": microstructure_specialist,
    "catalyst_event": catalyst_event_model,
}


def _reload_model(model_name: str) -> None:
    if model_name in {"direction_ensemble", "ensemble"}:
        classifier.load()
        return
    if model_name == "meta_isotonic":
        _load_isotonic_artifact()
        return

    loader = MODEL_RELOADERS.get(model_name)
    if loader is not None and hasattr(loader, "load"):
        loader.load()


def _train_direction_ensemble(lookback_days: int) -> dict:
    start = time.time()
    outcomes = load_direction_outcomes(lookback_days)

    if len(outcomes) < 30:
        return {
            "model": "direction_ensemble",
            "status": "skipped",
            "metrics": {"samples": len(outcomes)},
            "duration_seconds": round(time.time() - start, 2),
            "artifact_path": "",
            "error": f"Need at least 30 labeled outcomes, got {len(outcomes)}",
        }

    results: list[dict] = []
    for profile in ("scalp", "standard", "position"):
        trainer = DirectionTrainer(profile)
        results.append(trainer.run(outcomes))

    MODEL_DIR.mkdir(parents=True, exist_ok=True)
    manifest_path = MODEL_DIR / "direction_ensemble_manifest.json"
    manifest = {
        "trained_at": int(time.time()),
        "lookback_days": lookback_days,
        "profiles": results,
    }
    with open(manifest_path, "w", encoding="utf-8") as handle:
        json.dump(manifest, handle, indent=2)

    success_count = sum(1 for item in results if item.get("status") == "success")
    status = "success" if success_count > 0 else "failed"

    metrics = {
        "profiles_trained": success_count,
        "profiles_total": len(results),
        "samples": len(outcomes),
    }
    for item in results:
        name = str(item.get("model", "unknown"))
        acc = item.get("metrics", {}).get("accuracy") if isinstance(item.get("metrics"), dict) else None
        if acc is not None:
            metrics[f"{name}_accuracy"] = acc

    if success_count > 0:
        _reload_model("direction_ensemble")

    return {
        "model": "direction_ensemble",
        "status": status,
        "metrics": metrics,
        "duration_seconds": round(time.time() - start, 2),
        "artifact_path": str(manifest_path),
        "error": None if success_count > 0 else "No direction profile completed successfully",
    }


def _run_training_job(model_name: str, lookback_days: int) -> dict:
    if model_name in {"direction_ensemble", "ensemble"}:
        return _train_direction_ensemble(lookback_days)

    trainer_cls = TRAINERS.get(model_name)
    if not trainer_cls:
        return {
            "model": model_name,
            "status": "failed",
            "metrics": None,
            "duration_seconds": 0,
            "artifact_path": "",
            "error": f"Unknown model: {model_name}. Available: {', '.join(TRAINERS.keys())}, direction_ensemble, ensemble",
        }

    trainer = trainer_cls()
    try:
        result = trainer.run(lookback_days)
    except TypeError:
        result = trainer.run()
    if result.get("status") == "success":
        _reload_model(model_name)
    return result


@app.post("/train", response_model=TrainResponse)
async def train_model(req: TrainRequest) -> TrainResponse:
    result = _run_training_job(req.model_name, req.lookback_days)
    return TrainResponse(
        model=result["model"],
        status=result["status"],
        metrics=result.get("metrics"),
        duration_seconds=result["duration_seconds"],
        artifact_path=result["artifact_path"],
        error=result.get("error"),
    )


@app.post("/evaluate", response_model=EvaluateResponse)
async def evaluate_model(req: EvaluateRequest) -> EvaluateResponse:
    if req.model_name in {"direction_ensemble", "ensemble"}:
        result = _train_direction_ensemble(req.lookback_days)
        return EvaluateResponse(
            model=req.model_name,
            status=result["status"],
            metrics=result.get("metrics"),
            error=result.get("error"),
        )

    trainer_cls = TRAINERS.get(req.model_name)
    if not trainer_cls:
        return EvaluateResponse(
            model=req.model_name,
            status="failed",
            error=f"Unknown model: {req.model_name}. Available: {', '.join(TRAINERS.keys())}",
        )
    trainer = trainer_cls()
    try:
        if all(
            hasattr(trainer, method)
            for method in ("load_data", "preprocess", "train", "evaluate")
        ):
            data = trainer.load_data()
            splits = trainer.preprocess(data)
            X_train, X_val, X_test, y_train, y_val, y_test = splits
            model = trainer.train(X_train, y_train, X_val, y_val)
            metrics = trainer.evaluate(model, X_test, y_test)
            status = "success"
        else:
            result = _run_training_job(req.model_name, req.lookback_days)
            metrics = result.get("metrics")
            status = result.get("status", "failed")
        return EvaluateResponse(
            model=req.model_name,
            status=status,
            metrics=metrics,
        )
    except Exception as e:
        return EvaluateResponse(
            model=req.model_name,
            status="failed",
            error=str(e),
        )


@app.post("/retrain", response_model=RetrainResponse)
async def retrain_models(req: RetrainRequest) -> RetrainResponse:
    global training_status, training_job_id

    start = time.time()
    job_id = str(uuid4())
    training_status = "training"
    training_job_id = job_id

    normalized_models = [
        "direction_ensemble" if model == "ensemble" else model for model in req.models
    ]

    try:
        results = [_run_training_job(model, req.lookback_days) for model in normalized_models]
        status = "success" if any(r.get("status") == "success" for r in results) else "failed"
        return RetrainResponse(
            job_id=job_id,
            status=status,
            models=normalized_models,
            results=results,
            duration_seconds=round(time.time() - start, 2),
        )
    finally:
        training_status = "idle"


@app.get("/catalog/models")
async def catalog_models():
    return get_model_catalog()


@app.get("/catalog/roadmap")
async def catalog_roadmap():
    return get_training_roadmap()


@app.get("/health")
async def health():
    return {
        "version": app.version,
        "models": [
            {
                "name": "lstm-predictor",
                "version": lstm.version,
                "loaded": lstm.is_loaded,
                "lastTrained": lstm.last_trained,
                "accuracy": lstm.accuracy,
            },
            {
                "name": "signal-classifier",
                "version": classifier.version,
                "loaded": classifier.is_loaded,
                "lastTrained": classifier.last_trained,
                "accuracy": classifier.accuracy,
            },
            {
                "name": "anomaly-detector",
                "version": anomaly_detector.version,
                "loaded": anomaly_detector.is_loaded,
                "lastTrained": anomaly_detector.last_trained,
                "accuracy": None,
            },
            {
                "name": "rug-detector",
                "version": rug_detector.version,
                "loaded": rug_detector.is_loaded,
                "lastTrained": rug_detector.last_trained,
                "accuracy": rug_detector.accuracy,
            },
            {
                "name": "wallet-classifier",
                "version": wallet_classifier.version,
                "loaded": wallet_classifier.is_loaded,
                "lastTrained": wallet_classifier.last_trained,
                "accuracy": wallet_classifier.accuracy,
            },
            {
                "name": "sentiment-analyzer",
                "version": sentiment_analyzer.version,
                "loaded": sentiment_analyzer.is_loaded,
                "lastTrained": sentiment_analyzer.last_trained,
                "accuracy": sentiment_analyzer.accuracy,
            },
            {
                "name": "trend-scorer",
                "version": trend_scorer.version,
                "loaded": trend_scorer.is_loaded,
                "lastTrained": trend_scorer.last_trained,
                "accuracy": trend_scorer.accuracy,
            },
            {
                "name": "ta-interpreter",
                "version": ta_interpreter.version,
                "loaded": ta_interpreter.is_loaded,
                "lastTrained": ta_interpreter.last_trained,
                "accuracy": ta_interpreter.accuracy,
            },
            {
                "name": "regime-detector",
                "version": regime_detector.version,
                "loaded": regime_detector.is_loaded,
                "lastTrained": regime_detector.last_trained,
                "accuracy": regime_detector.accuracy,
            },
            {
                "name": "strategy-bandit",
                "version": strategy_bandit.version,
                "loaded": strategy_bandit.is_loaded,
                "lastTrained": strategy_bandit.last_trained,
                "accuracy": strategy_bandit.accuracy,
            },
            {
                "name": "project-risk-scorer",
                "version": project_risk_scorer.version,
                "loaded": project_risk_scorer.is_loaded,
                "lastTrained": project_risk_scorer.last_trained,
                "accuracy": project_risk_scorer.accuracy,
            },
            {
                "name": "portfolio-optimizer",
                "version": portfolio_optimizer.version,
                "loaded": portfolio_optimizer.is_loaded,
                "lastTrained": portfolio_optimizer.last_trained,
                "accuracy": portfolio_optimizer.accuracy,
            },
            {
                "name": "intent-classifier",
                "version": intent_classifier.version,
                "loaded": intent_classifier.is_loaded,
                "lastTrained": intent_classifier.last_trained,
                "accuracy": intent_classifier.accuracy,
            },
            {
                "name": "pump-detector",
                "version": pump_detector.version,
                "loaded": pump_detector.is_loaded,
                "lastTrained": pump_detector.last_trained,
                "accuracy": pump_detector.accuracy,
            },
            {
                "name": "narrative-detector",
                "version": narrative_detector.version,
                "loaded": narrative_detector.is_loaded,
                "lastTrained": narrative_detector.last_trained,
                "accuracy": narrative_detector.accuracy,
            },
            {
                "name": "divergence-detector",
                "version": divergence_detector.version,
                "loaded": divergence_detector.is_loaded,
                "lastTrained": divergence_detector.last_trained,
                "accuracy": divergence_detector.accuracy,
            },
            {
                "name": "blockchain-cycle-analyzer",
                "version": blockchain_cycle_analyzer.version,
                "loaded": blockchain_cycle_analyzer.is_loaded,
                "lastTrained": blockchain_cycle_analyzer.last_trained,
                "accuracy": blockchain_cycle_analyzer.accuracy,
            },
            {
                "name": "target-quantile",
                "version": target_quantile_model.version,
                "loaded": target_quantile_model.is_loaded,
                "lastTrained": target_quantile_model.last_trained,
                "accuracy": target_quantile_model.accuracy,
            },
            {
                "name": "conformal-interval",
                "version": conformal_interval_model.version,
                "loaded": conformal_interval_model.is_loaded,
                "lastTrained": conformal_interval_model.last_trained,
                "accuracy": conformal_interval_model.coverage,
            },
            {
                "name": "meta-stacking",
                "version": stacking_meta_model.version,
                "loaded": stacking_meta_model.is_loaded,
                "lastTrained": stacking_meta_model.last_trained,
                "accuracy": stacking_meta_model.accuracy,
            },
            {
                "name": "meta-isotonic",
                "version": "0.1.0",
                "loaded": _calibrator is not None,
                "lastTrained": None,
                "accuracy": None,
            },
            {
                "name": "meta-drift-detector",
                "version": drift_monitor.version,
                "loaded": drift_monitor.is_loaded,
                "lastTrained": drift_monitor.last_trained,
                "accuracy": None,
            },
            {
                "name": "microstructure-specialist",
                "version": microstructure_specialist.version,
                "loaded": microstructure_specialist.is_loaded,
                "lastTrained": microstructure_specialist.last_trained,
                "accuracy": microstructure_specialist.accuracy,
            },
            {
                "name": "catalyst-event",
                "version": catalyst_event_model.version,
                "loaded": catalyst_event_model.is_loaded,
                "lastTrained": catalyst_event_model.last_trained,
                "accuracy": catalyst_event_model.accuracy,
            },
        ],
        "uptime": int(time.time() - start_time),
        "predictionsServed": predictions_served,
        "training_status": training_status,
        "training_job_id": training_job_id,
    }
