#!/usr/bin/env python3
"""
Ensemble Pipeline
==================
Orchestrates the 5 trading strategies and combines their predictions
using the neural gating network.

Pipeline Steps:
1. Load all 5 strategy models for given symbols
2. Generate predictions from each strategy
3. Extract market context features
4. Apply gating network to compute weights
5. Combine predictions into final ensemble output

Author: ML-Specialist (NEXUS v4.0)
Version: 1.0.0
Created: 2026-01-25
"""

import numpy as np
import pandas as pd
import torch
from typing import Dict, List, Optional, Tuple, Any, Union
from dataclasses import dataclass, field
from pathlib import Path
from loguru import logger
from concurrent.futures import ThreadPoolExecutor
import time

from .gating_network import GatingNetwork, GatingConfig, ContextEncoder, StrategyOutputEncoder


@dataclass
class EnsemblePrediction:
    """Final ensemble prediction combining all strategies."""

    # Core predictions
    direction: float  # -1 (bearish) to 1 (bullish)
    magnitude: float  # Expected return magnitude (%)
    confidence: float  # 0 to 1, calibrated confidence

    # Strategy weights
    strategy_weights: Dict[str, float]  # Weight per strategy
    dominant_strategy: str  # Strategy with highest weight

    # Individual strategy predictions
    strategy_predictions: Dict[str, Dict[str, float]]

    # Metadata
    timestamp: Optional[pd.Timestamp] = None
    symbol: Optional[str] = None
    processing_time_ms: float = 0.0

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'direction': float(self.direction),
            'magnitude': float(self.magnitude),
            'confidence': float(self.confidence),
            'strategy_weights': self.strategy_weights,
            'dominant_strategy': self.dominant_strategy,
            'strategy_predictions': self.strategy_predictions,
            'timestamp': str(self.timestamp) if self.timestamp else None,
            'symbol': self.symbol,
            'processing_time_ms': self.processing_time_ms
        }

    @property
    def signal(self) -> str:
        """Get trading signal string."""
        if self.confidence < 0.5:
            return 'HOLD'
        if self.direction > 0.3:
            return 'BUY'
        elif self.direction < -0.3:
            return 'SELL'
        return 'HOLD'

    @property
    def signal_strength(self) -> str:
        """Get signal strength classification."""
        strength = abs(self.direction) * self.confidence
        if strength > 0.7:
            return 'STRONG'
        elif strength > 0.4:
            return 'MODERATE'
        elif strength > 0.2:
            return 'WEAK'
        return 'NEUTRAL'


@dataclass
class StrategyPredictions:
    """Container for all strategy predictions."""

    pva: Optional[Dict[str, float]] = None
    mrd: Optional[Dict[str, float]] = None
    vbp: Optional[Dict[str, float]] = None
    msa: Optional[Dict[str, float]] = None
    mts: Optional[Dict[str, float]] = None

    def to_dict(self) -> Dict[str, Optional[Dict[str, float]]]:
        return {
            'pva': self.pva,
            'mrd': self.mrd,
            'vbp': self.vbp,
            'msa': self.msa,
            'mts': self.mts
        }

    def get_available(self) -> Dict[str, Dict[str, float]]:
        """Get only non-None predictions."""
        return {
            k: v for k, v in self.to_dict().items()
            if v is not None
        }


