//! Asset Selection Logic
//!
//! Filters and ranks assets for trading within selected universe.
//! Uses multi-factor scoring with ML integration:
//! - ML predictions: 40% weight
//! - Momentum: 30% weight
//! - Value: 20% weight
//! - Liquidity (quality): 10% weight

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Asset scoring result with multi-factor breakdown
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssetScore {
    /// Trading symbol
    pub symbol: String,

    /// ML model prediction score (0.0-1.0)
    /// Weight: 40%
    pub ml_score: f64,

    /// Momentum factor score (0.0-1.0)
    /// Weight: 30%
    pub momentum_score: f64,

    /// Value factor score (0.0-1.0)
    /// Weight: 20%
    pub value_score: f64,

    /// Quality/liquidity factor score (0.0-1.0)
    /// Weight: 10%
    pub quality_score: f64,

    /// Final composite score (weighted average)
    pub composite_score: f64,

    /// Per-model prediction scores (DQN, PPO, MAMBA2, TFT)
    pub model_scores: HashMap<String, f64>,
}

impl AssetScore {
    /// Factor weights for composite score calculation
    pub const ML_WEIGHT: f64 = 0.40;
    pub const MOMENTUM_WEIGHT: f64 = 0.30;
    pub const VALUE_WEIGHT: f64 = 0.20;
    pub const LIQUIDITY_WEIGHT: f64 = 0.10;

    /// Create a new asset score with calculated composite
    pub fn new(
        symbol: String,
        ml_score: f64,
        momentum_score: f64,
        value_score: f64,
        quality_score: f64,
    ) -> Self {
        // Clamp all scores to valid range
        let ml = Self::clamp_score(ml_score);
        let momentum = Self::clamp_score(momentum_score);
        let value = Self::clamp_score(value_score);
        let quality = Self::clamp_score(quality_score);

        // Calculate weighted composite score
        let composite = ml * Self::ML_WEIGHT
            + momentum * Self::MOMENTUM_WEIGHT
            + value * Self::VALUE_WEIGHT
            + quality * Self::LIQUIDITY_WEIGHT;

        Self {
            symbol,
            ml_score: ml,
            momentum_score: momentum,
            value_score: value,
            quality_score: quality,
            composite_score: composite,
            model_scores: HashMap::new(),
        }
    }

    /// Create with model scores for ML ensemble
    pub fn with_model_scores(
        symbol: String,
        model_scores: HashMap<String, f64>,
        momentum_score: f64,
        value_score: f64,
        quality_score: f64,
    ) -> Self {
        // Calculate ML score as average of model scores
        let ml_score = if model_scores.is_empty() {
            0.0
        } else {
            model_scores.values().sum::<f64>() / model_scores.len() as f64
        };

        let mut asset = Self::new(symbol, ml_score, momentum_score, value_score, quality_score);
        asset.model_scores = model_scores;
        asset
    }

    /// Clamp score to valid 0.0-1.0 range, handling NaN and infinity
    fn clamp_score(score: f64) -> f64 {
        if score.is_nan() {
            0.0
        } else if score.is_infinite() {
            if score.is_sign_positive() {
                1.0
            } else {
                0.0
            }
        } else {
            score.clamp(0.0, 1.0)
        }
    }
}

/// Asset selector for ranking and filtering
pub struct AssetSelector {
    /// Minimum ML confidence threshold
    min_ml_confidence: f64,

    /// Minimum composite score threshold
    min_composite_score: f64,
}

impl AssetSelector {
    /// Create a new asset selector with default thresholds
    pub fn new() -> Self {
        Self {
            min_ml_confidence: 0.0,
            min_composite_score: 0.0,
        }
    }

    /// Create with custom thresholds
    pub fn with_thresholds(min_ml_confidence: f64, min_composite_score: f64) -> Self {
        Self {
            min_ml_confidence,
            min_composite_score,
        }
    }

