#!/usr/bin/env python3
"""
Asset Metamodel (Nivel 2)
=========================
Metamodel that synthesizes predictions from 5m and 15m base models
along with attention scores to produce final trading predictions.

This is the top level (Nivel 2) of the hierarchical ML architecture:
- Nivel 0: Attention Score Model (when to pay attention)
- Nivel 1: Base Models (5m/15m predictions with attention features)
- Nivel 2: Asset Metamodel (synthesizes everything) <- THIS FILE

Key Features:
1. XGBoost Stacking: Combines base model predictions
2. Per-Asset: One metamodel per trading asset
3. OOS-Only Training: Uses only out-of-sample predictions from base models
4. Confidence Output: Generates confidence score for filtering

Architecture:
    meta_features = [
        pred_high_5m, pred_low_5m,      # 5m predictions
        pred_high_15m, pred_low_15m,    # 15m predictions
        attention_5m, attention_15m,     # Attention scores
        attention_class_5m, attention_class_15m,  # Attention classes
        ATR_ratio, volume_z              # Context features
    ]

    output = [delta_high_final, delta_low_final, confidence]

Author: ML Pipeline
Version: 1.0.0
Created: 2026-01-07
"""

import numpy as np
import pandas as pd
from typing import Dict, List, Tuple, Optional, Any, Union
from dataclasses import dataclass, field
from pathlib import Path
import joblib
from loguru import logger

try:
    from xgboost import XGBRegressor, XGBClassifier
    HAS_XGBOOST = True
except ImportError:
    HAS_XGBOOST = False
    logger.warning("XGBoost not available")

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.metrics import accuracy_score, f1_score


@dataclass
class MetamodelConfig:
    """Configuration for the Asset Metamodel"""

    # Feature configuration
    prediction_features: List[str] = field(default_factory=lambda: [
        'pred_high_5m', 'pred_low_5m',
        'pred_high_15m', 'pred_low_15m'
    ])

    attention_features: List[str] = field(default_factory=lambda: [
        'attention_5m', 'attention_15m',
        'attention_class_5m', 'attention_class_15m'
    ])

    context_features: List[str] = field(default_factory=lambda: [
        'ATR_ratio', 'volume_z'
    ])

    # XGBoost parameters for regression models
    xgb_params: Dict = field(default_factory=lambda: {
        'n_estimators': 200,
        'max_depth': 4,
        'learning_rate': 0.05,
        'subsample': 0.8,
        'colsample_bytree': 0.8,
        'min_child_weight': 15,
        'gamma': 0.2,
        'reg_alpha': 0.2,
        'reg_lambda': 1.5,
        'tree_method': 'hist',
        'random_state': 42
    })

    # XGBoost parameters for confidence classifier
    clf_params: Dict = field(default_factory=lambda: {
        'n_estimators': 150,
        'max_depth': 3,
        'learning_rate': 0.05,
        'subsample': 0.8,
        'colsample_bytree': 0.8,
        'min_child_weight': 20,
        'gamma': 0.3,
        'reg_alpha': 0.2,
        'reg_lambda': 1.5,
        'tree_method': 'hist',
        'random_state': 42,
        'objective': 'binary:logistic'
    })

    # Training configuration
    val_split: float = 0.15
    min_train_samples: int = 2000

    # Confidence thresholds
    win_rr_threshold: float = 2.0  # R:R ratio to consider a "win"

    # Feature importance tracking
    track_feature_importance: bool = True


@dataclass
class MetamodelPrediction:
    """Result of metamodel prediction"""
    delta_high_final: np.ndarray
    delta_low_final: np.ndarray
    confidence: np.ndarray
    confidence_proba: np.ndarray

    def to_dict(self) -> Dict[str, Any]:
        return {
            'delta_high_final': self.delta_high_final.tolist() if isinstance(self.delta_high_final, np.ndarray) else [self.delta_high_final],
            'delta_low_final': self.delta_low_final.tolist() if isinstance(self.delta_low_final, np.ndarray) else [self.delta_low_final],
            'confidence': self.confidence.tolist() if isinstance(self.confidence, np.ndarray) else [self.confidence],
            'confidence_proba': self.confidence_proba.tolist() if isinstance(self.confidence_proba, np.ndarray) else [self.confidence_proba]
        }