class EnsemblePipeline:
    """
    Pipeline for ensemble prediction using 5 trading strategies.

    Orchestrates:
    - PVA (Price Variation Attention)
    - MRD (Momentum Regime Detection)
    - VBP (Volatility Breakout Predictor)
    - MSA (Market Structure Analysis)
    - MTS (Multi-Timeframe Synthesis)

    Usage:
        pipeline = EnsemblePipeline()
        pipeline.load_strategies(symbols=['EURUSD', 'GBPUSD'])

        prediction = pipeline.predict(df_ohlcv)
        print(f"Direction: {prediction.direction:.3f}")
        print(f"Dominant strategy: {prediction.dominant_strategy}")
    """

    def __init__(
        self,
        gating_config: Optional[GatingConfig] = None,
        models_dir: Optional[str] = None,
        device: Optional[str] = None,
        parallel_inference: bool = True
    ):
        """
        Initialize the ensemble pipeline.

        Args:
            gating_config: Configuration for gating network
            models_dir: Directory containing saved strategy models
            device: PyTorch device ('cuda' or 'cpu')
            parallel_inference: Run strategy inference in parallel
        """
        self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu')
        self.models_dir = Path(models_dir) if models_dir else None
        self.parallel_inference = parallel_inference

        # Initialize components
        self.gating_config = gating_config or GatingConfig()
        self.gating_network = GatingNetwork(self.gating_config).to(self.device)
        self.context_encoder = ContextEncoder().to(self.device)
        self.strategy_encoder = StrategyOutputEncoder().to(self.device)

        # Strategy models (loaded lazily)
        self.strategies: Dict[str, Any] = {}
        self.loaded_symbols: List[str] = []

        # Performance tracking
        self._prediction_times: List[float] = []

        logger.info(
            f"EnsemblePipeline initialized: device={self.device}, "
            f"parallel={parallel_inference}"
        )

    def load_strategies(
        self,
        symbols: List[str],
        strategies_to_load: Optional[List[str]] = None
    ) -> Dict[str, bool]:
        """
        Load strategy models for given symbols.

        Args:
            symbols: List of trading symbols to load models for
            strategies_to_load: Specific strategies to load (default: all)

        Returns:
            Dictionary mapping strategy name to load success status
        """
        strategy_names = strategies_to_load or ['pva', 'mrd', 'vbp', 'msa', 'mts']
        load_status = {}

        for strategy in strategy_names:
            try:
                model = self._load_strategy_model(strategy, symbols)
                if model is not None:
                    self.strategies[strategy] = model
                    load_status[strategy] = True
                    logger.info(f"Loaded {strategy.upper()} model")
                else:
                    load_status[strategy] = False
                    logger.warning(f"Failed to load {strategy.upper()} model")
            except Exception as e:
                load_status[strategy] = False
                logger.error(f"Error loading {strategy.upper()}: {e}")

        self.loaded_symbols = symbols
        logger.info(f"Loaded {sum(load_status.values())}/{len(strategy_names)} strategies")

        return load_status

    def _load_strategy_model(
        self,
        strategy_name: str,
        symbols: List[str]
    ) -> Optional[Any]:
        """
        Load a specific strategy model.

        Args:
            strategy_name: Name of strategy to load
            symbols: Symbols the model should support

        Returns:
            Loaded model or None if loading fails
        """
        if self.models_dir is None:
            # Return mock model for testing
            return self._create_mock_model(strategy_name)

        model_path = self.models_dir / strategy_name

        if not model_path.exists():
            logger.warning(f"Model path does not exist: {model_path}")
            return self._create_mock_model(strategy_name)

        try:
            # Import and load strategy-specific model
            if strategy_name == 'pva':
                from ..strategies.pva import PVAModel
                model = PVAModel.load(str(model_path), device=self.device)
            elif strategy_name == 'mrd':
                from ..strategies.mrd import MRDModel
                model = MRDModel()
                model.load(str(model_path))
            elif strategy_name == 'vbp':
                from ..strategies.vbp import VBPModel
                model = VBPModel()
                model.load(str(model_path))
            elif strategy_name == 'msa':
                from ..strategies.msa import MSAModel
                model = MSAModel()
                model.load(str(model_path))
            elif strategy_name == 'mts':
                from ..strategies.mts import MTSModel
                model = MTSModel()
                model.load(str(model_path))
            else:
                return None

            return model

        except Exception as e:
            logger.error(f"Failed to load {strategy_name}: {e}")
            return self._create_mock_model(strategy_name)

    def _create_mock_model(self, strategy_name: str) -> 'MockStrategyModel':
        """Create a mock model for testing when real model is unavailable."""
        return MockStrategyModel(strategy_name)

    def predict_all_strategies(
        self,
        df: pd.DataFrame,
        symbol: Optional[str] = None
    ) -> StrategyPredictions:
        """
        Generate predictions from all loaded strategies.

        Args:
            df: OHLCV DataFrame with required data
            symbol: Trading symbol (optional)

        Returns:
            StrategyPredictions container with all predictions
        """
        predictions = StrategyPredictions()

        if self.parallel_inference and len(self.strategies) > 1:
            # Run predictions in parallel
            with ThreadPoolExecutor(max_workers=5) as executor:
                futures = {
                    name: executor.submit(self._predict_strategy, name, model, df)
                    for name, model in self.strategies.items()
                }

                for name, future in futures.items():
                    try:
                        result = future.result(timeout=10.0)
                        setattr(predictions, name, result)
                    except Exception as e:
                        logger.warning(f"Strategy {name} prediction failed: {e}")
        else:
            # Sequential prediction
            for name, model in self.strategies.items():
                try:
                    result = self._predict_strategy(name, model, df)
                    setattr(predictions, name, result)
                except Exception as e:
                    logger.warning(f"Strategy {name} prediction failed: {e}")

        return predictions

    def _predict_strategy(
        self,
        strategy_name: str,
        model: Any,
        df: pd.DataFrame
    ) -> Dict[str, float]:
        """
        Generate prediction from a single strategy.

        Args:
            strategy_name: Name of strategy
            model: Strategy model instance
            df: OHLCV DataFrame

        Returns:
            Dictionary with prediction values
        """
        try:
            if strategy_name == 'pva':
                return self._predict_pva(model, df)
            elif strategy_name == 'mrd':
                return self._predict_mrd(model, df)
            elif strategy_name == 'vbp':
                return self._predict_vbp(model, df)
            elif strategy_name == 'msa':
                return self._predict_msa(model, df)
            elif strategy_name == 'mts':
                return self._predict_mts(model, df)
            else:
                raise ValueError(f"Unknown strategy: {strategy_name}")
        except Exception as e:
            logger.error(f"Error in {strategy_name} prediction: {e}")
            return self._get_neutral_prediction(strategy_name)

    def _predict_pva(self, model: Any, df: pd.DataFrame) -> Dict[str, float]:
        """Generate PVA prediction."""
        if isinstance(model, MockStrategyModel):
            return model.predict(df)

        pred = model.predict(df)
        return {
            'direction': float(pred.direction),
            'magnitude': float(pred.magnitude),
            'confidence': float(pred.confidence),
            'raw_prediction': float(pred.raw_prediction)
        }

    def _predict_mrd(self, model: Any, df: pd.DataFrame) -> Dict[str, float]:
        """Generate MRD prediction."""
        if isinstance(model, MockStrategyModel):
            return model.predict(df)

        pred = model.predict(df)
        # Convert regime to direction
        regime_to_direction = {0: -1.0, 1: 0.0, 2: 1.0}  # down, range, up
        return {
            'regime': float(pred.regime),
            'duration': float(pred.duration),
            'continuation_prob': float(pred.continuation_prob),
            'reversal_prob': float(pred.reversal_prob),
            'regime_confidence': float(pred.regime_confidence)
        }

    def _predict_vbp(self, model: Any, df: pd.DataFrame) -> Dict[str, float]:
        """Generate VBP prediction."""
        if isinstance(model, MockStrategyModel):
            return model.predict(df)

        preds = model.predict(df)
        pred = preds[-1] if isinstance(preds, list) else preds
        return {
            'breakout_prob': float(pred.breakout_probability),
            'direction': float(pred.direction),
            'magnitude': float(pred.magnitude),
            'confidence': float(pred.confidence),
            'is_breakout': float(pred.is_breakout)
        }

    def _predict_msa(self, model: Any, df: pd.DataFrame) -> Dict[str, float]:
        """Generate MSA prediction."""
        if isinstance(model, MockStrategyModel):
            return model.predict(df)

        preds = model.predict(df)
        pred = preds[-1] if isinstance(preds, list) else preds

        # Map direction to float
        direction_map = {'neutral': 0.0, 'bullish': 1.0, 'bearish': -1.0}
        direction = direction_map.get(pred.next_bos_direction, 0.0)

        return {
            'bos_conf': float(pred.bos_confidence),
            'poi_reaction_prob': float(pred.poi_reaction_prob),
            'structure_continuation_prob': float(pred.structure_continuation_prob),
            'is_bullish': 1.0 if direction > 0 else 0.0,
            'is_bearish': 1.0 if direction < 0 else 0.0
        }

    def _predict_mts(self, model: Any, df: pd.DataFrame) -> Dict[str, float]:
        """Generate MTS prediction."""
        if isinstance(model, MockStrategyModel):
            return model.predict(df)

        pred = model.predict(df)
        return {
            'unified_direction': float(pred.unified_direction),
            'confidence': float(pred.confidence),
            'confidence_by_alignment': float(pred.confidence_by_alignment),
            'signal_strength': float(pred.signal_strength),
            'is_buy': 1.0 if pred.recommended_action == 'buy' else 0.0
        }

    def _get_neutral_prediction(self, strategy_name: str) -> Dict[str, float]:
        """Get neutral prediction for a strategy."""
        neutral_predictions = {
            'pva': {'direction': 0.0, 'magnitude': 0.0, 'confidence': 0.0, 'raw_prediction': 0.0},
            'mrd': {'regime': 1.0, 'duration': 0.0, 'continuation_prob': 0.5,
                    'reversal_prob': 0.5, 'regime_confidence': 0.0},
            'vbp': {'breakout_prob': 0.0, 'direction': 0.0, 'magnitude': 0.0,
                    'confidence': 0.0, 'is_breakout': 0.0},
            'msa': {'bos_conf': 0.0, 'poi_reaction_prob': 0.0,
                    'structure_continuation_prob': 0.5, 'is_bullish': 0.0, 'is_bearish': 0.0},
            'mts': {'unified_direction': 0.0, 'confidence': 0.0,
                    'confidence_by_alignment': 0.0, 'signal_strength': 0.0, 'is_buy': 0.0}
        }
        return neutral_predictions.get(strategy_name, {})

    def extract_market_context(self, df: pd.DataFrame) -> torch.Tensor:
        """
        Extract market context features from OHLCV data.

        Features include:
        - Volatility metrics
        - Trend indicators
        - Volume characteristics
        - Recent performance

        Args:
            df: OHLCV DataFrame

        Returns:
            Context tensor (1, context_dim)
        """
        features = []

        # Price-based features
        close = df['close'].values
        high = df['high'].values
        low = df['low'].values
        volume = df['volume'].values if 'volume' in df.columns else np.ones(len(df))

        # Returns
        returns = np.diff(close) / close[:-1]
        returns = np.concatenate([[0], returns])

        # Volatility (multiple windows)
        for window in [10, 20, 50]:
            if len(returns) >= window:
                vol = pd.Series(returns).rolling(window).std().iloc[-1]
                features.append(vol if not np.isnan(vol) else 0.0)
            else:
                features.append(0.0)

        # ATR
        tr = np.maximum(high - low,
                        np.maximum(np.abs(high - np.roll(close, 1)),
                                   np.abs(low - np.roll(close, 1))))
        for window in [14, 28]:
            if len(tr) >= window:
                atr = pd.Series(tr).rolling(window).mean().iloc[-1]
                features.append(atr / close[-1] if close[-1] > 0 else 0.0)
            else:
                features.append(0.0)

        # Trend strength (simple moving average slope)
        for window in [10, 20, 50]:
            if len(close) >= window:
                sma = pd.Series(close).rolling(window).mean()
                slope = (sma.iloc[-1] - sma.iloc[-min(5, window)]) / close[-1]
                features.append(slope if not np.isnan(slope) else 0.0)
            else:
                features.append(0.0)

        # Momentum
        for period in [5, 10, 20]:
            if len(close) > period:
                mom = (close[-1] - close[-period-1]) / close[-period-1]
                features.append(mom)
            else:
                features.append(0.0)

        # Volume features
        if len(volume) >= 20:
            vol_ma = pd.Series(volume).rolling(20).mean().iloc[-1]
            vol_ratio = volume[-1] / vol_ma if vol_ma > 0 else 1.0
            features.append(vol_ratio)
        else:
            features.append(1.0)

        # Recent performance (cumulative returns)
        for period in [5, 10, 20]:
            if len(returns) >= period:
                cum_ret = np.sum(returns[-period:])
                features.append(cum_ret)
            else:
                features.append(0.0)

        # Range position (0-1)
        if len(close) >= 20:
            recent_high = high[-20:].max()
            recent_low = low[-20:].min()
            range_pos = (close[-1] - recent_low) / (recent_high - recent_low + 1e-8)
            features.append(range_pos)
        else:
            features.append(0.5)

        # Pad to expected dimension
        expected_dim = 50
        while len(features) < expected_dim:
            features.append(0.0)

        features = features[:expected_dim]

        # Convert to tensor
        context = torch.tensor(features, dtype=torch.float32, device=self.device)
        context = context.unsqueeze(0)  # Add batch dimension

        # Encode context
        with torch.no_grad():
            encoded = self.context_encoder(context)

        return encoded

    def apply_gating(
        self,
        strategy_predictions: StrategyPredictions,
        context: torch.Tensor
    ) -> Tuple[torch.Tensor, Dict[str, float]]:
        """
        Apply gating network to combine strategy predictions.

        Args:
            strategy_predictions: All strategy predictions
            context: Encoded market context

        Returns:
            Tuple of (weights tensor, weights dict)
        """
        # Encode strategy outputs to tensor format
        strategy_tensors = {}
        available = strategy_predictions.get_available()

        for name, pred in available.items():
            # Convert prediction dict to tensor
            values = list(pred.values())[:5]  # Take first 5 values
            while len(values) < 5:
                values.append(0.0)

            tensor = torch.tensor(values, dtype=torch.float32, device=self.device)
            tensor = tensor.unsqueeze(0)  # Add batch dimension

            # Encode to common dimension
            encoded = self.strategy_encoder(name, tensor)
            strategy_tensors[name] = encoded

        # Compute gating weights
        self.gating_network.eval()
        with torch.no_grad():
            weights = self.gating_network(strategy_tensors, context)

        # Convert to dict
        weights_dict = {
            name: float(weights[0, i].item())
            for i, name in enumerate(self.gating_network.STRATEGY_NAMES)
        }

        return weights, weights_dict

    def predict(
        self,
        df: pd.DataFrame,
        symbol: Optional[str] = None
    ) -> EnsemblePrediction:
        """
        Generate ensemble prediction combining all strategies.

        Args:
            df: OHLCV DataFrame
            symbol: Trading symbol (optional)

        Returns:
            EnsemblePrediction with combined prediction
        """
        start_time = time.time()

        # Step 1: Get predictions from all strategies
        strategy_preds = self.predict_all_strategies(df, symbol)

        # Step 2: Extract market context
        context = self.extract_market_context(df)

        # Step 3: Apply gating to get weights
        weights_tensor, weights_dict = self.apply_gating(strategy_preds, context)

        # Step 4: Combine predictions
        available = strategy_preds.get_available()

        # Compute weighted direction
        total_direction = 0.0
        total_magnitude = 0.0
        total_confidence = 0.0
        total_weight = 0.0

        for name, pred in available.items():
            weight = weights_dict.get(name, 0.0)

            # Extract direction from each strategy
            if name == 'pva':
                direction = pred.get('direction', 0.0)
                magnitude = pred.get('magnitude', 0.0)
                confidence = pred.get('confidence', 0.0)
            elif name == 'mrd':
                # Map regime to direction
                regime = pred.get('regime', 1.0)
                direction = {0: -1.0, 1: 0.0, 2: 1.0}.get(int(regime), 0.0)
                direction *= pred.get('continuation_prob', 0.5)
                magnitude = 0.01  # MRD doesn't predict magnitude directly
                confidence = pred.get('regime_confidence', 0.0)
            elif name == 'vbp':
                direction = pred.get('direction', 0.0)
                magnitude = pred.get('magnitude', 0.0)
                confidence = pred.get('confidence', 0.0) * pred.get('breakout_prob', 0.0)
            elif name == 'msa':
                is_bull = pred.get('is_bullish', 0.0)
                is_bear = pred.get('is_bearish', 0.0)
                direction = is_bull - is_bear
                direction *= pred.get('bos_conf', 0.0)
                magnitude = 0.01
                confidence = pred.get('bos_conf', 0.0)
            elif name == 'mts':
                direction = pred.get('unified_direction', 0.0)
                magnitude = 0.01
                confidence = pred.get('confidence', 0.0)
            else:
                continue

            total_direction += direction * weight
            total_magnitude += magnitude * weight
            total_confidence += confidence * weight
            total_weight += weight

        # Normalize
        if total_weight > 0:
            total_direction /= total_weight
            total_magnitude /= total_weight
            total_confidence /= total_weight

        # Clamp values
        total_direction = np.clip(total_direction, -1.0, 1.0)
        total_confidence = np.clip(total_confidence, 0.0, 1.0)

        # Find dominant strategy
        dominant_strategy = max(weights_dict.items(), key=lambda x: x[1])[0]

        # Calculate processing time
        processing_time = (time.time() - start_time) * 1000

        return EnsemblePrediction(
            direction=float(total_direction),
            magnitude=float(total_magnitude),
            confidence=float(total_confidence),
            strategy_weights=weights_dict,
            dominant_strategy=dominant_strategy,
            strategy_predictions={k: v for k, v in available.items()},
            timestamp=df.index[-1] if hasattr(df.index, '__getitem__') else None,
            symbol=symbol,
            processing_time_ms=processing_time
        )


