//! Volume-Based Features for Wave C Feature Engineering
//!
//! This module implements 10 advanced volume features to complement the existing
//! 40 volume features in extraction.rs. These features capture volume dynamics,
//! price-volume relationships, and market participation patterns.
//!
//! ## Features Implemented (Indices 256-265)
//! 1. Volume Ratio to SMA-50 (256)
//! 2. Volume ROC 5-period (257)
//! 3. Volume ROC 10-period (258)
//! 4. Volume Acceleration (259)
//! 5. Volume Trend Slope (260)
//! 6. VWAP Intraday Deviation (261)
//! 7. Volume-Price Correlation (262)
//! 8. Volume Percentile 10-period (263)
//! 9. Volume Concentration HHI (264)
//! 10. Volume Imbalance Buy/Sell (265)
//!
//! ## Performance Target
//! - Latency: <150μs for all 10 features per bar
//! - Memory: <100 bytes per bar (reuses existing `VecDeque`)
//!
//! ## Integration
//! These features extend the 256-dimension feature vector to 266 dimensions.
//!
//! ## References
//! - `WAVE_C_VOLUME_FEATURES_DESIGN.md` (comprehensive design document)
//! - ml/src/features/extraction.rs (existing 40 volume features)

use anyhow::Result;
use std::collections::VecDeque;

pub use crate::OHLCVBar;

/// Volume feature extractor with stateful rolling windows (Wave G17 optimized)
///
/// Memory Optimization:
/// - OLD: `VecDeque<OHLCVBar>` with capacity 260 → ~32.5KB per symbol (always allocated)
/// - NEW: `Option<VecDeque<OHLCVBar>>` → 0 bytes until first bar (lazy allocated)
/// - Savings: ~32KB per symbol × 100K symbols (for unused symbols) = 3.2 GB
///
/// Note: Lazy allocation via Option reduces memory for unused symbols to zero.
#[derive(Debug)]
pub struct VolumeFeatureExtractor {
    /// Rolling window of bars (Wave G17: lazy allocation for 100% savings on unused symbols)
    bars: Option<VecDeque<OHLCVBar>>,
}

impl VolumeFeatureExtractor {
    /// Creates a new volume feature extractor (Wave G17: lazy allocation)
    pub const fn new() -> Self {
        Self { bars: None }
    }

    /// Updates the extractor with a new bar
    pub fn update(&mut self, bar: &OHLCVBar) {
        let buffer = self
            .bars
            .get_or_insert_with(|| VecDeque::with_capacity(260));
        buffer.push_back(*bar);
        if buffer.len() > 260 {
            buffer.pop_front();
        }
    }

    /// Helper: Get bars reference
    fn bars(&self) -> &VecDeque<OHLCVBar> {
        static EMPTY: once_cell::sync::Lazy<VecDeque<OHLCVBar>> =
            once_cell::sync::Lazy::new(VecDeque::new);
        self.bars.as_ref().unwrap_or(&EMPTY)
    }

    /// Extracts all 10 volume features (indices 256-265)
    ///
    /// ## Returns
    /// - Array of 10 features: [256, 257, ..., 265]
    ///
    /// ## Performance
    /// - Target: <150μs per call
    /// - Complexity: O(1) amortized for most features, O(n) for correlation/HHI
    pub fn extract_features(&self) -> Result<[f64; 10]> {
        let mut features = [0.0; 10];

        // Feature 256: Volume ratio to SMA-50
        features[0] = self.compute_volume_ratio_sma50();

        // Feature 257: Volume ROC 5-period
        features[1] = self.compute_volume_roc(5);

        // Feature 258: Volume ROC 10-period
        features[2] = self.compute_volume_roc(10);

        // Feature 259: Volume acceleration
        features[3] = self.compute_volume_acceleration();

        // Feature 260: Volume trend slope (20-period linear regression)
        features[4] = self.compute_volume_trend_slope(20);

        // Feature 261: VWAP intraday deviation
        features[5] = self.compute_vwap_deviation();

        // Feature 262: Volume-price correlation (20-period)
        features[6] = self.compute_volume_price_correlation(20);

        // Feature 263: Volume percentile (10-period)
        features[7] = self.compute_volume_percentile(10);

        // Feature 264: Volume concentration HHI (20-period)
        features[8] = self.compute_volume_concentration_hhi(20);

        // Feature 265: Volume imbalance (5-period buy/sell)
        features[9] = self.compute_volume_imbalance(5);

        // Validate no NaN/Inf
        for (i, &val) in features.iter().enumerate() {
            if !val.is_finite() {
                anyhow::bail!("Invalid volume feature at index {}: {}", i + 256, val);
            }
        }

        Ok(features)
    }