@dataclass
class TrainingMetrics:
    """Metrics from metamodel training"""
    # Regression metrics
    mae_high: float = 0.0
    mae_low: float = 0.0
    rmse_high: float = 0.0
    rmse_low: float = 0.0
    r2_high: float = 0.0
    r2_low: float = 0.0

    # Confidence metrics
    confidence_accuracy: float = 0.0
    confidence_f1: float = 0.0

    # Data counts
    n_train: int = 0
    n_val: int = 0

    # Improvement over simple average
    improvement_over_avg: float = 0.0


class AssetMetamodel:
    """
    Metamodel for a single asset that synthesizes 5m and 15m predictions.

    This is Nivel 2 of the hierarchical architecture:
    - Takes predictions from Nivel 1 (base models with attention)
    - Takes attention scores from Nivel 0 (attention model)
    - Outputs final predictions with confidence

    Key Design Decisions:
    1. Separate models for high and low (asymmetric movements)
    2. Binary confidence classifier (tradeable vs not)
    3. Uses only OOS predictions for training (no leakage)
    4. Lightweight stacking (avoid overfitting on predictions)

    Usage:
        metamodel = AssetMetamodel('XAUUSD', config)
        metamodel.fit(meta_features_df, targets_df)

        prediction = metamodel.predict(meta_features)
        print(f"High: {prediction.delta_high_final}")
        print(f"Low: {prediction.delta_low_final}")
        print(f"Confidence: {prediction.confidence}")
    """

    def __init__(self, symbol: str, config: MetamodelConfig = None):
        self.symbol = symbol
        self.config = config or MetamodelConfig()

        # Feature names
        self.feature_names = (
            self.config.prediction_features +
            self.config.attention_features +
            self.config.context_features
        )

        # Models
        self.model_high: Optional[XGBRegressor] = None
        self.model_low: Optional[XGBRegressor] = None
        self.model_confidence: Optional[XGBClassifier] = None

        # Training state
        self.is_fitted = False
        self.training_metrics: Optional[TrainingMetrics] = None
        self.feature_importance: Optional[pd.DataFrame] = None

        logger.info(f"Initialized AssetMetamodel for {symbol}")
        logger.info(f"  Features: {len(self.feature_names)}")
        logger.info(f"  Prediction features: {self.config.prediction_features}")
        logger.info(f"  Attention features: {self.config.attention_features}")
        logger.info(f"  Context features: {self.config.context_features}")

    def _validate_features(self, X: pd.DataFrame) -> pd.DataFrame:
        """Validate and select features from input DataFrame."""
        missing = set(self.feature_names) - set(X.columns)
        if missing:
            logger.warning(f"Missing features: {missing}")
            # Fill missing with zeros
            for feat in missing:
                X[feat] = 0.0

        return X[self.feature_names]

    def _compute_confidence_target(
        self,
        target_high: np.ndarray,
        target_low: np.ndarray,
        pred_high: np.ndarray,
        pred_low: np.ndarray
    ) -> np.ndarray:
        """
        Compute binary confidence target based on actual outcomes.

        A prediction is "confident" if the actual outcome was favorable:
        - For LONG: actual_high >= predicted_high * threshold
        - For SHORT: actual_low >= predicted_low * threshold

        We use the better of LONG or SHORT outcome.
        """
        # Determine which direction was better
        long_favorable = target_high >= pred_high * 0.8  # Within 80% of prediction
        short_favorable = target_low >= pred_low * 0.8

        # Confident if either direction was favorable
        confident = (long_favorable | short_favorable).astype(int)

        logger.info(f"Confidence target distribution:")
        logger.info(f"  Confident (1): {confident.sum()} ({confident.mean():.1%})")
        logger.info(f"  Not confident (0): {(1-confident).sum()} ({(1-confident).mean():.1%})")

        return confident

    def fit(
        self,
        meta_features: pd.DataFrame,
        target_high: np.ndarray,
        target_low: np.ndarray,
        sample_weights: np.ndarray = None
    ) -> 'AssetMetamodel':
        """
        Train the metamodel.

        Args:
            meta_features: DataFrame with columns matching feature_names
            target_high: Actual delta high values
            target_low: Actual delta low values
            sample_weights: Optional sample weights

        Returns:
            Self for chaining
        """
        logger.info(f"\n{'='*60}")
        logger.info(f"Training AssetMetamodel for {self.symbol}")
        logger.info(f"{'='*60}")

        # Validate and select features
        X = self._validate_features(meta_features)
        X_values = X.values

        # Remove invalid samples
        valid_mask = (
            ~np.isnan(X_values).any(axis=1) &
            ~np.isnan(target_high) &
            ~np.isnan(target_low)
        )

        X_valid = X_values[valid_mask]
        y_high_valid = target_high[valid_mask]
        y_low_valid = target_low[valid_mask]

        if sample_weights is not None:
            weights_valid = sample_weights[valid_mask]
        else:
            weights_valid = None

        n_valid = len(X_valid)
        logger.info(f"Valid samples: {n_valid} / {len(X_values)}")

        if n_valid < self.config.min_train_samples:
            raise ValueError(
                f"Insufficient training data: {n_valid} < {self.config.min_train_samples}"
            )

        # Train/val split (time-based)
        split_idx = int(n_valid * (1 - self.config.val_split))
        X_train, X_val = X_valid[:split_idx], X_valid[split_idx:]
        y_high_train, y_high_val = y_high_valid[:split_idx], y_high_valid[split_idx:]
        y_low_train, y_low_val = y_low_valid[:split_idx], y_low_valid[split_idx:]

        if weights_valid is not None:
            weights_train = weights_valid[:split_idx]
        else:
            weights_train = None

        logger.info(f"Train samples: {len(X_train)}, Val samples: {len(X_val)}")

        # ===== Train HIGH model =====
        logger.info("\nTraining HIGH model...")
        self.model_high = XGBRegressor(**self.config.xgb_params)
        self.model_high.fit(
            X_train, y_high_train,
            sample_weight=weights_train,
            eval_set=[(X_val, y_high_val)],
            verbose=False
        )

        y_high_pred_val = self.model_high.predict(X_val)
        mae_high = mean_absolute_error(y_high_val, y_high_pred_val)
        rmse_high = np.sqrt(mean_squared_error(y_high_val, y_high_pred_val))
        r2_high = r2_score(y_high_val, y_high_pred_val)

        logger.info(f"  HIGH MAE: {mae_high:.4f}")
        logger.info(f"  HIGH RMSE: {rmse_high:.4f}")
        logger.info(f"  HIGH R2: {r2_high:.4f}")

        # ===== Train LOW model =====
        logger.info("\nTraining LOW model...")
        self.model_low = XGBRegressor(**self.config.xgb_params)
        self.model_low.fit(
            X_train, y_low_train,
            sample_weight=weights_train,
            eval_set=[(X_val, y_low_val)],
            verbose=False
        )

        y_low_pred_val = self.model_low.predict(X_val)
        mae_low = mean_absolute_error(y_low_val, y_low_pred_val)
        rmse_low = np.sqrt(mean_squared_error(y_low_val, y_low_pred_val))
        r2_low = r2_score(y_low_val, y_low_pred_val)

        logger.info(f"  LOW MAE: {mae_low:.4f}")
        logger.info(f"  LOW RMSE: {rmse_low:.4f}")
        logger.info(f"  LOW R2: {r2_low:.4f}")

        # ===== Train CONFIDENCE classifier =====
        logger.info("\nTraining CONFIDENCE classifier...")

        # Get predictions on training set for confidence target
        y_high_pred_train = self.model_high.predict(X_train)
        y_low_pred_train = self.model_low.predict(X_train)

        confidence_target_train = self._compute_confidence_target(
            y_high_train, y_low_train,
            y_high_pred_train, y_low_pred_train
        )

        confidence_target_val = self._compute_confidence_target(
            y_high_val, y_low_val,
            y_high_pred_val, y_low_pred_val
        )

        self.model_confidence = XGBClassifier(**self.config.clf_params)
        self.model_confidence.fit(
            X_train, confidence_target_train,
            sample_weight=weights_train,
            eval_set=[(X_val, confidence_target_val)],
            verbose=False
        )

        confidence_pred_val = self.model_confidence.predict(X_val)
        conf_accuracy = accuracy_score(confidence_target_val, confidence_pred_val)
        conf_f1 = f1_score(confidence_target_val, confidence_pred_val)

        logger.info(f"  Confidence Accuracy: {conf_accuracy:.4f}")
        logger.info(f"  Confidence F1: {conf_f1:.4f}")

        # ===== Compare to simple average =====
        # Simple average baseline
        avg_high = (X_val[:, 0] + X_val[:, 2]) / 2  # pred_high_5m + pred_high_15m
        avg_low = (X_val[:, 1] + X_val[:, 3]) / 2   # pred_low_5m + pred_low_15m

        mae_avg_high = mean_absolute_error(y_high_val, avg_high)
        mae_avg_low = mean_absolute_error(y_low_val, avg_low)

        improvement_high = (mae_avg_high - mae_high) / mae_avg_high * 100
        improvement_low = (mae_avg_low - mae_low) / mae_avg_low * 100
        improvement_avg = (improvement_high + improvement_low) / 2

        logger.info(f"\nImprovement over simple average:")
        logger.info(f"  HIGH: {improvement_high:.1f}%")
        logger.info(f"  LOW: {improvement_low:.1f}%")
        logger.info(f"  Average: {improvement_avg:.1f}%")

        # Store metrics
        self.training_metrics = TrainingMetrics(
            mae_high=mae_high,
            mae_low=mae_low,
            rmse_high=rmse_high,
            rmse_low=rmse_low,
            r2_high=r2_high,
            r2_low=r2_low,
            confidence_accuracy=conf_accuracy,
            confidence_f1=conf_f1,
            n_train=len(X_train),
            n_val=len(X_val),
            improvement_over_avg=improvement_avg
        )

        # Compute feature importance
        if self.config.track_feature_importance:
            self._compute_feature_importance()

        self.is_fitted = True
        logger.info(f"\nAssetMetamodel training complete for {self.symbol}")

        return self

    def predict(
        self,
        meta_features: Union[pd.DataFrame, np.ndarray]
    ) -> MetamodelPrediction:
        """
        Generate predictions.

        Args:
            meta_features: DataFrame or array with meta features

        Returns:
            MetamodelPrediction with final predictions and confidence
        """
        if not self.is_fitted:
            raise ValueError("Model not fitted. Call fit() first.")

        # Handle input format
        if isinstance(meta_features, pd.DataFrame):
            X = self._validate_features(meta_features).values
        else:
            X = meta_features
            if X.ndim == 1:
                X = X.reshape(1, -1)

        # Handle NaN
        X = np.nan_to_num(X, nan=0.0)

        # Predict
        delta_high_final = self.model_high.predict(X)
        delta_low_final = self.model_low.predict(X)
        confidence = self.model_confidence.predict(X)
        confidence_proba = self.model_confidence.predict_proba(X)[:, 1]

        # Ensure non-negative
        delta_high_final = np.maximum(delta_high_final, 0)
        delta_low_final = np.maximum(delta_low_final, 0)

        return MetamodelPrediction(
            delta_high_final=delta_high_final,
            delta_low_final=delta_low_final,
            confidence=confidence,
            confidence_proba=confidence_proba
        )

    def predict_single(
        self,
        meta_features: Union[pd.Series, Dict, np.ndarray]
    ) -> Tuple[float, float, int, float]:
        """
        Predict for a single sample.

        Returns:
            Tuple of (delta_high, delta_low, confidence, confidence_proba)
        """
        if isinstance(meta_features, dict):
            meta_features = pd.DataFrame([meta_features])
        elif isinstance(meta_features, pd.Series):
            meta_features = pd.DataFrame([meta_features.to_dict()])
        elif isinstance(meta_features, np.ndarray):
            if meta_features.ndim == 1:
                meta_features = meta_features.reshape(1, -1)

        pred = self.predict(meta_features)
        return (
            float(pred.delta_high_final[0]),
            float(pred.delta_low_final[0]),
            int(pred.confidence[0]),
            float(pred.confidence_proba[0])
        )

    def _compute_feature_importance(self):
        """Compute and store feature importance from all models."""
        high_imp = self.model_high.feature_importances_
        low_imp = self.model_low.feature_importances_
        conf_imp = self.model_confidence.feature_importances_

        self.feature_importance = pd.DataFrame({
            'feature': self.feature_names,
            'high_importance': high_imp,
            'low_importance': low_imp,
            'confidence_importance': conf_imp,
            'combined': (high_imp + low_imp + conf_imp) / 3
        }).sort_values('combined', ascending=False)

        logger.info("\nTop features (combined importance):")
        for _, row in self.feature_importance.head(5).iterrows():
            logger.info(f"  {row['feature']}: {row['combined']:.4f}")

    def get_training_summary(self) -> Dict[str, Any]:
        """Get summary of training results."""
        if not self.is_fitted:
            return {'is_fitted': False}

        return {
            'is_fitted': True,
            'symbol': self.symbol,
            'metrics': {
                'mae_high': self.training_metrics.mae_high,
                'mae_low': self.training_metrics.mae_low,
                'rmse_high': self.training_metrics.rmse_high,
                'rmse_low': self.training_metrics.rmse_low,
                'r2_high': self.training_metrics.r2_high,
                'r2_low': self.training_metrics.r2_low,
                'confidence_accuracy': self.training_metrics.confidence_accuracy,
                'confidence_f1': self.training_metrics.confidence_f1,
                'improvement_over_avg': self.training_metrics.improvement_over_avg
            },
            'data': {
                'n_train': self.training_metrics.n_train,
                'n_val': self.training_metrics.n_val
            },
            'config': {
                'prediction_features': self.config.prediction_features,
                'attention_features': self.config.attention_features,
                'context_features': self.config.context_features
            },
            'feature_importance': self.feature_importance.to_dict() if self.feature_importance is not None else None
        }

    def save(self, path: str):
        """Save model to disk."""
        from dataclasses import asdict
        path = Path(path)
        path.mkdir(parents=True, exist_ok=True)

        # Save models
        joblib.dump(self.model_high, path / 'model_high.joblib')
        joblib.dump(self.model_low, path / 'model_low.joblib')
        joblib.dump(self.model_confidence, path / 'model_confidence.joblib')

        # Convert config to dict for pickle compatibility
        config_dict = asdict(self.config)

        # Save metadata
        metadata = {
            'symbol': self.symbol,
            'config_dict': config_dict,
            'feature_names': self.feature_names,
            'training_metrics': asdict(self.training_metrics) if self.training_metrics else None,
            'feature_importance': self.feature_importance.to_dict() if self.feature_importance is not None else None,
            'is_fitted': self.is_fitted
        }
        joblib.dump(metadata, path / 'metadata.joblib')

        logger.info(f"AssetMetamodel saved to {path}")

    @classmethod
    def load(cls, path: str) -> 'AssetMetamodel':
        """Load model from disk."""
        path = Path(path)

        # Load metadata
        metadata = joblib.load(path / 'metadata.joblib')

        # Reconstruct config
        if 'config_dict' in metadata:
            config = MetamodelConfig(**metadata['config_dict'])
        else:
            config = MetamodelConfig()

        # Create instance
        model = cls(metadata['symbol'], config)
        model.is_fitted = metadata['is_fitted']

        if metadata['training_metrics']:
            model.training_metrics = TrainingMetrics(**metadata['training_metrics'])

        if metadata['feature_importance']:
            model.feature_importance = pd.DataFrame(metadata['feature_importance'])

        # Load models
        model.model_high = joblib.load(path / 'model_high.joblib')
        model.model_low = joblib.load(path / 'model_low.joblib')
        model.model_confidence = joblib.load(path / 'model_confidence.joblib')

        logger.info(f"AssetMetamodel loaded from {path}")
        return model


