//! Price-Based Features for Wave C Feature Engineering
//!
//! This module implements 15 advanced price-based features for HFT ML models:
//! - Returns (log, simple, volatility-adjusted)
//! - Volatility estimators (Parkinson, Garman-Klass, Yang-Zhang)
//! - Momentum (velocity, acceleration)
//! - Range metrics (HL spread, normalized range)
//! - Statistical features (skewness, kurtosis, percentile)
//! - Fractal analysis (Hurst exponent, fractal dimension)
//!
//! ## Performance Target
//! - <200μs for all 15 features per bar
//! - Uses SIMD optimizations where applicable
//!
//! ## Feature Index Allocation
//! - Features 27-41: Price-based features (15 total)
//! - Extends existing 26-feature system (0-25 used, see `WAVE_19_FEATURE_INDEX_MAP.md`)

use std::collections::VecDeque;

pub use crate::OHLCVBar;

/// Price-based feature extractor for all 15 features
#[derive(Debug)]
pub struct PriceFeatureExtractor;

impl PriceFeatureExtractor {
    /// Create new extractor (stateless, so just returns unit struct)
    pub const fn new() -> Self {
        Self
    }

    /// Extract all 15 price-based features from rolling window
    ///
    /// ## Arguments
    /// - `bars`: Rolling window of OHLCV bars (minimum 50 for statistical features)
    ///
    /// ## Returns
    /// - `[f64; 15]`: Array of 15 price features
    ///
    /// ## Feature Breakdown
    /// - `[0-2]`: Returns (simple, log, volatility-adjusted)
    /// - `[3-5]`: Volatility (Parkinson, Garman-Klass, Yang-Zhang)
    /// - `[6-7]`: Momentum (velocity, acceleration)
    /// - `[8-9]`: Range (HL spread, normalized range)
    /// - `[10-12]`: Statistical (skewness, kurtosis, quantile)
    /// - `[13-14]`: Fractal (Hurst exponent, fractal dimension)
    pub fn extract_all(bars: &VecDeque<OHLCVBar>) -> [f64; 15] {
        if bars.len() < 2 {
            return [0.0; 15];
        }

        let mut features = [0.0; 15];
        let Some(bar) = bars.back() else {
            return features;
        };

        // Returns (3 features)
        features[0] = Self::compute_simple_return(bars);
        features[1] = Self::compute_log_return(bars);
        features[2] = Self::compute_volatility_adjusted_return(bars);

        // Volatility (3 features)
        features[3] = Self::compute_parkinson_volatility(bar);
        features[4] = Self::compute_garman_klass_volatility(bar);
        features[5] = Self::compute_yang_zhang_volatility(bars);

        // Momentum (2 features)
        features[6] = Self::compute_price_velocity(bars, 5);
        features[7] = Self::compute_price_acceleration(bars);

        // Range (2 features)
        features[8] = Self::compute_hl_spread(bar);
        features[9] = Self::compute_normalized_range(bar);

        // Statistical (3 features)
        features[10] = Self::compute_rolling_skewness(bars, 20);
        features[11] = Self::compute_rolling_kurtosis(bars, 20);
        features[12] = Self::compute_quantile_position(bars, 20);

        // Fractal (2 features)
        features[13] = Self::compute_hurst_exponent(bars, 20);
        features[14] = Self::compute_fractal_dimension(bars, 20);

        features
    }

    /// 1. Simple return: (close - `prev_close`) / `prev_close`
    pub fn compute_simple_return(bars: &VecDeque<OHLCVBar>) -> f64 {
        if bars.len() < 2 {
            return 0.0;
        }
        let curr = match bars.back() {
            Some(b) => b.close,
            None => return 0.0,
        };
        let prev = bars[bars.len() - 2].close;
        safe_clip((curr - prev) / (prev + 1e-8), -0.5, 0.5)
    }

