//! V10 Blocks AA + EE — Return-based realized volatility decomposition and
//! higher-moment features.
//!
//! ## Block AA — Realized variance decomposition (Andersen-Bollerslev-Diebold)
//!
//! - **Realized variance (RV)** = Σ r_i² — total quadratic variation in window
//! - **Realized semivariance down (RV-)** = Σ r_i² · I{r_i < 0} — downside-only RV
//! - **Realized semivariance up (RV+)** = Σ r_i² · I{r_i > 0} — upside-only RV
//! - **Realized jump component** = max(0, RV − BV) where
//!   `BV = (π/2) · Σ |r_i| · |r_{i-1}|` is the **bipower variation** (Barndorff-Nielsen
//!   & Shephard 2004). BV is robust to jumps because consecutive |r_i| · |r_{i-1}|
//!   stays bounded when only one r_i is a jump; RV explodes. The difference
//!   `RV − BV` isolates the **discontinuous (jump) component** of price variation.
//!
//! Citations: Andersen-Bollerslev-Diebold (2007), Barndorff-Nielsen & Shephard
//! (2004), Sarrafshirazi (2025 MSc, HF Bitcoin), Rehman (2024 RJEF on 452 firms).
//!
//! ## Block EE — Realized higher moments at multiple windows (Amaya 2015)
//!
//! - **Realized skewness** at short (~15 min) and long (~60 min) windows
//! - **Realized kurtosis** at short and long windows (excess kurtosis: gaussian = 0)
//!
//! From Amaya-Christoffersen-Jacobs-Vasquez (2015, *J. Financial Economics*):
//! low-skew + high-kurt sort yields ~43 bps/week on cross-sectional weekly
//! returns. The intraday analogue is a same-day moment z-score; for v10 we emit
//! raw skew + kurt at two window sizes so the model can choose timescale.
//!
//! Window sizes assume imbalance bars at ~8 sec/bar:
//!   - SHORT_WINDOW = 100 bars ≈ 13 min
//!   - LONG_WINDOW  = 400 bars ≈ 53 min

use std::collections::VecDeque;

/// Short window for moments (≈ 15 minutes at 8 sec/bar imbalance bars).
pub const SHORT_WINDOW: usize = 100;
/// Long window for moments (≈ 60 minutes at 8 sec/bar imbalance bars).
pub const LONG_WINDOW: usize = 400;

/// Streaming aggregator producing Block AA + EE features (8 total).
///
/// Maintains two independent rolling windows of returns:
/// - `short_returns`: SHORT_WINDOW size (~15 min)
/// - `long_returns`: LONG_WINDOW size (~60 min)
///
/// The realized variance decomposition (Block AA) is computed on the short
/// window only; skew/kurt (Block EE) are reported at both window sizes.
#[derive(Debug, Clone)]
pub struct ReturnMoments {
    short_returns: VecDeque<f64>,
    long_returns: VecDeque<f64>,
}

impl ReturnMoments {
    pub fn new() -> Self {
        Self {
            short_returns: VecDeque::with_capacity(SHORT_WINDOW),
            long_returns: VecDeque::with_capacity(LONG_WINDOW),
        }
    }

    /// Append a new bar-return (log-return preferred, but any centered return
    /// works). Pops oldest entry when window exceeds capacity.
    pub fn update(&mut self, ret: f64) {
        self.short_returns.push_back(ret);
        if self.short_returns.len() > SHORT_WINDOW {
            self.short_returns.pop_front();
        }
        self.long_returns.push_back(ret);
        if self.long_returns.len() > LONG_WINDOW {
            self.long_returns.pop_front();
        }
    }

    pub fn reset(&mut self) {
        self.short_returns.clear();
        self.long_returns.clear();
    }

    pub fn short_window_size(&self) -> usize {
        self.short_returns.len()
    }
    pub fn long_window_size(&self) -> usize {
        self.long_returns.len()
    }

    /// **Block AA (4 features)** — Realized variance decomposition from the
    /// short (~15 min) window. Returns `[RV, RV_down, RV_up, jump_component]`.
    ///
    /// All four are non-negative. With < 2 observations returns `[0; 4]`.
    pub fn realized_variance_decomposition(&self) -> [f64; 4] {
        let returns = &self.short_returns;
        if returns.len() < 2 {
            return [0.0; 4];
        }
        let mut rv = 0.0_f64;
        let mut rv_down = 0.0_f64;
        let mut rv_up = 0.0_f64;
        for &r in returns {
            let r2 = r * r;
            rv += r2;
            if r < 0.0 {
                rv_down += r2;
            } else if r > 0.0 {
                rv_up += r2;
            }
        }
        // Bipower variation: π/2 · Σ |r_i| · |r_{i-1}|
        // Robust to jumps (single-bar jumps don't multiply with the next bar).
        // Convergence: under no jumps, BV → integrated variance (same limit as RV).
        let mut bv = 0.0_f64;
        for i in 1..returns.len() {
            bv += returns[i - 1].abs() * returns[i].abs();
        }
        bv *= std::f64::consts::FRAC_PI_2;
        // Jump component: positive part of (RV − BV).
        // Negative differences are estimation noise; clamp to 0.
        let jump = (rv - bv).max(0.0);
        [rv, rv_down, rv_up, jump]
    }