    // ===== Feature Implementation Methods =====

    /// Feature 256: Volume ratio to SMA-50
    ///
    /// Formula: (`current_volume` - `sma_50`) / `sma_50`
    /// Range: [-2.0, 5.0]
    fn compute_volume_ratio_sma50(&self) -> f64 {
        if self.bars().len() < 50 {
            return 0.0;
        }

        let Some(bar) = self.bars().back() else {
            return 0.0;
        };
        let sma_50 = self.compute_volume_sma(50);

        let ratio = (bar.volume - sma_50) / (sma_50 + 1e-8);
        safe_clip(ratio, -2.0, 5.0)
    }

    /// Feature 257/258: Volume ROC (Rate of Change)
    ///
    /// Formula: (`current_volume` - `volume_n_bars_ago`) / `volume_n_bars_ago`
    /// Range: [-1.0, 3.0]
    fn compute_volume_roc(&self, period: usize) -> f64 {
        let len = self.bars().len();
        if len <= period {
            return 0.0;
        }

        let curr_vol = match self.bars().back() {
            Some(b) => b.volume,
            None => return 0.0,
        };
        let prev_vol = self.bars()[len - period - 1].volume;

        let roc = (curr_vol - prev_vol) / (prev_vol + 1e-8);
        safe_clip(roc, -1.0, 3.0)
    }

    /// Feature 259: Volume acceleration (second derivative)
    ///
    /// Formula: (`velocity_1` - `velocity_2`) / 1000
    /// Range: [-5.0, 5.0]
    fn compute_volume_acceleration(&self) -> f64 {
        if self.bars().len() < 3 {
            return 0.0;
        }

        let curr = match self.bars().back() {
            Some(b) => b.volume,
            None => return 0.0,
        };
        let prev1 = self.bars()[self.bars().len() - 2].volume;
        let prev2 = self.bars()[self.bars().len() - 3].volume;

        let vel1 = curr - prev1;
        let vel2 = prev1 - prev2;
        let accel = vel1 - vel2;

        safe_clip(accel / 1000.0, -5.0, 5.0)
    }

    /// Feature 260: Volume trend slope (linear regression)
    ///
    /// Formula: Linear regression slope over period
    /// Range: [-1.0, 1.0]
    fn compute_volume_trend_slope(&self, period: usize) -> f64 {
        if self.bars().len() < period {
            return 0.0;
        }

        let start = self.bars().len() - period;
        let n = period as f64;

        // Linear regression formula: slope = (n*Σxy - Σx*Σy) / (n*Σx² - (Σx)²)
        let sum_x = (n * (n - 1.0)) / 2.0;
        let sum_x2 = (n * (n - 1.0) * (2.0 * n - 1.0)) / 6.0;

        let mut sum_y = 0.0;
        let mut sum_xy = 0.0;

        for (i, bar) in self.bars().iter().skip(start).enumerate() {
            sum_y += bar.volume;
            sum_xy += i as f64 * bar.volume;
        }

        let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
        safe_clip(slope / 100.0, -1.0, 1.0)
    }