    /// Select top N assets by composite score
    pub fn select_top_n(&self, mut assets: Vec<AssetScore>, n: usize) -> Vec<AssetScore> {
        // Filter by thresholds
        assets.retain(|asset| {
            asset.ml_score >= self.min_ml_confidence
                && asset.composite_score >= self.min_composite_score
        });

        // Sort by composite score descending
        assets.sort_by(|a, b| {
            b.composite_score
                .partial_cmp(&a.composite_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        // Take top N
        assets.into_iter().take(n).collect()
    }

    /// Select assets above threshold
    pub fn select_above_threshold(&self, mut assets: Vec<AssetScore>) -> Vec<AssetScore> {
        // Filter by thresholds
        assets.retain(|asset| {
            asset.ml_score >= self.min_ml_confidence
                && asset.composite_score >= self.min_composite_score
        });

        // Sort by composite score descending
        assets.sort_by(|a, b| {
            b.composite_score
                .partial_cmp(&a.composite_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        assets
    }

    /// Select top quantile (e.g., top 20%)
    pub fn select_top_quantile(
        &self,
        mut assets: Vec<AssetScore>,
        quantile: f64,
    ) -> Vec<AssetScore> {
        if quantile <= 0.0 || quantile > 1.0 {
            return vec![];
        }

        // Filter by thresholds
        assets.retain(|asset| {
            asset.ml_score >= self.min_ml_confidence
                && asset.composite_score >= self.min_composite_score
        });

        // Sort by composite score descending
        assets.sort_by(|a, b| {
            b.composite_score
                .partial_cmp(&a.composite_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        let n = (assets.len() as f64 * quantile).ceil() as usize;
        assets.into_iter().take(n).collect()
    }
}

impl Default for AssetSelector {
    fn default() -> Self {
        Self::new()
    }
}

/// Calculate momentum score from extracted features
///
/// Uses Wave A technical indicators:
/// - RSI (feature 23): Overbought/oversold detection
/// - MACD (feature 24): Momentum direction
/// - Stochastic (features 20-21): Short-term momentum
/// - ADX (feature 18): Trend strength
pub fn calculate_momentum_from_features(features: &[f64]) -> f64 {
    if features.len() < 26 {
        return 0.5; // Neutral if insufficient features
    }

    // Extract momentum indicators (all normalized to [-1, 1] or [0, 1])
    let rsi = features.get(23).copied().unwrap_or(0.5); // [0, 1] - 0.5 is neutral
    let macd = features.get(24).copied().unwrap_or(0.0); // [-1, 1] - positive = bullish
    let stoch_k = features.get(20).copied().unwrap_or(0.5); // [0, 1] - >0.8 overbought, <0.2 oversold
    let adx = features.get(18).copied().unwrap_or(0.5); // [0, 1] - trend strength

    // Weight by reliability:
    // - RSI: 30% (reliable mean-reversion signal)
    // - MACD: 40% (strong momentum indicator)
    // - Stochastic: 20% (short-term momentum)
    // - ADX: 10% (trend strength amplifier)

    let rsi_signal = (rsi - 0.5) * 2.0; // Convert [0, 1] → [-1, 1]
    let stoch_signal = (stoch_k - 0.5) * 2.0;

    let composite =
        rsi_signal * 0.30 + macd * 0.40 + stoch_signal * 0.20 + (adx - 0.5) * 2.0 * 0.10; // ADX amplifies signals

    // Normalize to [0, 1] using sigmoid with amplification for stronger signals
    // Amplify by 3x to ensure bullish/bearish signals reach the expected thresholds (>0.7 or <0.3)
    let score = 1.0 / (1.0 + (-composite * 3.0).exp());
    score.clamp(0.0, 1.0)
}

/// Calculate momentum score from price data (legacy function)
pub fn calculate_momentum_score(returns: &[f64], lookback_periods: usize) -> f64 {
    if returns.is_empty() || lookback_periods == 0 {
        return 0.5; // Neutral
    }

    let relevant_returns: Vec<f64> = returns
        .iter()
        .rev()
        .take(lookback_periods)
        .copied()
        .collect();

    if relevant_returns.is_empty() {
        return 0.5;
    }

    // Calculate average return
    let avg_return: f64 = relevant_returns.iter().sum::<f64>() / relevant_returns.len() as f64;

    // Normalize to 0.0-1.0 range using sigmoid with amplification
    // Positive returns -> score > 0.5, negative returns -> score < 0.5
    // Amplify by 50x to ensure reasonable sigmoid response for typical HFT returns (0.01-0.02)
    let score = 1.0 / (1.0 + (-avg_return * 50.0).exp());

    score.clamp(0.0, 1.0)
}

/// Calculate value score from extracted features
///
/// Uses Wave A technical indicators for mean-reversion detection:
/// - Bollinger Bands (feature 19): Position relative to bands
/// - RSI (feature 23): Overbought/oversold detection
/// - Williams %R (feature 7): Momentum extreme
pub fn calculate_value_from_features(features: &[f64]) -> f64 {
    if features.len() < 26 {
        return 0.5; // Neutral if insufficient features
    }

    // Extract value indicators
    let bollinger_pos = features.get(19).copied().unwrap_or(0.0); // [-1, 1] - <-0.5 = undervalued, >0.5 = overvalued
    let rsi = features.get(23).copied().unwrap_or(0.5); // [0, 1] - <0.3 = oversold, >0.7 = overbought
    let williams_r = features.get(7).copied().unwrap_or(-0.5); // [-1, 1] - <-0.8 = oversold, >-0.2 = overbought

    // Weight by signal reliability:
    // - Bollinger: 50% (mean-reversion signal)
    // - RSI: 30% (overbought/oversold)
    // - Williams %R: 20% (momentum extreme)

    // Invert signals: Low Bollinger/RSI/Williams = undervalued (high score)
    let bollinger_signal = -bollinger_pos; // Invert: low position = high value
    let rsi_signal = (0.5 - rsi) * 2.0; // <0.5 = undervalued, >0.5 = overvalued
    let williams_signal = -williams_r; // Invert: low %R = high value

    let composite = bollinger_signal * 0.50 + rsi_signal * 0.30 + williams_signal * 0.20;

    // Normalize to [0, 1] using sigmoid with amplification for stronger signals
    // Scale factor of 2.0 ensures extreme values reach test thresholds (>0.7 or <0.3)
    let score = 1.0 / (1.0 + (-composite * 2.0).exp());
    score.clamp(0.0, 1.0)
}

/// Calculate value score from fundamental metrics (legacy function)
pub fn calculate_value_score(price: f64, fair_value: f64, volatility: f64) -> f64 {
    if price <= 0.0 || fair_value <= 0.0 {
        return 0.5; // Neutral
    }

    // Calculate discount/premium
    let discount = (fair_value - price) / fair_value;

    // Adjust for volatility (higher vol = less confident in valuation)
    let volatility_adj = 1.0 - (volatility / 2.0).min(0.5);

    // Normalize to 0.0-1.0 range
    // Discount (undervalued) -> score > 0.5
    // Premium (overvalued) -> score < 0.5
    let raw_score = 0.5 + (discount * volatility_adj);

    raw_score.clamp(0.0, 1.0)
}

/// Calculate liquidity score from extracted features
///
/// Uses Wave A volume and microstructure indicators:
/// - Volume ratio (feature 3): Volume momentum
/// - Volume MA ratio (feature 4): Volume trend
/// - OBV (feature 10): On-Balance Volume
/// - MFI (feature 11): Money Flow Index
pub fn calculate_liquidity_from_features(features: &[f64]) -> f64 {
    if features.len() < 26 {
        return 0.5; // Neutral if insufficient features
    }

    // Extract liquidity indicators (all normalized to [-1, 1])
    let volume_ratio = features.get(3).copied().unwrap_or(0.0); // Volume momentum
    let volume_ma = features.get(4).copied().unwrap_or(0.0); // Volume trend
    let obv = features.get(10).copied().unwrap_or(0.0); // On-Balance Volume
    let mfi = features.get(11).copied().unwrap_or(0.0); // Money Flow Index

    // Weight by signal reliability:
    // - Volume ratio: 30% (immediate liquidity)
    // - Volume MA: 25% (sustained liquidity)
    // - OBV: 25% (buying/selling pressure)
    // - MFI: 20% (volume-weighted momentum)

    // Higher volume = higher liquidity score
    let composite = volume_ratio * 0.30 + volume_ma * 0.25 + obv * 0.25 + mfi * 0.20;

    // Normalize to [0, 1] using sigmoid with amplification for stronger signals
    // Scale factor of 2.0 ensures extreme values reach test thresholds (>0.7 or <0.3)
    let score = 1.0 / (1.0 + (-composite * 2.0).exp());
    score.clamp(0.0, 1.0)
}

/// Calculate liquidity/quality score (legacy function)
pub fn calculate_liquidity_score(avg_volume: f64, spread_bps: f64, market_cap: Option<f64>) -> f64 {
    // Volume score (higher is better)
    let volume_score = if avg_volume > 0.0 {
        (avg_volume.ln() / 20.0).min(1.0) // Log scale, cap at 1.0
    } else {
        0.0
    };

    // Spread score (lower spread is better)
    let spread_score = if spread_bps > 0.0 {
        (1.0 / (1.0 + spread_bps)).min(1.0)
    } else {
        0.0
    };

    // Market cap score (if available)
    let cap_score = market_cap
        .map(|cap| {
            if cap > 0.0 {
                (cap.ln() / 30.0).min(1.0) // Log scale
            } else {
                0.0
            }
        })
        .unwrap_or(0.5); // Neutral if not available

    // Weighted average: volume 40%, spread 40%, cap 20%
    let score = volume_score * 0.40 + spread_score * 0.40 + cap_score * 0.20;

    score.clamp(0.0, 1.0)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_asset_score_creation() {
        let asset = AssetScore::new(
            "ES.FUT".to_string(),
            0.9,  // ml
            0.8,  // momentum
            0.7,  // value
            0.95, // liquidity
        );

        assert_eq!(asset.symbol, "ES.FUT");
        assert_eq!(asset.ml_score, 0.9);
        assert_eq!(asset.momentum_score, 0.8);
        assert_eq!(asset.value_score, 0.7);
        assert_eq!(asset.quality_score, 0.95);

        // Check composite calculation
        let expected = 0.9 * 0.4 + 0.8 * 0.3 + 0.7 * 0.2 + 0.95 * 0.1;
        assert!((asset.composite_score - expected).abs() < 0.001);
    }

    #[test]
    fn test_score_clamping() {
        let asset = AssetScore::new(
            "TEST.FUT".to_string(),
            1.5,  // Above 1.0
            -0.5, // Below 0.0
            f64::NAN,
            f64::INFINITY,
        );

        assert_eq!(asset.ml_score, 1.0);
        assert_eq!(asset.momentum_score, 0.0);
        assert_eq!(asset.value_score, 0.0);
        assert_eq!(asset.quality_score, 1.0);
    }

    #[test]
    fn test_factor_weights() {
        assert!((AssetScore::ML_WEIGHT - 0.40).abs() < 0.001);
        assert!((AssetScore::MOMENTUM_WEIGHT - 0.30).abs() < 0.001);
        assert!((AssetScore::VALUE_WEIGHT - 0.20).abs() < 0.001);
        assert!((AssetScore::LIQUIDITY_WEIGHT - 0.10).abs() < 0.001);

        // Sum should be 1.0
        let sum = AssetScore::ML_WEIGHT
            + AssetScore::MOMENTUM_WEIGHT
            + AssetScore::VALUE_WEIGHT
            + AssetScore::LIQUIDITY_WEIGHT;
        assert!((sum - 1.0).abs() < 0.001);
    }

    #[test]
    fn test_model_scores_aggregation() {
        let mut model_scores = HashMap::new();
        model_scores.insert("DQN".to_string(), 0.8);
        model_scores.insert("PPO".to_string(), 0.9);
        model_scores.insert("MAMBA2".to_string(), 0.85);
        model_scores.insert("TFT".to_string(), 0.75);

        let asset = AssetScore::with_model_scores(
            "ES.FUT".to_string(),
            model_scores.clone(),
            0.7,
            0.6,
            0.9,
        );

        // ML score should be average of model scores
        let expected_ml = (0.8 + 0.9 + 0.85 + 0.75) / 4.0;
        assert!((asset.ml_score - expected_ml).abs() < 0.001);
        assert_eq!(asset.model_scores.len(), 4);
    }

    #[test]
    fn test_selector_top_n() {
        let selector = AssetSelector::new();
        let assets = vec![
            AssetScore::new("A".into(), 0.9, 0.8, 0.7, 0.95),
            AssetScore::new("B".into(), 0.7, 0.6, 0.5, 0.85),
            AssetScore::new("C".into(), 0.8, 0.7, 0.6, 0.90),
        ];

        let selected = selector.select_top_n(assets, 2);

        assert_eq!(selected.len(), 2);
        assert_eq!(selected[0].symbol, "A");
        assert_eq!(selected[1].symbol, "C");
    }

    #[test]
    fn test_selector_with_thresholds() {
        let selector = AssetSelector::with_thresholds(0.5, 0.6);
        let assets = vec![
            AssetScore::new("A".into(), 0.9, 0.8, 0.7, 0.95), // Pass
            AssetScore::new("B".into(), 0.3, 0.6, 0.5, 0.85), // Fail ML threshold
            AssetScore::new("C".into(), 0.6, 0.4, 0.3, 0.75), // Fail composite threshold
        ];

        let selected = selector.select_top_n(assets, 10);

        assert_eq!(selected.len(), 1);
        assert_eq!(selected[0].symbol, "A");
    }

    #[test]
    fn test_momentum_calculation() {
        // Positive returns
        let returns = vec![0.01, 0.02, 0.015, 0.01];
        let score = calculate_momentum_score(&returns, 4);
        assert!(score > 0.5, "Positive returns should score > 0.5");

        // Negative returns
        let returns = vec![-0.01, -0.02, -0.015, -0.01];
        let score = calculate_momentum_score(&returns, 4);
        assert!(score < 0.5, "Negative returns should score < 0.5");
    }

    #[test]
    fn test_value_calculation() {
        // Undervalued (fair value > price)
        let score = calculate_value_score(100.0, 110.0, 0.2);
        assert!(score > 0.5, "Undervalued should score > 0.5");

        // Overvalued (fair value < price)
        let score = calculate_value_score(110.0, 100.0, 0.2);
        assert!(score < 0.5, "Overvalued should score < 0.5");
    }

    #[test]
    fn test_liquidity_calculation() {
        // High liquidity
        let score = calculate_liquidity_score(1_000_000.0, 0.5, Some(10_000_000_000.0));
        assert!(score > 0.65, "High liquidity should score high (got {})", score);

        // Low liquidity
        let score = calculate_liquidity_score(1_000.0, 5.0, Some(100_000.0));
        assert!(score < 0.5, "Low liquidity should score low");
    }

    // ===== Feature-Based Scoring Tests =====

    #[test]
    fn test_momentum_from_features_bullish() {
        // Create bullish feature vector (26 features)
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(23) { *f = 0.8; } // RSI high (overbought, bullish)
        if let Some(f) = features.get_mut(24) { *f = 0.7; } // MACD positive (bullish)
        if let Some(f) = features.get_mut(20) { *f = 0.9; } // Stochastic high (overbought, bullish)
        if let Some(f) = features.get_mut(18) { *f = 0.8; } // ADX high (strong trend)

        let score = calculate_momentum_from_features(&features);
        assert!(
            score > 0.7,
            "Bullish momentum should score > 0.7, got {}",
            score
        );
    }

    #[test]
    fn test_momentum_from_features_bearish() {
        // Create bearish feature vector
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(23) { *f = 0.2; } // RSI low (oversold, bearish)
        if let Some(f) = features.get_mut(24) { *f = -0.7; } // MACD negative (bearish)
        if let Some(f) = features.get_mut(20) { *f = 0.1; } // Stochastic low (oversold, bearish)
        if let Some(f) = features.get_mut(18) { *f = 0.7; } // ADX high (strong downtrend)

        let score = calculate_momentum_from_features(&features);
        assert!(
            score < 0.3,
            "Bearish momentum should score < 0.3, got {}",
            score
        );
    }

    #[test]
    fn test_momentum_from_features_neutral() {
        // Create neutral feature vector
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(23) { *f = 0.5; } // RSI neutral
        if let Some(f) = features.get_mut(24) { *f = 0.0; } // MACD neutral
        if let Some(f) = features.get_mut(20) { *f = 0.5; } // Stochastic neutral
        if let Some(f) = features.get_mut(18) { *f = 0.5; } // ADX neutral

        let score = calculate_momentum_from_features(&features);
        assert!(
            (score - 0.5).abs() < 0.1,
            "Neutral momentum should score ~0.5, got {}",
            score
        );
    }

    #[test]
    fn test_momentum_from_features_insufficient() {
        // Test with insufficient features
        let features = vec![0.5; 10]; // Only 10 features
        let score = calculate_momentum_from_features(&features);
        assert_eq!(score, 0.5, "Should return neutral on insufficient features");
    }

    #[test]
    fn test_value_from_features_undervalued() {
        // Create undervalued feature vector
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(19) { *f = -0.8; } // Bollinger low (undervalued)
        if let Some(f) = features.get_mut(23) { *f = 0.2; } // RSI low (oversold, undervalued)
        if let Some(f) = features.get_mut(7) { *f = -0.9; } // Williams %R low (oversold, undervalued)

        let score = calculate_value_from_features(&features);
        assert!(
            score > 0.7,
            "Undervalued asset should score > 0.7, got {}",
            score
        );
    }

    #[test]
    fn test_value_from_features_overvalued() {
        // Create overvalued feature vector
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(19) { *f = 0.8; } // Bollinger high (overvalued)
        if let Some(f) = features.get_mut(23) { *f = 0.8; } // RSI high (overbought, overvalued)
        if let Some(f) = features.get_mut(7) { *f = -0.1; } // Williams %R high (overbought, overvalued)

        let score = calculate_value_from_features(&features);
        assert!(
            score < 0.3,
            "Overvalued asset should score < 0.3, got {}",
            score
        );
    }

    #[test]
    fn test_value_from_features_neutral() {
        // Create neutral feature vector
        let mut features = vec![0.0; 26];
        features[19] = 0.0; // Bollinger neutral
        features[23] = 0.5; // RSI neutral
        features[7] = -0.5; // Williams %R neutral

        let score = calculate_value_from_features(&features);
        assert!(
            (score - 0.5).abs() < 0.1,
            "Neutral value should score ~0.5, got {}",
            score
        );
    }

    #[test]
    fn test_value_from_features_insufficient() {
        // Test with insufficient features
        let features = vec![0.5; 15];
        let score = calculate_value_from_features(&features);
        assert_eq!(score, 0.5, "Should return neutral on insufficient features");
    }

    #[test]
    fn test_liquidity_from_features_high() {
        // Create high liquidity feature vector
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(3) { *f = 0.8; } // Volume ratio high (strong volume)
        if let Some(f) = features.get_mut(4) { *f = 0.7; } // Volume MA high (sustained volume)
        if let Some(f) = features.get_mut(10) { *f = 0.6; } // OBV positive (buying pressure)
        if let Some(f) = features.get_mut(11) { *f = 0.7; } // MFI high (strong money flow)

        let score = calculate_liquidity_from_features(&features);
        assert!(
            score > 0.7,
            "High liquidity should score > 0.7, got {}",
            score
        );
    }

    #[test]
    fn test_liquidity_from_features_low() {
        // Create low liquidity feature vector
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(3) { *f = -0.8; } // Volume ratio low (weak volume)
        if let Some(f) = features.get_mut(4) { *f = -0.7; } // Volume MA low (declining volume)
        if let Some(f) = features.get_mut(10) { *f = -0.6; } // OBV negative (selling pressure)
        if let Some(f) = features.get_mut(11) { *f = -0.7; } // MFI low (weak money flow)

        let score = calculate_liquidity_from_features(&features);
        assert!(
            score < 0.3,
            "Low liquidity should score < 0.3, got {}",
            score
        );
    }

    #[test]
    fn test_liquidity_from_features_neutral() {
        // Create neutral feature vector
        let mut features = vec![0.0; 26];
        if let Some(f) = features.get_mut(3) { *f = 0.0; } // Volume ratio neutral
        if let Some(f) = features.get_mut(4) { *f = 0.0; } // Volume MA neutral
        if let Some(f) = features.get_mut(10) { *f = 0.0; } // OBV neutral
        if let Some(f) = features.get_mut(11) { *f = 0.0; } // MFI neutral

        let score = calculate_liquidity_from_features(&features);
        assert!(
            (score - 0.5).abs() < 0.1,
            "Neutral liquidity should score ~0.5, got {}",
            score
        );
    }

    #[test]
    fn test_liquidity_from_features_insufficient() {
        // Test with insufficient features
        let features = vec![0.5; 8];
        let score = calculate_liquidity_from_features(&features);
        assert_eq!(score, 0.5, "Should return neutral on insufficient features");
    }

    #[test]
    fn test_feature_based_scoring_consistency() {
        // Test that all three scoring functions handle edge cases consistently
        let mut features = vec![0.0; 26];

        // Test with all zeros
        let momentum = calculate_momentum_from_features(&features);
        let value = calculate_value_from_features(&features);
        let liquidity = calculate_liquidity_from_features(&features);

        // All should return finite values in [0, 1]
        assert!(momentum.is_finite() && (0.0..=1.0).contains(&momentum));
        assert!(value.is_finite() && (0.0..=1.0).contains(&value));
        assert!(liquidity.is_finite() && (0.0..=1.0).contains(&liquidity));

        // Test with extreme values
        for i in 0..26 {
            if let Some(f) = features.get_mut(i) {
                *f = 1.0;
            }
        }
        let momentum = calculate_momentum_from_features(&features);
        let value = calculate_value_from_features(&features);
        let liquidity = calculate_liquidity_from_features(&features);

        assert!(momentum.is_finite() && (0.0..=1.0).contains(&momentum));
        assert!(value.is_finite() && (0.0..=1.0).contains(&value));
        assert!(liquidity.is_finite() && (0.0..=1.0).contains(&liquidity));

        // Test with negative extremes
        for i in 0..26 {
            if let Some(f) = features.get_mut(i) {
                *f = -1.0;
            }
        }
        let momentum = calculate_momentum_from_features(&features);
        let value = calculate_value_from_features(&features);
        let liquidity = calculate_liquidity_from_features(&features);

        assert!(momentum.is_finite() && (0.0..=1.0).contains(&momentum));
        assert!(value.is_finite() && (0.0..=1.0).contains(&value));
        assert!(liquidity.is_finite() && (0.0..=1.0).contains(&liquidity));
    }

    #[test]
    fn test_feature_based_scoring_weight_validation() {
        // Verify that scoring weights sum to expected values
        let mut features = vec![0.5; 26];

        // Momentum weights: RSI 30%, MACD 40%, Stochastic 20%, ADX 10% = 100%
        if let Some(f) = features.get_mut(23) { *f = 0.6; } // RSI
        if let Some(f) = features.get_mut(24) { *f = 0.3; } // MACD
        if let Some(f) = features.get_mut(20) { *f = 0.7; } // Stochastic
        if let Some(f) = features.get_mut(18) { *f = 0.4; } // ADX

        let momentum = calculate_momentum_from_features(&features);
        assert!(momentum.is_finite());

        // Value weights: Bollinger 50%, RSI 30%, Williams 20% = 100%
        if let Some(f) = features.get_mut(19) { *f = -0.5; } // Bollinger
        if let Some(f) = features.get_mut(23) { *f = 0.3; } // RSI
        if let Some(f) = features.get_mut(7) { *f = -0.6; } // Williams

        let value = calculate_value_from_features(&features);
        assert!(value.is_finite());

        // Liquidity weights: Volume ratio 30%, Volume MA 25%, OBV 25%, MFI 20% = 100%
        if let Some(f) = features.get_mut(3) { *f = 0.5; } // Volume ratio
        if let Some(f) = features.get_mut(4) { *f = 0.6; } // Volume MA
        if let Some(f) = features.get_mut(10) { *f = 0.4; } // OBV
        if let Some(f) = features.get_mut(11) { *f = 0.7; } // MFI

        let liquidity = calculate_liquidity_from_features(&features);
        assert!(liquidity.is_finite());
    }
}