class MetamodelRegistry:
    """
    Registry to manage multiple asset metamodels.

    Usage:
        registry = MetamodelRegistry()

        # Add trained metamodels
        registry.add(xauusd_metamodel)
        registry.add(eurusd_metamodel)

        # Predict
        prediction = registry.predict(features, 'XAUUSD')

        # Save/load all
        registry.save('models/metamodels')
        registry.load('models/metamodels')
    """

    def __init__(self):
        self.models: Dict[str, AssetMetamodel] = {}

    def add(self, model: AssetMetamodel):
        """Add a metamodel to the registry."""
        self.models[model.symbol] = model
        logger.info(f"Added {model.symbol} to MetamodelRegistry")

    def get(self, symbol: str) -> Optional[AssetMetamodel]:
        """Get metamodel for a symbol."""
        return self.models.get(symbol)

    def predict(
        self,
        meta_features: pd.DataFrame,
        symbol: str
    ) -> MetamodelPrediction:
        """Predict using the appropriate metamodel."""
        if symbol not in self.models:
            raise ValueError(f"No metamodel for symbol: {symbol}")
        return self.models[symbol].predict(meta_features)

    def list_symbols(self) -> List[str]:
        """List all registered symbols."""
        return list(self.models.keys())

    def save(self, path: str):
        """Save all metamodels."""
        path = Path(path)
        path.mkdir(parents=True, exist_ok=True)

        for symbol, model in self.models.items():
            model_path = path / symbol
            model.save(str(model_path))

        # Save registry metadata
        metadata = {
            'symbols': list(self.models.keys())
        }
        joblib.dump(metadata, path / 'registry_metadata.joblib')

        logger.info(f"Saved {len(self.models)} metamodels to {path}")

    def load(self, path: str):
        """Load all metamodels."""
        path = Path(path)

        metadata = joblib.load(path / 'registry_metadata.joblib')

        for symbol in metadata['symbols']:
            model_path = path / symbol
            if model_path.exists():
                self.models[symbol] = AssetMetamodel.load(str(model_path))

        logger.info(f"Loaded {len(self.models)} metamodels from {path}")