    /// Feature 261: VWAP intraday deviation
    ///
    /// Formula: (close - vwap) / close
    /// Range: [-0.1, 0.1]
    fn compute_vwap_deviation(&self) -> f64 {
        if self.bars().len() < 20 {
            return 0.0;
        }

        let Some(bar) = self.bars().back() else {
            return 0.0;
        };
        let vwap = self.compute_vwap(20);

        let deviation = (bar.close - vwap) / (bar.close + 1e-8);
        safe_clip(deviation, -0.1, 0.1)
    }

    /// Feature 262: Volume-price correlation (Pearson)
    ///
    /// Formula: Pearson correlation coefficient
    /// Range: [-1.0, 1.0]
    fn compute_volume_price_correlation(&self, period: usize) -> f64 {
        if self.bars().len() < period {
            return 0.0;
        }

        let start = self.bars().len() - period;

        let prices: Vec<f64> = self.bars().iter().skip(start).map(|b| b.close).collect();
        let volumes: Vec<f64> = self.bars().iter().skip(start).map(|b| b.volume).collect();

        self.compute_correlation(&prices, &volumes)
    }

    /// Feature 263: Volume percentile rank
    ///
    /// Formula: count(vol < `current_vol`) / period
    /// Range: [0.0, 1.0]
    fn compute_volume_percentile(&self, period: usize) -> f64 {
        if self.bars().len() < period {
            return 0.5; // Neutral
        }

        let current_vol = match self.bars().back() {
            Some(b) => b.volume,
            None => return 0.5,
        };
        let start = self.bars().len() - period;

        let count_below = self
            .bars()
            .iter()
            .skip(start)
            .filter(|b| b.volume < current_vol)
            .count();

        count_below as f64 / period as f64
    }

    /// Feature 264: Volume concentration (Herfindahl-Hirschman Index)
    ///
    /// Formula: HHI = `Σ(vol_i` / `total_vol)²`
    /// Range: [0.0, 1.0] (normalized from [1/n, 1])
    fn compute_volume_concentration_hhi(&self, period: usize) -> f64 {
        if self.bars().len() < period {
            return 0.5; // Neutral
        }

        let start = self.bars().len() - period;
        let total_vol: f64 = self.bars().iter().skip(start).map(|b| b.volume).sum();

        if total_vol < 1e-8 {
            return 0.5; // Neutral for zero volume
        }

        let hhi: f64 = self
            .bars()
            .iter()
            .skip(start)
            .map(|b| {
                let share = b.volume / total_vol;
                share * share
            })
            .sum();

        // Normalize: HHI ∈ [1/n, 1] → [0, 1]
        let min_hhi = 1.0 / period as f64;
        let normalized = (hhi - min_hhi) / (1.0 - min_hhi);

        safe_clip(normalized, 0.0, 1.0)
    }

    /// Feature 265: Volume imbalance (buy vs sell pressure)
    ///
    /// Formula: (`buy_vol` - `sell_vol`) / `total_vol`
    /// Range: [-1.0, 1.0]
    fn compute_volume_imbalance(&self, period: usize) -> f64 {
        if self.bars().len() < period {
            return 0.0;
        }

        let start = self.bars().len() - period;
        let mut buy_vol = 0.0;
        let mut sell_vol = 0.0;

        for bar in self.bars().iter().skip(start) {
            if bar.close > bar.open {
                buy_vol += bar.volume;
            } else if bar.close < bar.open {
                sell_vol += bar.volume;
            } else {
                // Doji bars (close == open) contribute to neither
            }
        }

        let total_vol = buy_vol + sell_vol + 1e-8;
        let imbalance = (buy_vol - sell_vol) / total_vol;

        safe_clip(imbalance, -1.0, 1.0)
    }

    // ===== Helper Methods (reuse extraction.rs patterns) =====

    fn compute_volume_sma(&self, period: usize) -> f64 {
        let start = self.bars().len().saturating_sub(period);
        let sum: f64 = self.bars().iter().skip(start).map(|b| b.volume).sum();
        sum / period as f64
    }