    /// **Block EE (4 features)** — Realized higher moments at both windows.
    /// Returns `[skew_short, kurt_short, skew_long, kurt_long]`.
    ///
    /// Skewness uses the **central third-moment** normalization
    /// `m3 / m2^{3/2}` (Fisher-Pearson). Kurtosis is **excess kurtosis**
    /// `m4/m2² - 3` (Gaussian = 0). Variance-zero windows return 0.
    pub fn realized_moments(&self) -> [f64; 4] {
        [
            realized_skewness(&self.short_returns),
            realized_kurtosis(&self.short_returns),
            realized_skewness(&self.long_returns),
            realized_kurtosis(&self.long_returns),
        ]
    }

    /// All 8 features concatenated in the v10 emission order:
    /// `[RV, RV_down, RV_up, jump, skew_short, kurt_short, skew_long, kurt_long]`.
    pub fn all_features(&self) -> [f64; 8] {
        let [rv, rv_d, rv_u, jump] = self.realized_variance_decomposition();
        let [skew_s, kurt_s, skew_l, kurt_l] = self.realized_moments();
        [rv, rv_d, rv_u, jump, skew_s, kurt_s, skew_l, kurt_l]
    }
}

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

/// Standardized third central moment (Fisher-Pearson skewness).
/// Returns 0 for n < 3 or zero-variance windows.
fn realized_skewness(returns: &VecDeque<f64>) -> f64 {
    let n = returns.len();
    if n < 3 {
        return 0.0;
    }
    let n_f = n as f64;
    let mean: f64 = returns.iter().sum::<f64>() / n_f;
    let mut m2 = 0.0_f64;
    let mut m3 = 0.0_f64;
    for &r in returns {
        let d = r - mean;
        m2 += d * d;
        m3 += d * d * d;
    }
    m2 /= n_f;
    m3 /= n_f;
    if m2 < 1e-12 {
        return 0.0;
    }
    m3 / m2.powf(1.5)
}