class MockStrategyModel:
    """Mock strategy model for testing when real models are unavailable."""

    def __init__(self, strategy_name: str):
        self.strategy_name = strategy_name
        self._is_trained = True

    def predict(self, df: pd.DataFrame) -> Dict[str, float]:
        """Generate random mock prediction."""
        np.random.seed(hash(str(df.index[-1])) % (2**32 - 1) if len(df) > 0 else 42)

        if self.strategy_name == 'pva':
            return {
                'direction': np.random.uniform(-1, 1),
                'magnitude': np.random.uniform(0, 0.02),
                'confidence': np.random.uniform(0.3, 0.8),
                'raw_prediction': np.random.uniform(-0.01, 0.01)
            }
        elif self.strategy_name == 'mrd':
            return {
                'regime': float(np.random.choice([0, 1, 2])),
                'duration': float(np.random.randint(1, 50)),
                'continuation_prob': np.random.uniform(0.4, 0.8),
                'reversal_prob': np.random.uniform(0.2, 0.6),
                'regime_confidence': np.random.uniform(0.5, 0.9)
            }
        elif self.strategy_name == 'vbp':
            return {
                'breakout_prob': np.random.uniform(0, 1),
                'direction': np.random.choice([-1.0, 0.0, 1.0]),
                'magnitude': np.random.uniform(0, 0.03),
                'confidence': np.random.uniform(0.3, 0.8),
                'is_breakout': float(np.random.random() > 0.7)
            }
        elif self.strategy_name == 'msa':
            return {
                'bos_conf': np.random.uniform(0.3, 0.9),
                'poi_reaction_prob': np.random.uniform(0.2, 0.8),
                'structure_continuation_prob': np.random.uniform(0.3, 0.7),
                'is_bullish': float(np.random.random() > 0.5),
                'is_bearish': float(np.random.random() > 0.5)
            }
        elif self.strategy_name == 'mts':
            return {
                'unified_direction': np.random.uniform(-1, 1),
                'confidence': np.random.uniform(0.4, 0.9),
                'confidence_by_alignment': np.random.uniform(0.3, 0.8),
                'signal_strength': np.random.uniform(0.2, 0.7),
                'is_buy': float(np.random.random() > 0.5)
            }
        else:
            return {'value': 0.0}