# Convenience functions

def create_meta_features(
    pred_5m: Dict[str, np.ndarray],
    pred_15m: Dict[str, np.ndarray],
    attention_5m: Tuple[np.ndarray, np.ndarray],
    attention_15m: Tuple[np.ndarray, np.ndarray],
    context_features: Dict[str, np.ndarray]
) -> pd.DataFrame:
    """
    Create meta features DataFrame from component predictions.

    Args:
        pred_5m: Dict with 'high' and 'low' predictions from 5m model
        pred_15m: Dict with 'high' and 'low' predictions from 15m model
        attention_5m: Tuple of (attention_score, attention_class) for 5m
        attention_15m: Tuple of (attention_score, attention_class) for 15m
        context_features: Dict with 'ATR_ratio' and 'volume_z'

    Returns:
        DataFrame with meta features
    """
    return pd.DataFrame({
        'pred_high_5m': pred_5m['high'],
        'pred_low_5m': pred_5m['low'],
        'pred_high_15m': pred_15m['high'],
        'pred_low_15m': pred_15m['low'],
        'attention_5m': attention_5m[0],
        'attention_15m': attention_15m[0],
        'attention_class_5m': attention_5m[1],
        'attention_class_15m': attention_15m[1],
        'ATR_ratio': context_features['ATR_ratio'],
        'volume_z': context_features['volume_z']
    })