    /// 2. Log return: ln(close / `prev_close`)
    pub fn compute_log_return(bars: &VecDeque<OHLCVBar>) -> f64 {
        if bars.len() < 2 {
            return 0.0;
        }
        let curr = match bars.back() {
            Some(b) => b.close,
            None => return 0.0,
        };
        let prev = bars[bars.len() - 2].close;
        safe_log_return(curr, prev)
    }

    /// 3. Volatility-adjusted return: `simple_return` / volatility
    pub fn compute_volatility_adjusted_return(bars: &VecDeque<OHLCVBar>) -> f64 {
        if bars.len() < 20 {
            return 0.0;
        }
        let simple_ret = Self::compute_simple_return(bars);
        let volatility = Self::compute_rolling_std(bars, 20);
        if volatility < 1e-8 {
            return 0.0;
        }
        safe_clip(simple_ret / volatility, -3.0, 3.0)
    }

    /// 4. Parkinson volatility: sqrt((ln(high/low))^2 / (4*ln(2)))
    pub fn compute_parkinson_volatility(bar: &OHLCVBar) -> f64 {
        if bar.high <= bar.low || bar.high <= 0.0 || bar.low <= 0.0 {
            return 0.0;
        }
        let hl_ratio = bar.high / bar.low;
        let ln_ratio = hl_ratio.ln();
        let parkinson = (ln_ratio.powi(2) / (4.0 * 2_f64.ln())).sqrt();
        safe_clip(parkinson, 0.0, 0.5)
    }

    /// 5. Garman-Klass volatility: 0.5*(ln(H/L))^2 - (2*ln(2)-1)*(ln(C/O))^2
    pub fn compute_garman_klass_volatility(bar: &OHLCVBar) -> f64 {
        if bar.high <= 0.0 || bar.low <= 0.0 || bar.close <= 0.0 || bar.open <= 0.0 {
            return 0.0;
        }
        if bar.high <= bar.low {
            return 0.0;
        }

        let hl_term = 0.5 * (bar.high / bar.low).ln().powi(2);
        let co_term = (2.0 * 2_f64.ln() - 1.0) * (bar.close / bar.open).ln().powi(2);
        let gk = (hl_term - co_term).sqrt();

        safe_clip(gk, 0.0, 0.5)
    }

    /// 6. Yang-Zhang volatility: Combined OHLC estimator (simplified)
    pub fn compute_yang_zhang_volatility(bars: &VecDeque<OHLCVBar>) -> f64 {
        if bars.len() < 2 {
            return 0.0;
        }

        let Some(bar) = bars.back() else {
            return 0.0;
        };
        let prev = &bars[bars.len() - 2];

        // Overnight volatility: ln(open_t / close_{t-1})
        let overnight = if prev.close > 0.0 && bar.open > 0.0 {
            (bar.open / prev.close).ln().powi(2)
        } else {
            0.0
        };

        // Intraday volatility: Garman-Klass component
        let intraday = Self::compute_garman_klass_volatility(bar).powi(2);

        // Combined Yang-Zhang estimator
        let yz = (overnight + 0.34 * intraday).sqrt();
        safe_clip(yz, 0.0, 0.5)
    }

    /// 7. Price velocity: (close - close`[n_periods_ago]`) / `n_periods`
    pub fn compute_price_velocity(bars: &VecDeque<OHLCVBar>, period: usize) -> f64 {
        if bars.len() <= period {
            return 0.0;
        }
        let curr = match bars.back() {
            Some(b) => b.close,
            None => return 0.0,
        };
        let prev = bars[bars.len() - period - 1].close;
        safe_clip((curr - prev) / period as f64, -10.0, 10.0)
    }

    /// 8. Price acceleration: velocity - `prev_velocity`
    pub fn compute_price_acceleration(bars: &VecDeque<OHLCVBar>) -> f64 {
        if bars.len() < 3 {
            return 0.0;
        }
        let p0 = bars[bars.len() - 3].close;
        let p1 = bars[bars.len() - 2].close;
        let p2 = match bars.back() {
            Some(b) => b.close,
            None => return 0.0,
        };

        let vel1 = p2 - p1;
        let vel2 = p1 - p0;
        safe_clip(vel1 - vel2, -5.0, 5.0)
    }