    fn compute_vwap(&self, period: usize) -> f64 {
        let start = self.bars().len().saturating_sub(period);
        let (weighted_sum, volume_sum): (f64, f64) = self
            .bars()
            .iter()
            .skip(start)
            .map(|b| (b.close * b.volume, b.volume))
            .fold((0.0, 0.0), |(ws, vs), (w, v)| (ws + w, vs + v));
        weighted_sum / (volume_sum + 1e-8)
    }

    fn compute_correlation(&self, x: &[f64], y: &[f64]) -> f64 {
        if x.len() != y.len() || x.is_empty() {
            return 0.0;
        }

        let n = x.len() as f64;
        let mean_x: f64 = x.iter().sum::<f64>() / n;
        let mean_y: f64 = y.iter().sum::<f64>() / n;

        let mut cov = 0.0;
        let mut var_x = 0.0;
        let mut var_y = 0.0;

        for i in 0..x.len() {
            let dx = x[i] - mean_x;
            let dy = y[i] - mean_y;
            cov += dx * dy;
            var_x += dx * dx;
            var_y += dy * dy;
        }

        let denom = (var_x * var_y).sqrt();
        if denom > 1e-8 {
            safe_clip(cov / denom, -1.0, 1.0)
        } else {
            0.0
        }
    }
}

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

// ===== Utility Functions =====