/// Excess kurtosis (`m4 / m2² - 3` so a Gaussian distribution scores 0).
/// Returns 0 for n < 4 or zero-variance windows.
fn realized_kurtosis(returns: &VecDeque<f64>) -> f64 {
    let n = returns.len();
    if n < 4 {
        return 0.0;
    }
    let n_f = n as f64;
    let mean: f64 = returns.iter().sum::<f64>() / n_f;
    let mut m2 = 0.0_f64;
    let mut m4 = 0.0_f64;
    for &r in returns {
        let d = r - mean;
        m2 += d * d;
        m4 += d * d * d * d;
    }
    m2 /= n_f;
    m4 /= n_f;
    if m2 < 1e-12 {
        return 0.0;
    }
    m4 / (m2 * m2) - 3.0
}

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

    #[test]
    fn test_cold_start_returns_zeros() {
        let m = ReturnMoments::new();
        assert_eq!(m.realized_variance_decomposition(), [0.0; 4]);
        assert_eq!(m.realized_moments(), [0.0; 4]);
        assert_eq!(m.all_features(), [0.0; 8]);
    }

    #[test]
    fn test_rv_decomposition_pure_negative_returns() {
        // 10 negative returns of magnitude 0.01 → RV = RV_down = 10 × 0.0001 = 0.001
        let mut m = ReturnMoments::new();
        for _ in 0..10 {
            m.update(-0.01);
        }
        let [rv, rv_down, rv_up, _jump] = m.realized_variance_decomposition();
        assert!((rv - 0.001).abs() < 1e-9, "RV expected 0.001, got {rv}");
        assert!((rv_down - 0.001).abs() < 1e-9, "RV_down expected 0.001, got {rv_down}");
        assert_eq!(rv_up, 0.0, "RV_up should be exactly 0 with no positive returns");
        // No jumps in constant-magnitude series — BV ≈ RV → jump near 0
        // (Actually with constant |r|, BV = π/2 × 9 × 0.0001 ≈ 0.001414 > RV=0.001
        // so jump = max(0, RV - BV) = 0)
    }

    #[test]
    fn test_rv_decomposition_split_when_returns_balanced() {
        // 5 positive, 5 negative — symmetric magnitudes → RV_up = RV_down
        let mut m = ReturnMoments::new();
        for _ in 0..5 {
            m.update(0.01);
        }
        for _ in 0..5 {
            m.update(-0.01);
        }
        let [rv, rv_down, rv_up, _] = m.realized_variance_decomposition();
        assert!((rv - 0.001).abs() < 1e-9, "RV expected 0.001, got {rv}");
        assert!((rv_down - 0.0005).abs() < 1e-9, "RV_down expected 0.0005, got {rv_down}");
        assert!((rv_up - 0.0005).abs() < 1e-9, "RV_up expected 0.0005, got {rv_up}");
        assert!((rv_down + rv_up - rv).abs() < 1e-9, "RV_down + RV_up should equal RV");
    }

    #[test]
    fn test_jump_component_detects_single_large_jump_in_quiet_series() {
        // 99 small returns + 1 large jump — jump should be detectably > 0
        // because BV multiplies adjacent |r|; the large r contributes to only
        // 2 BV terms whereas RV gets r² in full.
        let mut m = ReturnMoments::new();
        for _ in 0..99 {
            m.update(0.001); // small returns
        }
        m.update(0.1); // 100× larger jump
        let [rv, _, _, jump] = m.realized_variance_decomposition();
        assert!(rv > 0.01, "RV should reflect the 0.1 jump²=0.01 contribution");
        assert!(
            jump > 0.0,
            "single-bar jump in quiet series should yield positive jump component, got {jump}"
        );
        // Jump should be a substantial fraction of RV (jump dominates RV here)
        assert!(jump > 0.5 * rv, "jump component should dominate RV in this regime");
    }

    #[test]
    fn test_skewness_zero_for_symmetric_distribution() {
        // Perfectly symmetric returns around 0 → skewness = 0
        let mut m = ReturnMoments::new();
        for sign in [1.0_f64, -1.0_f64].iter().cycle().take(50) {
            m.update(*sign * 0.01);
        }
        let [skew_s, _, _, _] = m.realized_moments();
        assert!(skew_s.abs() < 1e-9, "symmetric returns → skew=0, got {skew_s}");
    }

    #[test]
    fn test_skewness_negative_for_left_tail() {
        // Returns mostly small-positive with one large-negative → left tail → skew < 0
        let mut m = ReturnMoments::new();
        for _ in 0..50 {
            m.update(0.001);
        }
        m.update(-0.1); // one large negative
        let [skew_s, _, _, _] = m.realized_moments();
        assert!(skew_s < -0.5, "left-tail outlier should produce negative skew, got {skew_s}");
    }

    #[test]
    fn test_excess_kurtosis_zero_for_uniform_distribution() {
        // 100 uniformly-spaced returns in [-0.01, 0.01] — uniform distribution
        // has excess kurtosis = -1.2 (not 0; tighter than Gaussian).
        // Test: any reasonable bounded variance distribution has finite excess kurt.
        let mut m = ReturnMoments::new();
        for i in 0..100 {
            let r = -0.01 + (i as f64) * 0.0002; // linspace [-0.01, 0.01] across 100 pts
            m.update(r);
        }
        let [_, kurt_s, _, _] = m.realized_moments();
        // Uniform distribution: excess kurt = 9/5 - 3 = -1.2
        assert!(
            (kurt_s - (-1.2)).abs() < 0.05,
            "uniform distribution should give excess kurt ≈ -1.2, got {kurt_s}"
        );
    }

    #[test]
    fn test_excess_kurtosis_positive_for_heavy_tail() {
        // Mostly small returns + heavy tails → leptokurtic → excess kurt > 0
        let mut m = ReturnMoments::new();
        for _ in 0..95 {
            m.update(0.0001);
        }
        for _ in 0..5 {
            m.update(0.05); // ~500× larger heavy-tail samples
        }
        let [_, kurt_s, _, _] = m.realized_moments();
        assert!(kurt_s > 3.0, "heavy-tailed sample should produce excess kurt > 3, got {kurt_s}");
    }

    #[test]
    fn test_long_window_independent_of_short_window() {
        // After 50 returns, short window has 50 (sub-cap), long window also 50.
        // Skew/kurt at both windows should match since the data is identical.
        let mut m = ReturnMoments::new();
        for i in 0..50 {
            m.update(((i as f64) * 0.1).sin() * 0.01);
        }
        let [skew_s, kurt_s, skew_l, kurt_l] = m.realized_moments();
        assert!((skew_s - skew_l).abs() < 1e-9, "windows of equal size should match on skew");
        assert!((kurt_s - kurt_l).abs() < 1e-9, "windows of equal size should match on kurt");
    }

    #[test]
    fn test_short_window_rolls_when_long_does_not() {
        // After 200 returns: short fills + rolls (only last 100 retained), long
        // has all 200. Skew/kurt should DIFFER if data is non-stationary.
        let mut m = ReturnMoments::new();
        // First 100: positive drift
        for i in 0..100 {
            m.update(0.001 + ((i as f64) * 0.05).sin() * 0.0005);
        }
        // Next 100: negative drift
        for i in 100..200 {
            m.update(-0.001 + ((i as f64) * 0.05).sin() * 0.0005);
        }
        let [skew_s, _, skew_l, _] = m.realized_moments();
        // Short window sees only negative-drift half → distinct from full mix
        assert_ne!(skew_s, skew_l, "windows of different size + non-stationary data should differ");
    }
}