    /// 9. High-Low spread: (high - low) / close
    pub fn compute_hl_spread(bar: &OHLCVBar) -> f64 {
        let range = bar.high - bar.low;
        safe_clip(range / (bar.close + 1e-8), 0.0, 0.1)
    }

    /// 10. Normalized range: (high - low) / (high + low)
    pub fn compute_normalized_range(bar: &OHLCVBar) -> f64 {
        let range = bar.high - bar.low;
        let sum = bar.high + bar.low;
        if sum < 1e-8 {
            return 0.0;
        }
        safe_clip(range / sum, 0.0, 1.0)
    }

    /// 11. Rolling skewness: Third moment of returns distribution
    pub fn compute_rolling_skewness(bars: &VecDeque<OHLCVBar>, period: usize) -> f64 {
        if bars.len() < period || period < 2 {
            return 0.0;
        }

        let start = bars.len().saturating_sub(period);
        let prices: Vec<f64> = bars.iter().skip(start).map(|b| b.close).collect();

        let mean = prices.iter().sum::<f64>() / prices.len() as f64;
        let std = Self::compute_std_from_prices(&prices, mean);

        if std < 1e-8 {
            return 0.0;
        }

        let skew: f64 = prices
            .iter()
            .map(|&p| ((p - mean) / std).powi(3))
            .sum::<f64>()
            / prices.len() as f64;

        safe_clip(skew, -3.0, 3.0)
    }

    /// 12. Rolling kurtosis: Fourth moment (excess kurtosis)
    pub fn compute_rolling_kurtosis(bars: &VecDeque<OHLCVBar>, period: usize) -> f64 {
        if bars.len() < period || period < 2 {
            return 0.0;
        }

        let start = bars.len().saturating_sub(period);
        let prices: Vec<f64> = bars.iter().skip(start).map(|b| b.close).collect();

        let mean = prices.iter().sum::<f64>() / prices.len() as f64;
        let std = Self::compute_std_from_prices(&prices, mean);

        if std < 1e-8 {
            return 0.0;
        }

        let kurt: f64 = prices
            .iter()
            .map(|&p| ((p - mean) / std).powi(4))
            .sum::<f64>()
            / prices.len() as f64;

        // Excess kurtosis (normal distribution = 0)
        safe_clip(kurt - 3.0, -3.0, 3.0)
    }

    /// 13. Quantile position: (close - min) / (max - min)
    pub fn compute_quantile_position(bars: &VecDeque<OHLCVBar>, period: usize) -> f64 {
        if bars.len() < period || period < 1 {
            return 0.5;
        }

        let start = bars.len().saturating_sub(period);
        let prices: Vec<f64> = bars.iter().skip(start).map(|b| b.close).collect();

        let min = prices.iter().copied().fold(f64::INFINITY, f64::min);
        let max = prices.iter().copied().fold(f64::NEG_INFINITY, f64::max);
        let current = match bars.back() {
            Some(b) => b.close,
            None => return 0.5,
        };

        if (max - min).abs() < 1e-8 {
            return 0.5;
        }

        safe_clip((current - min) / (max - min), 0.0, 1.0)
    }