/// Safe clipping: Clip value to [min, max] range, handles NaN/Inf
const fn safe_clip(value: f64, min: f64, max: f64) -> f64 {
    if !value.is_finite() {
        return 0.0;
    }
    value.clamp(min, max)
}

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

    fn create_bars_with_volume(volumes: Vec<f64>) -> Vec<OHLCVBar> {
        volumes
            .iter()
            .enumerate()
            .map(|(i, &vol)| OHLCVBar {
                timestamp: Utc::now() + chrono::Duration::hours(i as i64),
                open: 100.0,
                high: 101.0,
                low: 99.0,
                close: 100.5,
                volume: vol,
            })
            .collect()
    }

    fn create_bars_with_price_volume(prices: Vec<f64>, volumes: Vec<f64>) -> Vec<OHLCVBar> {
        prices
            .iter()
            .zip(volumes.iter())
            .enumerate()
            .map(|(i, (&p, &v))| OHLCVBar {
                timestamp: Utc::now() + chrono::Duration::hours(i as i64),
                open: p,
                high: p + 1.0,
                low: p - 1.0,
                close: p,
                volume: v,
            })
            .collect()
    }

    fn create_bars_with_ohlc(ohlc: Vec<(f64, f64)>, volumes: Vec<f64>) -> Vec<OHLCVBar> {
        ohlc.iter()
            .zip(volumes.iter())
            .enumerate()
            .map(|(i, (&(o, c), &v))| OHLCVBar {
                timestamp: Utc::now() + chrono::Duration::hours(i as i64),
                open: o,
                high: o.max(c) + 1.0,
                low: o.min(c) - 1.0,
                close: c,
                volume: v,
            })
            .collect()
    }

    #[test]
    fn test_volume_ratio_normal() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1000.0; 51]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[0] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[0]
        );
    }

    #[test]
    fn test_volume_ratio_2x_spike() {
        let mut extractor = VolumeFeatureExtractor::new();
        let mut volumes = vec![1000.0; 50];
        volumes.push(2000.0);
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        // SMA-50 = (49*1000 + 2000) / 50 = 1020
        // Ratio = (2000 - 1020) / 1020 = 0.96
        assert!(
            (features[0] - 0.96).abs() < 0.02,
            "Expected 0.96, got {}",
            features[0]
        );
    }

    #[test]
    fn test_volume_ratio_extreme_clipping() {
        let mut extractor = VolumeFeatureExtractor::new();
        let mut volumes = vec![1000.0; 50];
        volumes.push(10000.0);
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[0] - 5.0).abs() < 0.01,
            "Expected 5.0 (clipped), got {}",
            features[0]
        );
    }

    #[test]
    fn test_volume_roc_5_flat() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1000.0; 10]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[1] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[1]
        );
    }

    #[test]
    fn test_volume_roc_5_doubling() {
        let mut extractor = VolumeFeatureExtractor::new();
        let volumes = vec![1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 2000.0];
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[1] - 1.0).abs() < 0.01,
            "Expected 1.0, got {}",
            features[1]
        );
    }

    #[test]
    fn test_volume_acceleration_constant() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1000.0, 1100.0, 1200.0]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[3] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[3]
        );
    }

    #[test]
    fn test_volume_acceleration_positive() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1000.0, 1100.0, 1300.0]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            features[3] > 0.0,
            "Expected positive acceleration, got {}",
            features[3]
        );
    }

    #[test]
    fn test_volume_trend_flat() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1000.0; 25]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[4] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[4]
        );
    }

    #[test]
    fn test_volume_trend_uptrend() {
        let mut extractor = VolumeFeatureExtractor::new();
        let volumes: Vec<f64> = (1000..1025).map(|x| x as f64 * 100.0).collect();
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            features[4] > 0.0,
            "Expected positive slope, got {}",
            features[4]
        );
    }

    #[test]
    fn test_vwap_at_fair_value() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_price_volume(vec![100.0; 25], vec![1000.0; 25]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[5] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[5]
        );
    }

    #[test]
    fn test_volume_price_correlation_positive() {
        let mut extractor = VolumeFeatureExtractor::new();
        let prices: Vec<f64> = (100..120).map(|x| x as f64).collect();
        let volumes: Vec<f64> = (1000..1020).map(|x| x as f64 * 100.0).collect();
        let bars = create_bars_with_price_volume(prices, volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            features[6] > 0.5,
            "Expected strong positive correlation, got {}",
            features[6]
        );
    }

    #[test]
    fn test_volume_price_correlation_negative() {
        let mut extractor = VolumeFeatureExtractor::new();
        let prices: Vec<f64> = (100..120).rev().map(|x| x as f64).collect();
        let volumes: Vec<f64> = (1000..1020).map(|x| x as f64 * 100.0).collect();
        let bars = create_bars_with_price_volume(prices, volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            features[6] < -0.5,
            "Expected strong negative correlation, got {}",
            features[6]
        );
    }

    #[test]
    fn test_volume_percentile_minimum() {
        let mut extractor = VolumeFeatureExtractor::new();
        let mut volumes = vec![1000.0; 10];
        volumes[9] = 500.0;
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[7] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[7]
        );
    }

    #[test]
    fn test_volume_percentile_maximum() {
        let mut extractor = VolumeFeatureExtractor::new();
        let mut volumes = vec![1000.0; 10];
        volumes[9] = 2000.0;
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[7] - 1.0).abs() < 0.11,
            "Expected 1.0, got {}",
            features[7]
        );
    }

    #[test]
    fn test_volume_concentration_uniform() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1000.0; 25]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[8] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[8]
        );
    }

    #[test]
    fn test_volume_concentration_high() {
        let mut extractor = VolumeFeatureExtractor::new();
        // Create more extreme concentration: 19 very small + 1 dominant volume
        let mut volumes = vec![10.0; 19];
        volumes.push(9900.0);
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        // Total = 19*10 + 9900 = 10090
        // HHI = 19*(10/10090)² + (9900/10090)² ≈ 0.000019 + 0.963 = 0.963
        // min_hhi = 1/20 = 0.05
        // normalized = (0.963 - 0.05) / (1 - 0.05) = 0.96
        assert!(
            features[8] > 0.9,
            "Expected high HHI (>0.9), got {}",
            features[8]
        );
    }

    #[test]
    fn test_volume_imbalance_balanced() {
        let mut extractor = VolumeFeatureExtractor::new();
        let ohlc = vec![(100.0, 100.0); 5]; // Doji bars
        let bars = create_bars_with_ohlc(ohlc, vec![1000.0; 5]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[9] - 0.0).abs() < 0.01,
            "Expected 0.0, got {}",
            features[9]
        );
    }

    #[test]
    fn test_volume_imbalance_buying() {
        let mut extractor = VolumeFeatureExtractor::new();
        let ohlc = vec![(100.0, 110.0); 5]; // All bullish bars
        let bars = create_bars_with_ohlc(ohlc, vec![1000.0; 5]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[9] - 1.0).abs() < 0.01,
            "Expected 1.0, got {}",
            features[9]
        );
    }

    #[test]
    fn test_volume_imbalance_selling() {
        let mut extractor = VolumeFeatureExtractor::new();
        let ohlc = vec![(110.0, 100.0); 5]; // All bearish bars
        let bars = create_bars_with_ohlc(ohlc, vec![1000.0; 5]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();
        assert!(
            (features[9] - -1.0).abs() < 0.01,
            "Expected -1.0, got {}",
            features[9]
        );
    }

    #[test]
    fn test_insufficient_history_returns_default() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1000.0; 3]);

        for bar in &bars {
            extractor.update(bar);
        }

        // Should succeed but return mostly 0.0 values
        let features = extractor.extract_features().unwrap();

        // Most features should be 0.0 or neutral (0.5 for percentile/HHI)
        assert!((features[0] - 0.0).abs() < 0.01); // Volume ratio (insufficient)
        assert!((features[7] - 0.5).abs() < 0.01); // Percentile (neutral)
        assert!((features[8] - 0.5).abs() < 0.01); // HHI (neutral)
    }

    #[test]
    fn test_zero_volume_handling() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![0.0; 55]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();

        // All values should be finite (no NaN/Inf)
        for (i, &val) in features.iter().enumerate() {
            assert!(
                val.is_finite(),
                "Found non-finite value at index {}: {}",
                i,
                val
            );
        }
    }

    #[test]
    fn test_extreme_volume_clipping() {
        let mut extractor = VolumeFeatureExtractor::new();
        let bars = create_bars_with_volume(vec![1_000_000.0; 55]);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();

        // All values should be within expected ranges
        assert!(features[0] >= -2.0 && features[0] <= 5.0); // Volume ratio
        assert!(features[1] >= -1.0 && features[1] <= 3.0); // ROC 5
        assert!(features[2] >= -1.0 && features[2] <= 3.0); // ROC 10
        assert!(features[3] >= -5.0 && features[3] <= 5.0); // Acceleration
        assert!(features[4] >= -1.0 && features[4] <= 1.0); // Trend slope
        assert!(features[5] >= -0.1 && features[5] <= 0.1); // VWAP deviation
        assert!(features[6] >= -1.0 && features[6] <= 1.0); // Correlation
        assert!(features[7] >= 0.0 && features[7] <= 1.0); // Percentile
        assert!(features[8] >= 0.0 && features[8] <= 1.0); // HHI
        assert!(features[9] >= -1.0 && features[9] <= 1.0); // Imbalance
    }

    #[test]
    fn test_all_features_finite() {
        let mut extractor = VolumeFeatureExtractor::new();

        // Create diverse bars with varying volumes
        let volumes = vec![
            1000.0, 1200.0, 800.0, 1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0,
            1200.0, 800.0, 1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0,
            800.0, 1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0, 800.0,
            1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0, 800.0, 1500.0,
            900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0, 800.0, 1500.0, 900.0,
        ];
        let bars = create_bars_with_volume(volumes);

        for bar in &bars {
            extractor.update(bar);
        }

        let features = extractor.extract_features().unwrap();

        // Validate all features are finite
        for (i, &val) in features.iter().enumerate() {
            assert!(
                val.is_finite(),
                "Feature {} is not finite: {}",
                i + 256,
                val
            );
        }
    }
}