if __name__ == "__main__":
    # Test the module
    print("Testing AssetMetamodel...")

    np.random.seed(42)
    n = 5000

    # Simulate meta features
    pred_high_5m = np.random.randn(n) * 5 + 10
    pred_low_5m = np.random.randn(n) * 4 + 8
    pred_high_15m = np.random.randn(n) * 6 + 12
    pred_low_15m = np.random.randn(n) * 5 + 9
    attention_5m = np.random.rand(n) * 2 + 0.5
    attention_15m = np.random.rand(n) * 2 + 0.5
    attention_class_5m = np.random.choice([0, 1, 2], n)
    attention_class_15m = np.random.choice([0, 1, 2], n)
    atr_ratio = np.random.rand(n) + 0.5
    volume_z = np.random.randn(n)

    meta_features = pd.DataFrame({
        'pred_high_5m': pred_high_5m,
        'pred_low_5m': pred_low_5m,
        'pred_high_15m': pred_high_15m,
        'pred_low_15m': pred_low_15m,
        'attention_5m': attention_5m,
        'attention_15m': attention_15m,
        'attention_class_5m': attention_class_5m,
        'attention_class_15m': attention_class_15m,
        'ATR_ratio': atr_ratio,
        'volume_z': volume_z
    })

    # Simulate targets (correlated with predictions)
    target_high = (pred_high_5m + pred_high_15m) / 2 + np.random.randn(n) * 3
    target_low = (pred_low_5m + pred_low_15m) / 2 + np.random.randn(n) * 3
    target_high = np.maximum(target_high, 0)
    target_low = np.maximum(target_low, 0)

    # Test model
    config = MetamodelConfig(min_train_samples=1000)
    metamodel = AssetMetamodel('XAUUSD', config)
    metamodel.fit(meta_features, target_high, target_low)

    # Test prediction
    print("\nTesting predictions...")
    test_features = meta_features.iloc[-100:]
    prediction = metamodel.predict(test_features)

    print(f"Delta high: min={prediction.delta_high_final.min():.2f}, "
          f"max={prediction.delta_high_final.max():.2f}, "
          f"mean={prediction.delta_high_final.mean():.2f}")

    print(f"Delta low: min={prediction.delta_low_final.min():.2f}, "
          f"max={prediction.delta_low_final.max():.2f}, "
          f"mean={prediction.delta_low_final.mean():.2f}")

    print(f"Confidence: {prediction.confidence.mean():.1%} confident")

    # Test save/load
    print("\nTesting save/load...")
    metamodel.save('/tmp/test_metamodel')
    loaded_model = AssetMetamodel.load('/tmp/test_metamodel')

    pred2 = loaded_model.predict(test_features.iloc[:5])
    print(f"Loaded model predictions: {pred2.delta_high_final}")

    # Test registry
    print("\nTesting MetamodelRegistry...")
    registry = MetamodelRegistry()
    registry.add(metamodel)

    pred3 = registry.predict(test_features.iloc[:5], 'XAUUSD')
    print(f"Registry predictions: {pred3.delta_high_final}")

    print("\nTest complete!")