    /// 14. Hurst exponent: R/S analysis for trend persistence
    pub fn compute_hurst_exponent(bars: &VecDeque<OHLCVBar>, period: usize) -> f64 {
        if bars.len() < period || period < 10 {
            return 0.5; // Random walk
        }

        let start = bars.len().saturating_sub(period);
        let prices: Vec<f64> = bars.iter().skip(start).map(|b| b.close).collect();

        // Calculate log returns
        let returns: Vec<f64> = prices
            .windows(2)
            .map(|w| safe_log_return(w[1], w[0]))
            .collect();

        if returns.is_empty() {
            return 0.5;
        }

        // Mean return
        let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;

        // Cumulative deviations
        let mut cumulative = vec![0.0];
        let mut sum = 0.0;
        for &ret in &returns {
            sum += ret - mean_return;
            cumulative.push(sum);
        }

        // Range
        let max_cum = cumulative.iter().copied().fold(f64::NEG_INFINITY, f64::max);
        let min_cum = cumulative.iter().copied().fold(f64::INFINITY, f64::min);
        let range = max_cum - min_cum;

        // Standard deviation
        let variance: f64 = returns
            .iter()
            .map(|&r| (r - mean_return).powi(2))
            .sum::<f64>()
            / returns.len() as f64;
        let std = variance.sqrt();

        if std < 1e-8 || range < 1e-8 {
            return 0.5;
        }

        // R/S statistic
        let rs = range / std;

        // Hurst exponent approximation: H ≈ log(R/S) / log(n) (safe version to prevent division by zero)
        let n = returns.len() as f64;
        let hurst = if n > 1.5 {
            rs.ln() / n.ln()
        } else {
            0.5 // Random walk assumption for small windows
        };

        safe_clip(hurst, 0.0, 1.0)
    }

    /// 15. Fractal dimension: 2 - Hurst
    pub fn compute_fractal_dimension(bars: &VecDeque<OHLCVBar>, period: usize) -> f64 {
        let hurst = Self::compute_hurst_exponent(bars, period);
        safe_clip(2.0 - hurst, 1.0, 2.0)
    }

    // Helper functions

    /// Compute rolling standard deviation
    fn compute_rolling_std(bars: &VecDeque<OHLCVBar>, period: usize) -> f64 {
        if bars.len() < period || period < 2 {
            return 0.0;
        }

        let start = bars.len().saturating_sub(period);
        let prices: Vec<f64> = bars.iter().skip(start).map(|b| b.close).collect();
        let mean = prices.iter().sum::<f64>() / prices.len() as f64;
        Self::compute_std_from_prices(&prices, mean)
    }

    /// Compute standard deviation from price array
    fn compute_std_from_prices(prices: &[f64], mean: f64) -> f64 {
        if prices.len() < 2 {
            return 0.0;
        }
        let variance: f64 =
            prices.iter().map(|&p| (p - mean).powi(2)).sum::<f64>() / prices.len() as f64;
        variance.sqrt()
    }
}

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

// Safe math utilities (matching extraction.rs patterns)