if __name__ == "__main__":
    # Test the ensemble pipeline
    torch.manual_seed(42)
    np.random.seed(42)

    print("Testing EnsemblePipeline")
    print("=" * 60)

    # Create sample OHLCV data
    n_samples = 200
    dates = pd.date_range('2025-01-01', periods=n_samples, freq='5min')

    np.random.seed(42)
    price = 100 * np.cumprod(1 + np.random.randn(n_samples) * 0.001)

    df = pd.DataFrame({
        'open': price * (1 + np.random.randn(n_samples) * 0.001),
        'high': price * (1 + np.abs(np.random.randn(n_samples) * 0.002)),
        'low': price * (1 - np.abs(np.random.randn(n_samples) * 0.002)),
        'close': price,
        'volume': np.random.randint(1000, 10000, n_samples)
    }, index=dates)

    # Initialize pipeline
    pipeline = EnsemblePipeline()

    # Load strategies (will use mock models)
    load_status = pipeline.load_strategies(['EURUSD'])
    print(f"\nLoad status: {load_status}")

    # Generate prediction
    prediction = pipeline.predict(df, symbol='EURUSD')

    print(f"\n--- Ensemble Prediction ---")
    print(f"Direction: {prediction.direction:.4f}")
    print(f"Magnitude: {prediction.magnitude:.4f}")
    print(f"Confidence: {prediction.confidence:.4f}")
    print(f"Signal: {prediction.signal}")
    print(f"Signal Strength: {prediction.signal_strength}")
    print(f"Dominant Strategy: {prediction.dominant_strategy}")
    print(f"Processing Time: {prediction.processing_time_ms:.2f} ms")

    print(f"\n--- Strategy Weights ---")
    for name, weight in prediction.strategy_weights.items():
        print(f"  {name.upper()}: {weight:.4f}")

    print(f"\n--- Individual Predictions ---")
    for name, pred in prediction.strategy_predictions.items():
        print(f"  {name.upper()}: {pred}")

    print("\nEnsemblePipeline test complete!")