/// Safe log return: log(current / previous), handles edge cases
fn safe_log_return(current: f64, previous: f64) -> f64 {
    if previous <= 0.0 || current <= 0.0 {
        return 0.0;
    }
    let ratio = current / previous;
    if ratio <= 0.0 || !ratio.is_finite() {
        return 0.0;
    }
    safe_clip(ratio.ln(), -0.5, 0.5)
}

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

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

    // Test helper functions
    fn create_bars(prices: Vec<f64>) -> VecDeque<OHLCVBar> {
        prices
            .into_iter()
            .map(|p| OHLCVBar {
                timestamp: Utc::now(),
                open: p,
                high: p * 1.01,
                low: p * 0.99,
                close: p,
                volume: 1000.0,
            })
            .collect()
    }

    fn create_bars_constant(price: f64, count: usize) -> VecDeque<OHLCVBar> {
        (0..count)
            .map(|_| OHLCVBar {
                timestamp: Utc::now(),
                open: price,
                high: price,
                low: price,
                close: price,
                volume: 1000.0,
            })
            .collect()
    }

    fn create_linear_trend(start: f64, slope: f64, count: usize) -> VecDeque<OHLCVBar> {
        (0..count)
            .map(|i| {
                let price = start + slope * i as f64;
                OHLCVBar {
                    timestamp: Utc::now(),
                    open: price,
                    high: price * 1.01,
                    low: price * 0.99,
                    close: price,
                    volume: 1000.0,
                }
            })
            .collect()
    }

    fn create_oscillating_prices(center: f64, amplitude: f64, count: usize) -> VecDeque<OHLCVBar> {
        (0..count)
            .map(|i| {
                let price = center + amplitude * (i as f64 * 0.5).sin();
                OHLCVBar {
                    timestamp: Utc::now(),
                    open: price,
                    high: price * 1.01,
                    low: price * 0.99,
                    close: price,
                    volume: 1000.0,
                }
            })
            .collect()
    }

    fn assert_approx_eq(a: f64, b: f64, epsilon: f64) {
        assert!(
            (a - b).abs() < epsilon,
            "{} != {} (epsilon: {})",
            a,
            b,
            epsilon
        );
    }

    // Feature 1: Simple Return Tests
    #[test]
    fn test_simple_return_normal() {
        let bars = create_bars(vec![100.0, 110.0]);
        let ret = PriceFeatureExtractor::compute_simple_return(&bars);
        assert_approx_eq(ret, 0.1, 0.001); // 10% gain
    }

    #[test]
    fn test_simple_return_negative() {
        let bars = create_bars(vec![100.0, 90.0]);
        let ret = PriceFeatureExtractor::compute_simple_return(&bars);
        assert_approx_eq(ret, -0.1, 0.001); // 10% loss
    }

    #[test]
    fn test_simple_return_clipping() {
        let bars = create_bars(vec![100.0, 300.0]);
        let ret = PriceFeatureExtractor::compute_simple_return(&bars);
        assert_eq!(ret, 0.5); // Clipped to 50%
    }

    // Feature 2: Log Return Tests
    #[test]
    fn test_log_return_normal() {
        let bars = create_bars(vec![100.0, 110.0]);
        let ret = PriceFeatureExtractor::compute_log_return(&bars);
        assert_approx_eq(ret, 0.09531, 0.0001);
    }

    #[test]
    fn test_log_return_edge_cases() {
        let bars = create_bars(vec![100.0, 0.0]);
        assert_eq!(PriceFeatureExtractor::compute_log_return(&bars), 0.0);

        let bars = create_bars(vec![0.0, 100.0]);
        assert_eq!(PriceFeatureExtractor::compute_log_return(&bars), 0.0);
    }

    #[test]
    fn test_log_return_clipping() {
        let bars = create_bars(vec![100.0, 10000.0]);
        let ret = PriceFeatureExtractor::compute_log_return(&bars);
        assert!(ret >= -0.5 && ret <= 0.5);
    }

    // Feature 3: Volatility-Adjusted Return Tests
    #[test]
    fn test_volatility_adjusted_return() {
        let mut bars = create_bars_constant(100.0, 19);
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 105.0,
            high: 106.0,
            low: 104.0,
            close: 105.0,
            volume: 1000.0,
        });

        let ret = PriceFeatureExtractor::compute_volatility_adjusted_return(&bars);
        assert!(ret.is_finite());
    }

    #[test]
    fn test_volatility_adjusted_return_insufficient_data() {
        let bars = create_bars(vec![100.0, 110.0]);
        assert_eq!(
            PriceFeatureExtractor::compute_volatility_adjusted_return(&bars),
            0.0
        );
    }

    #[test]
    fn test_volatility_adjusted_return_zero_volatility() {
        let bars = create_bars_constant(100.0, 25);
        assert_eq!(
            PriceFeatureExtractor::compute_volatility_adjusted_return(&bars),
            0.0
        );
    }

    // Feature 4: Parkinson Volatility Tests
    #[test]
    fn test_parkinson_volatility() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 105.0,
            low: 95.0,
            close: 102.0,
            volume: 1000.0,
        };
        let vol = PriceFeatureExtractor::compute_parkinson_volatility(&bar);
        assert!(vol > 0.0 && vol <= 0.5);
    }

    #[test]
    fn test_parkinson_volatility_zero_range() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        };
        assert_eq!(
            PriceFeatureExtractor::compute_parkinson_volatility(&bar),
            0.0
        );
    }

    #[test]
    fn test_parkinson_volatility_invalid_prices() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: -100.0,
            high: -95.0,
            low: -105.0,
            close: -100.0,
            volume: 1000.0,
        };
        assert_eq!(
            PriceFeatureExtractor::compute_parkinson_volatility(&bar),
            0.0
        );
    }

    // Feature 5: Garman-Klass Volatility Tests
    #[test]
    fn test_garman_klass_volatility() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 98.0,
            high: 105.0,
            low: 95.0,
            close: 102.0,
            volume: 1000.0,
        };
        let vol = PriceFeatureExtractor::compute_garman_klass_volatility(&bar);
        assert!(vol >= 0.0 && vol <= 0.5);
    }

    #[test]
    fn test_garman_klass_volatility_zero_range() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        };
        assert_eq!(
            PriceFeatureExtractor::compute_garman_klass_volatility(&bar),
            0.0
        );
    }

    #[test]
    fn test_garman_klass_volatility_edge_cases() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 0.0,
            high: 100.0,
            low: 50.0,
            close: 75.0,
            volume: 1000.0,
        };
        assert_eq!(
            PriceFeatureExtractor::compute_garman_klass_volatility(&bar),
            0.0
        );
    }

    // Feature 6: Yang-Zhang Volatility Tests
    #[test]
    fn test_yang_zhang_volatility() {
        let mut bars = VecDeque::new();
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 98.0,
            high: 102.0,
            low: 96.0,
            close: 100.0,
            volume: 1000.0,
        });
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 101.0,
            high: 105.0,
            low: 99.0,
            close: 103.0,
            volume: 1000.0,
        });

        let vol = PriceFeatureExtractor::compute_yang_zhang_volatility(&bars);
        assert!(vol >= 0.0 && vol <= 0.5);
    }

    #[test]
    fn test_yang_zhang_volatility_insufficient_data() {
        let bars = create_bars(vec![100.0]);
        assert_eq!(
            PriceFeatureExtractor::compute_yang_zhang_volatility(&bars),
            0.0
        );
    }

    #[test]
    fn test_yang_zhang_volatility_stable() {
        let bars = create_bars_constant(100.0, 2);
        let vol = PriceFeatureExtractor::compute_yang_zhang_volatility(&bars);
        assert!(vol >= 0.0);
    }

    // Feature 7: Price Velocity Tests
    #[test]
    fn test_price_velocity_uptrend() {
        let bars = create_linear_trend(100.0, 0.5, 10);
        let vel = PriceFeatureExtractor::compute_price_velocity(&bars, 5);
        assert!(vel > 0.0);
    }

    #[test]
    fn test_price_velocity_downtrend() {
        let bars = create_linear_trend(100.0, -0.3, 10);
        let vel = PriceFeatureExtractor::compute_price_velocity(&bars, 5);
        assert!(vel < 0.0);
    }

    #[test]
    fn test_price_velocity_insufficient_data() {
        let bars = create_bars(vec![100.0, 101.0]);
        assert_eq!(PriceFeatureExtractor::compute_price_velocity(&bars, 5), 0.0);
    }

    // Feature 8: Price Acceleration Tests
    #[test]
    fn test_acceleration_uptrend() {
        let bars = create_bars(vec![100.0, 101.0, 103.0]);
        let accel = PriceFeatureExtractor::compute_price_acceleration(&bars);
        assert_eq!(accel, 1.0); // (103-101) - (101-100) = 1
    }

    #[test]
    fn test_acceleration_deceleration() {
        let bars = create_bars(vec![100.0, 103.0, 104.0]);
        let accel = PriceFeatureExtractor::compute_price_acceleration(&bars);
        assert!(accel < 0.0); // (104-103) - (103-100) = -2, clipped
    }

    #[test]
    fn test_acceleration_insufficient_data() {
        let bars = create_bars(vec![100.0, 101.0]);
        assert_eq!(
            PriceFeatureExtractor::compute_price_acceleration(&bars),
            0.0
        );
    }

    // Feature 9: HL Spread Tests
    #[test]
    fn test_hl_spread_normal() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 102.0,
            low: 98.0,
            close: 100.0,
            volume: 1000.0,
        };
        let spread = PriceFeatureExtractor::compute_hl_spread(&bar);
        assert_approx_eq(spread, 0.04, 0.001); // 4%
    }

    #[test]
    fn test_hl_spread_zero() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        };
        assert_eq!(PriceFeatureExtractor::compute_hl_spread(&bar), 0.0);
    }

    #[test]
    fn test_hl_spread_clipping() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 150.0,
            low: 50.0,
            close: 100.0,
            volume: 1000.0,
        };
        assert_eq!(PriceFeatureExtractor::compute_hl_spread(&bar), 0.1); // Clipped to 10%
    }

    // Feature 10: Normalized Range Tests
    #[test]
    fn test_normalized_range() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 110.0,
            low: 90.0,
            close: 100.0,
            volume: 1000.0,
        };
        let range = PriceFeatureExtractor::compute_normalized_range(&bar);
        assert_approx_eq(range, 0.1, 0.001); // 20 / 200 = 0.1
    }

    #[test]
    fn test_normalized_range_zero() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        };
        assert_eq!(PriceFeatureExtractor::compute_normalized_range(&bar), 0.0);
    }

    #[test]
    fn test_normalized_range_edge_case() {
        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 0.0,
            high: 0.0,
            low: 0.0,
            close: 0.0,
            volume: 1000.0,
        };
        assert_eq!(PriceFeatureExtractor::compute_normalized_range(&bar), 0.0);
    }

    // Feature 11: Rolling Skewness Tests
    #[test]
    fn test_skewness_symmetric() {
        let bars = create_bars_constant(100.0, 20);
        let skew = PriceFeatureExtractor::compute_rolling_skewness(&bars, 20);
        assert!(skew.abs() < 0.1); // Near-zero for constant prices
    }

    #[test]
    fn test_skewness_right_tail() {
        let mut bars = create_bars_constant(100.0, 19);
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 150.0,
            high: 151.0,
            low: 149.0,
            close: 150.0,
            volume: 1000.0,
        });
        let skew = PriceFeatureExtractor::compute_rolling_skewness(&bars, 20);
        assert!(skew > 0.0); // Positive skewness
    }

    #[test]
    fn test_skewness_insufficient_data() {
        let bars = create_bars(vec![100.0, 101.0, 102.0]);
        assert_eq!(
            PriceFeatureExtractor::compute_rolling_skewness(&bars, 20),
            0.0
        );
    }

    // Feature 12: Rolling Kurtosis Tests
    #[test]
    fn test_kurtosis_normal() {
        let bars = create_bars_constant(100.0, 25);
        let kurt = PriceFeatureExtractor::compute_rolling_kurtosis(&bars, 20);
        assert!(kurt.abs() < 0.1); // Near-zero excess kurtosis for constant
    }

    #[test]
    fn test_kurtosis_fat_tails() {
        let mut bars = create_bars_constant(100.0, 18);
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 150.0,
            high: 151.0,
            low: 149.0,
            close: 150.0,
            volume: 1000.0,
        });
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 50.0,
            high: 51.0,
            low: 49.0,
            close: 50.0,
            volume: 1000.0,
        });
        let kurt = PriceFeatureExtractor::compute_rolling_kurtosis(&bars, 20);
        assert!(kurt > 0.0); // Positive excess kurtosis
    }

    #[test]
    fn test_kurtosis_insufficient_data() {
        let bars = create_bars(vec![100.0, 101.0]);
        assert_eq!(
            PriceFeatureExtractor::compute_rolling_kurtosis(&bars, 20),
            0.0
        );
    }

    // Feature 13: Quantile Position Tests
    #[test]
    fn test_quantile_position_high() {
        let bars = create_linear_trend(90.0, 0.5, 21);
        let pos = PriceFeatureExtractor::compute_quantile_position(&bars, 20);
        assert!(pos > 0.95); // Near max
    }

    #[test]
    fn test_quantile_position_low() {
        let mut bars = create_bars_constant(100.0, 19);
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 90.0,
            high: 91.0,
            low: 89.0,
            close: 90.0,
            volume: 1000.0,
        });
        let pos = PriceFeatureExtractor::compute_quantile_position(&bars, 20);
        assert!(pos < 0.05); // Near min
    }

    #[test]
    fn test_quantile_position_constant() {
        let bars = create_bars_constant(100.0, 20);
        let pos = PriceFeatureExtractor::compute_quantile_position(&bars, 20);
        assert_approx_eq(pos, 0.5, 0.01); // Neutral
    }

    // Feature 14: Hurst Exponent Tests
    #[test]
    fn test_hurst_exponent_random_walk() {
        let bars = create_oscillating_prices(100.0, 2.0, 30);
        let hurst = PriceFeatureExtractor::compute_hurst_exponent(&bars, 20);
        assert!(hurst >= 0.0 && hurst <= 1.0);
    }

    #[test]
    fn test_hurst_exponent_trending() {
        let bars = create_linear_trend(100.0, 0.5, 30);
        let hurst = PriceFeatureExtractor::compute_hurst_exponent(&bars, 20);
        assert!(hurst >= 0.0 && hurst <= 1.0);
    }

    #[test]
    fn test_hurst_exponent_insufficient_data() {
        let bars = create_bars(vec![100.0, 101.0, 102.0]);
        assert_eq!(
            PriceFeatureExtractor::compute_hurst_exponent(&bars, 20),
            0.5
        );
    }

    // Feature 15: Fractal Dimension Tests
    #[test]
    fn test_fractal_dimension() {
        let bars = create_oscillating_prices(100.0, 2.0, 30);
        let fd = PriceFeatureExtractor::compute_fractal_dimension(&bars, 20);
        assert!(fd >= 1.0 && fd <= 2.0);
    }

    #[test]
    fn test_fractal_dimension_smooth() {
        let bars = create_linear_trend(100.0, 0.3, 30);
        let fd = PriceFeatureExtractor::compute_fractal_dimension(&bars, 20);
        assert!(fd >= 1.0 && fd <= 2.0);
    }

    #[test]
    fn test_fractal_dimension_insufficient_data() {
        let bars = create_bars(vec![100.0, 101.0]);
        let fd = PriceFeatureExtractor::compute_fractal_dimension(&bars, 20);
        assert_eq!(fd, 1.5); // 2.0 - 0.5 (default Hurst)
    }

    // Integration test: Extract all 15 features
    #[test]
    fn test_extract_all_features() {
        let bars = create_oscillating_prices(100.0, 5.0, 50);
        let features = PriceFeatureExtractor::extract_all(&bars);

        // Verify 15 features
        assert_eq!(features.len(), 15);

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

    #[test]
    fn test_extract_all_features_insufficient_data() {
        let bars = create_bars(vec![100.0]);
        let features = PriceFeatureExtractor::extract_all(&bars);

        // Should return all zeros
        for &val in &features {
            assert_eq!(val, 0.0);
        }
    }

    #[test]
    fn test_extract_all_features_realistic() {
        // Create realistic price movement
        let mut bars = VecDeque::new();
        for i in 0..60 {
            let price = 100.0 + (i as f64 * 0.1) + (i as f64 * 0.5).sin();
            bars.push_back(OHLCVBar {
                timestamp: Utc::now(),
                open: price - 0.5,
                high: price + 1.0,
                low: price - 1.0,
                close: price,
                volume: 1000.0 + (i as f64 * 10.0),
            });
        }

        let features = PriceFeatureExtractor::extract_all(&bars);

        // Validate non-zero values for price-driven features
        assert_ne!(features[0], 0.0); // Simple return
        assert_ne!(features[1], 0.0); // Log return
        assert!(features[3] > 0.0); // Parkinson volatility
        assert!(features[8] > 0.0); // HL spread
    }
}
