//! Advanced quantitative analytics for prediction market trading.
//!
//! Provides ~25 pure-Rust functions and 4 streaming detectors:
//! - Information theory: Shannon entropy, joint entropy, KL divergence, mutual information, transfer entropy
//! - Microstructure: Kyle's lambda, Amihud ratio, Roll spread, effective/realized spread, LOB imbalance, weighted mid
//! - Risk analytics: Cornish-Fisher VaR/CVaR, prediction Greeks
//! - Signal analysis: information coefficient (Spearman), signal half-life, Hurst exponent, variance ratio
//! - Statistical testing: deflated Sharpe ratio, Bonferroni, Benjamini-Hochberg FDR
//! - Streaming detectors: VPIN, CUSUM change-point, OFI tracker

use pyo3::prelude::*;
use std::collections::VecDeque;
use std::sync::Mutex;

// ===========================================================================
// Private math helpers
// ===========================================================================

/// Standard normal PDF.
#[inline]
fn normal_pdf(x: f64) -> f64 {
    const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7; // 1/sqrt(2*pi)
    INV_SQRT_2PI * (-0.5 * x * x).exp()
}

/// Standard normal CDF (Abramowitz & Stegun approximation).
#[inline]
fn normal_cdf(x: f64) -> f64 {
    const A1: f64 = 0.254829592;
    const A2: f64 = -0.284496736;
    const A3: f64 = 1.421413741;
    const A4: f64 = -1.453152027;
    const A5: f64 = 1.061405429;
    const P: f64 = 0.3275911;

    let sign = if x < 0.0 { -1.0 } else { 1.0 };
    let x_abs = x.abs() / std::f64::consts::SQRT_2;
    let t = 1.0 / (1.0 + P * x_abs);
    let y =
        1.0 - (((((A5 * t + A4) * t) + A3) * t + A2) * t + A1) * t * (-x_abs * x_abs).exp();
    0.5 * (1.0 + sign * y)
}

/// Inverse standard normal CDF (Peter Acklam's rational approximation).
/// Accurate to ~1.15e-9 across the full range.
#[inline]
fn normal_cdf_inv(p: f64) -> f64 {
    let p = p.clamp(1e-15, 1.0 - 1e-15);

    // Coefficients for rational approximation
    const A: [f64; 6] = [
        -3.969683028665376e+01,
         2.209460984245205e+02,
        -2.759285104469687e+02,
         1.383577518672690e+02,
        -3.066479806614716e+01,
         2.506628277459239e+00,
    ];
    const B: [f64; 5] = [
        -5.447609879822406e+01,
         1.615858368580409e+02,
        -1.556989798598866e+02,
         6.680131188771972e+01,
        -1.328068155288572e+01,
    ];
    const C: [f64; 6] = [
        -7.784894002430293e-03,
        -3.223964580411365e-01,
        -2.400758277161838e+00,
        -2.549732539343734e+00,
         4.374664141464968e+00,
         2.938163982698783e+00,
    ];
    const D: [f64; 4] = [
         7.784695709041462e-03,
         3.224671290700398e-01,
         2.445134137142996e+00,
         3.754408661907416e+00,
    ];

    const P_LOW: f64 = 0.02425;
    const P_HIGH: f64 = 1.0 - P_LOW;

    if p < P_LOW {
        // Rational approximation for lower region
        let q = (-2.0 * p.ln()).sqrt();
        (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
    } else if p <= P_HIGH {
        // Rational approximation for central region
        let q = p - 0.5;
        let r = q * q;
        (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
            / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
    } else {
        // Rational approximation for upper region (symmetry)
        let q = (-2.0 * (1.0 - p).ln()).sqrt();
        -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
    }
}

/// Assign ranks to values (average rank for ties).
fn rank_vector(values: &[f64]) -> Vec<f64> {
    let n = values.len();
    let mut indices: Vec<usize> = (0..n).collect();
    indices.sort_by(|&a, &b| {
        values[a]
            .partial_cmp(&values[b])
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    let mut ranks = vec![0.0; n];
    let mut i = 0;
    while i < n {
        let mut j = i + 1;
        while j < n && (values[indices[j]] - values[indices[i]]).abs() < 1e-15 {
            j += 1;
        }
        let avg_rank = (i + j + 1) as f64 / 2.0; // 1-based average
        for &idx in &indices[i..j] {
            ranks[idx] = avg_rank;
        }
        i = j;
    }
    ranks
}

// ===========================================================================
// Information Theory
// ===========================================================================

/// Binary Shannon entropy: -[p*log2(p) + (1-p)*log2(1-p)].
/// Returns 0.0 for p outside (0,1).
#[pyfunction]
#[inline]
pub fn shannon_entropy(p: f64) -> f64 {
    if !p.is_finite() || p <= 0.0 || p >= 1.0 {
        return 0.0;
    }
    -(p * p.log2() + (1.0 - p) * (1.0 - p).log2())
}

/// Multi-outcome entropy: -sum(p_i * log2(p_i)).
/// Skips non-positive probabilities.
#[pyfunction]
pub fn joint_entropy(probs: Vec<f64>) -> f64 {
    let mut h = 0.0;
    for &p in &probs {
        if p.is_finite() && p > 0.0 {
            h -= p * p.log2();
        }
    }
    if h.is_finite() {
        h
    } else {
        0.0
    }
}

/// KL divergence KL(P||Q) = sum(p_i * log(p_i / q_i)).
/// Clamps q values to epsilon to avoid division by zero.
#[pyfunction]
pub fn kl_divergence(p: Vec<f64>, q: Vec<f64>) -> PyResult<f64> {
    if p.len() != q.len() {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "p and q must have the same length",
        ));
    }
    if p.is_empty() {
        return Ok(0.0);
    }
    let eps = 1e-15;
    let mut kl = 0.0;
    for (&pi, &qi) in p.iter().zip(q.iter()) {
        if pi > 0.0 && pi.is_finite() {
            let qi_c = qi.max(eps);
            kl += pi * (pi / qi_c).ln();
        }
    }
    Ok(if kl.is_finite() { kl } else { 0.0 })
}

/// Mutual information MI(X;Y) via 2D histogram: H(X) + H(Y) - H(X,Y).
#[pyfunction]
#[pyo3(signature = (x, y, n_bins=10))]
pub fn mutual_information(x: Vec<f64>, y: Vec<f64>, n_bins: usize) -> PyResult<f64> {
    if x.len() != y.len() {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "x and y must have the same length",
        ));
    }
    let n = x.len();
    if n < 2 {
        return Ok(0.0);
    }
    let n_bins = n_bins.max(2);

    // Compute ranges
    let (x_min, x_max) = min_max(&x);
    let (y_min, y_max) = min_max(&y);
    let x_range = (x_max - x_min).max(1e-15);
    let y_range = (y_max - y_min).max(1e-15);

    // Build 2D histogram
    let mut joint = vec![vec![0u32; n_bins]; n_bins];
    let mut mx = vec![0u32; n_bins];
    let mut my = vec![0u32; n_bins];

    for i in 0..n {
        let bx = (((x[i] - x_min) / x_range * n_bins as f64).floor() as usize).min(n_bins - 1);
        let by = (((y[i] - y_min) / y_range * n_bins as f64).floor() as usize).min(n_bins - 1);
        joint[bx][by] += 1;
        mx[bx] += 1;
        my[by] += 1;
    }

    let nf = n as f64;
    let mut mi = 0.0;
    for bx in 0..n_bins {
        for by in 0..n_bins {
            let c = joint[bx][by];
            if c > 0 {
                let pxy = c as f64 / nf;
                let px = mx[bx] as f64 / nf;
                let py = my[by] as f64 / nf;
                mi += pxy * (pxy / (px * py)).ln();
            }
        }
    }

    Ok(if mi.is_finite() { mi.max(0.0) } else { 0.0 })
}

/// Transfer entropy TE(X->Y) via 3D histogram.
/// Measures directed information flow from source to target with given lag.
#[pyfunction]
#[pyo3(signature = (source, target, lag=1, n_bins=10))]
pub fn transfer_entropy(
    source: Vec<f64>,
    target: Vec<f64>,
    lag: usize,
    n_bins: usize,
) -> PyResult<f64> {
    if source.len() != target.len() {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "source and target must have the same length",
        ));
    }
    let n = source.len();
    let lag = lag.max(1);
    if n <= lag + 1 {
        return Ok(0.0);
    }
    let n_bins = n_bins.max(2);
    let effective = n - lag;

    // Build series: y_{t}, y_{t-lag}, x_{t-lag}
    let yt: Vec<f64> = target[lag..].to_vec();
    let yt_lag: Vec<f64> = target[..effective].to_vec();
    let xt_lag: Vec<f64> = source[..effective].to_vec();

    let (yt_min, yt_max) = min_max(&yt);
    let (ytl_min, ytl_max) = min_max(&yt_lag);
    let (xtl_min, xtl_max) = min_max(&xt_lag);
    let yt_range = (yt_max - yt_min).max(1e-15);
    let ytl_range = (ytl_max - ytl_min).max(1e-15);
    let xtl_range = (xtl_max - xtl_min).max(1e-15);

    // 3D histogram: (yt_bin, ytlag_bin, xtlag_bin)
    let nb = n_bins;
    let mut hist_3d = vec![0u32; nb * nb * nb]; // flat 3D
    let mut hist_yt_ytl = vec![0u32; nb * nb];
    let mut hist_ytl_xtl = vec![0u32; nb * nb];
    let mut hist_ytl = vec![0u32; nb];

    for i in 0..effective {
        let by = (((yt[i] - yt_min) / yt_range * nb as f64).floor() as usize).min(nb - 1);
        let bytl = (((yt_lag[i] - ytl_min) / ytl_range * nb as f64).floor() as usize).min(nb - 1);
        let bxtl =
            (((xt_lag[i] - xtl_min) / xtl_range * nb as f64).floor() as usize).min(nb - 1);

        hist_3d[by * nb * nb + bytl * nb + bxtl] += 1;
        hist_yt_ytl[by * nb + bytl] += 1;
        hist_ytl_xtl[bytl * nb + bxtl] += 1;
        hist_ytl[bytl] += 1;
    }

    let ef = effective as f64;
    let mut te = 0.0;
    for by in 0..nb {
        for bytl in 0..nb {
            for bxtl in 0..nb {
                let c3 = hist_3d[by * nb * nb + bytl * nb + bxtl];
                if c3 == 0 {
                    continue;
                }
                let p_yt_ytl_xtl = c3 as f64 / ef;
                let p_yt_ytl = hist_yt_ytl[by * nb + bytl] as f64 / ef;
                let p_ytl_xtl = hist_ytl_xtl[bytl * nb + bxtl] as f64 / ef;
                let p_ytl = hist_ytl[bytl] as f64 / ef;

                if p_yt_ytl > 0.0 && p_ytl_xtl > 0.0 && p_ytl > 0.0 {
                    let ratio = (p_yt_ytl_xtl * p_ytl) / (p_yt_ytl * p_ytl_xtl);
                    if ratio > 0.0 {
                        te += p_yt_ytl_xtl * ratio.ln();
                    }
                }
            }
        }
    }

    Ok(if te.is_finite() { te.max(0.0) } else { 0.0 })
}

// ===========================================================================
// Microstructure
// ===========================================================================

/// Kyle's lambda: OLS price impact coefficient = cov(dp, sv) / var(sv).
#[pyfunction]
pub fn kyles_lambda(price_changes: Vec<f64>, signed_volumes: Vec<f64>) -> PyResult<f64> {
    if price_changes.len() != signed_volumes.len() {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "price_changes and signed_volumes must have the same length",
        ));
    }
    let n = price_changes.len();
    if n < 2 {
        return Ok(0.0);
    }
    let nf = n as f64;
    let mean_dp = price_changes.iter().sum::<f64>() / nf;
    let mean_sv = signed_volumes.iter().sum::<f64>() / nf;

    let mut cov = 0.0;
    let mut var_sv = 0.0;
    for i in 0..n {
        let d_dp = price_changes[i] - mean_dp;
        let d_sv = signed_volumes[i] - mean_sv;
        cov += d_dp * d_sv;
        var_sv += d_sv * d_sv;
    }

    if var_sv < 1e-15 {
        return Ok(0.0);
    }
    let lambda = cov / var_sv;
    Ok(if lambda.is_finite() { lambda } else { 0.0 })
}

/// Amihud illiquidity ratio: mean(|r_i| / v_i).
/// Skips zero-volume observations.
#[pyfunction]
pub fn amihud_ratio(returns: Vec<f64>, volumes: Vec<f64>) -> PyResult<f64> {
    if returns.len() != volumes.len() {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "returns and volumes must have the same length",
        ));
    }
    let mut sum = 0.0;
    let mut count = 0u64;
    for (&r, &v) in returns.iter().zip(volumes.iter()) {
        if v.is_finite() && v > 0.0 && r.is_finite() {
            sum += r.abs() / v;
            count += 1;
        }
    }
    if count == 0 {
        return Ok(0.0);
    }
    let ratio = sum / count as f64;
    Ok(if ratio.is_finite() { ratio } else { 0.0 })
}

/// Roll implicit spread: 2 * sqrt(-cov(r_t, r_{t-1})).
/// Returns 0 if autocovariance is non-negative.
#[pyfunction]
pub fn roll_spread(returns: Vec<f64>) -> f64 {
    let n = returns.len();
    if n < 2 {
        return 0.0;
    }
    let mean = returns.iter().sum::<f64>() / n as f64;
    let mut autocov = 0.0;
    for i in 1..n {
        autocov += (returns[i] - mean) * (returns[i - 1] - mean);
    }
    autocov /= (n - 1) as f64;

    if autocov < 0.0 {
        let spread = 2.0 * (-autocov).sqrt();
        if spread.is_finite() {
            spread
        } else {
            0.0
        }
    } else {
        0.0
    }
}

/// Effective spread: 2 * signed distance from mid.
#[pyfunction]
#[inline]
pub fn effective_spread(fill_price: f64, mid: f64, is_buy: bool) -> f64 {
    let sign = if is_buy { 1.0 } else { -1.0 };
    2.0 * sign * (fill_price - mid)
}

/// Realized spread: 2 * sign * (fill_price - mid_after).
/// Measures how much spread the liquidity provider actually captures.
#[pyfunction]
#[inline]
pub fn realized_spread(fill_price: f64, mid_after: f64, is_buy: bool) -> f64 {
    let sign = if is_buy { 1.0 } else { -1.0 };
    2.0 * sign * (fill_price - mid_after)
}

/// Multi-level limit order book imbalance: (sum_bid_qty - sum_ask_qty) / (sum_bid_qty + sum_ask_qty).
/// Uses up to `levels` price levels from each side.
#[pyfunction]
#[pyo3(signature = (bids, asks, levels=5))]
pub fn lob_imbalance(bids: Vec<(f64, f64)>, asks: Vec<(f64, f64)>, levels: usize) -> f64 {
    let levels = levels.max(1);
    let bid_qty: f64 = bids.iter().take(levels).map(|(_, q)| q).sum();
    let ask_qty: f64 = asks.iter().take(levels).map(|(_, q)| q).sum();
    let total = bid_qty + ask_qty;
    if total < 1e-15 {
        return 0.0;
    }
    let imb = (bid_qty - ask_qty) / total;
    if imb.is_finite() {
        imb
    } else {
        0.0
    }
}

/// Microprice: volume-weighted mid price using best bid/ask.
/// weighted_mid = (ask_qty * bid_price + bid_qty * ask_price) / (bid_qty + ask_qty)
#[pyfunction]
pub fn weighted_mid(bids: Vec<(f64, f64)>, asks: Vec<(f64, f64)>) -> f64 {
    if bids.is_empty() || asks.is_empty() {
        return 0.0;
    }
    let (bid_price, bid_qty) = bids[0];
    let (ask_price, ask_qty) = asks[0];
    let total = bid_qty + ask_qty;
    if total < 1e-15 {
        return (bid_price + ask_price) / 2.0;
    }
    let mid = (ask_qty * bid_price + bid_qty * ask_price) / total;
    if mid.is_finite() {
        mid
    } else {
        0.0
    }
}

// ===========================================================================
// Risk Analytics
// ===========================================================================

/// Cornish-Fisher VaR: skew/kurtosis-adjusted Value at Risk.
#[pyfunction]
#[pyo3(signature = (returns, confidence=0.95))]
pub fn cornish_fisher_var(returns: Vec<f64>, confidence: f64) -> f64 {
    let n = returns.len();
    if n < 3 {
        return 0.0;
    }
    let nf = n as f64;
    let mean = returns.iter().sum::<f64>() / nf;
    let var = returns.iter().map(|&r| (r - mean).powi(2)).sum::<f64>() / nf;
    let std = var.sqrt();
    if std < 1e-15 {
        return 0.0;
    }

    let skew = returns.iter().map(|&r| ((r - mean) / std).powi(3)).sum::<f64>() / nf;
    let kurt = returns.iter().map(|&r| ((r - mean) / std).powi(4)).sum::<f64>() / nf - 3.0;

    let z = normal_cdf_inv(1.0 - confidence);
    let z2 = z * z;
    let cf_z = z + (z2 - 1.0) * skew / 6.0 + (z2 * z - 3.0 * z) * kurt / 24.0
        - (2.0 * z2 * z - 5.0 * z) * skew * skew / 36.0;

    let var_val = -(mean + cf_z * std);
    if var_val.is_finite() {
        var_val
    } else {
        0.0
    }
}

/// Cornish-Fisher CVaR (Expected Shortfall).
/// Uses empirical tail average below the CF VaR threshold.
#[pyfunction]
#[pyo3(signature = (returns, confidence=0.95))]
pub fn cornish_fisher_cvar(returns: Vec<f64>, confidence: f64) -> f64 {
    let n = returns.len();
    if n < 3 {
        return 0.0;
    }
    // Compute VaR inline to avoid cloning the returns vector.
    let nf = n as f64;
    let mean = returns.iter().sum::<f64>() / nf;
    let variance = returns.iter().map(|&r| (r - mean).powi(2)).sum::<f64>() / nf;
    let std = variance.sqrt();
    let var = if std < 1e-15 {
        0.0
    } else {
        let skew = returns.iter().map(|&r| ((r - mean) / std).powi(3)).sum::<f64>() / nf;
        let kurt = returns.iter().map(|&r| ((r - mean) / std).powi(4)).sum::<f64>() / nf - 3.0;
        let z = normal_cdf_inv(1.0 - confidence);
        let z2 = z * z;
        let cf_z = z + (z2 - 1.0) * skew / 6.0 + (z2 * z - 3.0 * z) * kurt / 24.0
            - (2.0 * z2 * z - 5.0 * z) * skew * skew / 36.0;
        let v = -(mean + cf_z * std);
        if v.is_finite() { v } else { 0.0 }
    };
    let threshold = -var;
    let tail: Vec<f64> = returns.iter().filter(|&&r| r <= threshold).copied().collect();
    if tail.is_empty() {
        return var;
    }
    let cvar = -(tail.iter().sum::<f64>() / tail.len() as f64);
    if cvar.is_finite() {
        cvar
    } else {
        0.0
    }
}

/// Prediction market Greeks for a binary outcome contract.
///
/// Uses a simple log-normal model: delta, gamma, theta, vega.
/// - price: current market probability
/// - size: position size
/// - is_yes: YES or NO side
/// - t_hours: hours to expiry
/// - vol: implied volatility
#[pyfunction]
#[pyo3(signature = (price, size, is_yes, t_hours=24.0, vol=0.2))]
pub fn prediction_greeks(
    price: f64,
    size: f64,
    is_yes: bool,
    t_hours: f64,
    vol: f64,
) -> PredictionGreeks {
    let p = price.clamp(0.01, 0.99);
    let t = (t_hours / 24.0).max(1e-6); // in days
    let sign = if is_yes { 1.0 } else { -1.0 };

    // Map probability to a "d1"-like quantity
    let d1 = normal_cdf_inv(p);
    let pdf_d1 = normal_pdf(d1);

    let delta = sign * size * pdf_d1;
    let gamma = if vol > 1e-10 && t > 1e-10 {
        size * pdf_d1 / (vol * t.sqrt())
    } else {
        0.0
    };
    let theta = if t > 1e-10 {
        -size * pdf_d1 * vol / (2.0 * t.sqrt()) / 24.0
    } else {
        0.0
    };
    let vega = size * pdf_d1 * t.sqrt();

    PredictionGreeks {
        delta: finite_or_zero(delta),
        gamma: finite_or_zero(gamma),
        theta: finite_or_zero(theta),
        vega: finite_or_zero(vega),
    }
}

/// Prediction market Greeks result.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PredictionGreeks {
    #[pyo3(get)]
    pub delta: f64,
    #[pyo3(get)]
    pub gamma: f64,
    #[pyo3(get)]
    pub theta: f64,
    #[pyo3(get)]
    pub vega: f64,
}

#[pymethods]
impl PredictionGreeks {
    fn __repr__(&self) -> String {
        format!(
            "PredictionGreeks(delta={:.4}, gamma={:.4}, theta={:.4}, vega={:.4})",
            self.delta, self.gamma, self.theta, self.vega
        )
    }
}

// ===========================================================================
// Signal Analysis
// ===========================================================================

/// Information Coefficient: Spearman rank correlation between predictions and outcomes.
#[pyfunction]
pub fn information_coefficient(predictions: Vec<f64>, outcomes: Vec<f64>) -> PyResult<f64> {
    if predictions.len() != outcomes.len() {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "predictions and outcomes must have the same length",
        ));
    }
    let n = predictions.len();
    if n < 2 {
        return Ok(0.0);
    }
    let rank_p = rank_vector(&predictions);
    let rank_o = rank_vector(&outcomes);

    let nf = n as f64;
    let mean_rp = rank_p.iter().sum::<f64>() / nf;
    let mean_ro = rank_o.iter().sum::<f64>() / nf;

    let mut cov = 0.0;
    let mut var_p = 0.0;
    let mut var_o = 0.0;
    for i in 0..n {
        let dp = rank_p[i] - mean_rp;
        let do_ = rank_o[i] - mean_ro;
        cov += dp * do_;
        var_p += dp * dp;
        var_o += do_ * do_;
    }

    let denom = (var_p * var_o).sqrt();
    if denom < 1e-15 {
        return Ok(0.0);
    }
    let ic = cov / denom;
    Ok(if ic.is_finite() { ic } else { 0.0 })
}

/// Signal half-life from AR(1) fit: -ln(2) / ln(phi).
/// phi is the AR(1) coefficient from regressing x_t on x_{t-1}.
#[pyfunction]
pub fn signal_half_life(values: Vec<f64>) -> f64 {
    let n = values.len();
    if n < 3 {
        return 0.0;
    }

    // AR(1): x_t = phi * x_{t-1} + epsilon
    // OLS: phi = cov(x_t, x_{t-1}) / var(x_{t-1})
    let mean_prev = values[..n - 1].iter().sum::<f64>() / (n - 1) as f64;
    let mean_curr = values[1..].iter().sum::<f64>() / (n - 1) as f64;

    let mut cov = 0.0;
    let mut var_prev = 0.0;
    for i in 0..n - 1 {
        let dp = values[i] - mean_prev;
        let dc = values[i + 1] - mean_curr;
        cov += dp * dc;
        var_prev += dp * dp;
    }

    if var_prev < 1e-15 {
        return 0.0;
    }
    let phi = cov / var_prev;

    // Half-life only meaningful for mean-reverting (0 < phi < 1)
    if phi <= 0.0 || phi >= 1.0 {
        return 0.0;
    }
    let hl = -(2.0_f64.ln()) / phi.ln();
    if hl.is_finite() && hl > 0.0 {
        hl
    } else {
        0.0
    }
}

/// Hurst exponent via rescaled range (R/S) analysis.
/// H > 0.5: trending, H < 0.5: mean-reverting, H = 0.5: random walk.
#[pyfunction]
pub fn hurst_exponent(series: Vec<f64>) -> f64 {
    let n = series.len();
    if n < 20 {
        return 0.5; // Not enough data, assume random walk
    }

    // R/S analysis over multiple sub-period sizes
    let mut log_n = Vec::new();
    let mut log_rs = Vec::new();

    let mut size = 8;
    while size <= n / 2 {
        let n_blocks = n / size;
        if n_blocks < 1 {
            break;
        }
        let mut rs_sum = 0.0;
        let mut rs_count = 0u32;

        for b in 0..n_blocks {
            let start = b * size;
            let end = start + size;
            let block = &series[start..end];

            let mean = block.iter().sum::<f64>() / size as f64;
            let std = (block.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / size as f64)
                .sqrt();

            if std < 1e-15 {
                continue;
            }

            // Cumulative deviation from mean
            let mut cumdev = Vec::with_capacity(size);
            let mut running = 0.0;
            for &x in block {
                running += x - mean;
                cumdev.push(running);
            }

            let range = cumdev.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
                - cumdev.iter().cloned().fold(f64::INFINITY, f64::min);

            rs_sum += range / std;
            rs_count += 1;
        }

        if rs_count > 0 {
            let avg_rs = rs_sum / rs_count as f64;
            if avg_rs > 0.0 {
                log_n.push((size as f64).ln());
                log_rs.push(avg_rs.ln());
            }
        }

        size *= 2;
    }

    if log_n.len() < 2 {
        return 0.5;
    }

    // OLS: log(R/S) = H * log(n) + c
    let k = log_n.len() as f64;
    let mean_ln = log_n.iter().sum::<f64>() / k;
    let mean_lrs = log_rs.iter().sum::<f64>() / k;

    let mut num = 0.0;
    let mut den = 0.0;
    for i in 0..log_n.len() {
        let dx = log_n[i] - mean_ln;
        num += dx * (log_rs[i] - mean_lrs);
        den += dx * dx;
    }

    if den < 1e-15 {
        return 0.5;
    }
    let h = num / den;
    if h.is_finite() {
        h.clamp(0.0, 1.0)
    } else {
        0.5
    }
}

/// Variance ratio test: VR(q) = Var(q-period returns) / (q * Var(1-period returns)).
/// VR = 1.0 indicates random walk.
#[pyfunction]
#[pyo3(signature = (returns, period=2))]
pub fn variance_ratio(returns: Vec<f64>, period: usize) -> f64 {
    let n = returns.len();
    let q = period.max(2);
    if n < q + 1 {
        return 1.0;
    }

    let mean = returns.iter().sum::<f64>() / n as f64;
    let var_1 = returns.iter().map(|&r| (r - mean).powi(2)).sum::<f64>() / (n - 1) as f64;

    if var_1 < 1e-15 {
        return 1.0;
    }

    // q-period returns
    let mut q_returns = Vec::with_capacity(n - q + 1);
    for i in 0..=(n - q) {
        let qr: f64 = returns[i..i + q].iter().sum();
        q_returns.push(qr);
    }

    let q_mean = q_returns.iter().sum::<f64>() / q_returns.len() as f64;
    let var_q = q_returns
        .iter()
        .map(|&r| (r - q_mean).powi(2))
        .sum::<f64>()
        / (q_returns.len() - 1).max(1) as f64;

    let vr = var_q / (q as f64 * var_1);
    if vr.is_finite() {
        vr
    } else {
        1.0
    }
}

// ===========================================================================
// Statistical Testing
// ===========================================================================

/// Deflated Sharpe Ratio p-value (Bailey & Lopez de Prado, 2014).
/// Tests whether observed Sharpe is significant given multiple trials.
#[pyfunction]
pub fn deflated_sharpe(
    sharpe: f64,
    n_obs: usize,
    n_trials: usize,
    skew: f64,
    kurt: f64,
) -> f64 {
    if n_obs < 2 || n_trials < 1 {
        return 1.0;
    }
    let n = n_obs as f64;
    let t = n_trials as f64;

    // Expected max Sharpe under null (Euler-Mascheroni approximation)
    let gamma_em = 0.5772156649;
    let e_max_sr = (1.0 - gamma_em) * normal_cdf_inv(1.0 - 1.0 / t)
        + gamma_em * normal_cdf_inv(1.0 - 1.0 / (t * std::f64::consts::E));

    // Standard error of Sharpe estimate with skew/kurtosis correction
    let se = ((1.0 - skew * sharpe + (kurt - 1.0) / 4.0 * sharpe * sharpe) / (n - 1.0))
        .max(1e-15)
        .sqrt();

    let z = (sharpe - e_max_sr) / se;
    let p = 1.0 - normal_cdf(z);
    if p.is_finite() {
        p.clamp(0.0, 1.0)
    } else {
        1.0
    }
}

/// Bonferroni correction: alpha / n_trials.
#[pyfunction]
#[inline]
pub fn bonferroni_threshold(alpha: f64, n_trials: usize) -> f64 {
    if n_trials == 0 {
        return alpha;
    }
    let t = alpha / n_trials as f64;
    if t.is_finite() {
        t
    } else {
        0.0
    }
}

/// Benjamini-Hochberg FDR control.
/// Returns Vec<bool> — true if the corresponding hypothesis is rejected.
#[pyfunction]
#[pyo3(signature = (p_values, alpha=0.05))]
pub fn benjamini_hochberg(p_values: Vec<f64>, alpha: f64) -> Vec<bool> {
    let m = p_values.len();
    if m == 0 {
        return vec![];
    }

    // Sort indices by p-value
    let mut indices: Vec<usize> = (0..m).collect();
    indices.sort_by(|&a, &b| {
        p_values[a]
            .partial_cmp(&p_values[b])
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    let mut rejected = vec![false; m];
    let mut max_k = 0usize;
    let mut found = false;

    // Find largest k where p_(k) <= k/m * alpha
    for (rank, &idx) in indices.iter().enumerate() {
        let k = rank + 1;
        let threshold = (k as f64 / m as f64) * alpha;
        if p_values[idx] <= threshold {
            max_k = k;
            found = true;
        }
    }

    if found {
        for &idx in &indices[..max_k] {
            rejected[idx] = true;
        }
    }

    rejected
}

// ===========================================================================
// Streaming Detectors
// ===========================================================================

// -- VPIN --

struct VpinInner {
    bucket_volume: f64,
    n_buckets: usize,
    current_buy_vol: f64,
    current_sell_vol: f64,
    current_bucket_vol: f64,
    buckets: VecDeque<(f64, f64)>, // (buy_vol, sell_vol) per bucket
    vpin: f64,
}

/// VPIN (Volume-synchronized Probability of Informed Trading) detector.
///
/// Tracks buy/sell volume in fixed-volume buckets and computes
/// the fraction of absolute order imbalance.
#[pyclass]
pub struct VpinDetector {
    inner: Mutex<VpinInner>,
}

#[pymethods]
impl VpinDetector {
    #[new]
    #[pyo3(signature = (bucket_volume=1000.0, n_buckets=50))]
    fn new(bucket_volume: f64, n_buckets: usize) -> Self {
        let bucket_volume = if bucket_volume > 0.0 {
            bucket_volume
        } else {
            1000.0
        };
        let n_buckets = n_buckets.max(1);
        VpinDetector {
            inner: Mutex::new(VpinInner {
                bucket_volume,
                n_buckets,
                current_buy_vol: 0.0,
                current_sell_vol: 0.0,
                current_bucket_vol: 0.0,
                buckets: VecDeque::with_capacity(n_buckets),
                vpin: 0.0,
            }),
        }
    }

    /// Feed a trade. Returns VPIN when a bucket completes, else returns current VPIN.
    fn update(&self, _price: f64, volume: f64, is_buy: bool) -> f64 {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        let vol = volume.abs();

        if is_buy {
            inner.current_buy_vol += vol;
        } else {
            inner.current_sell_vol += vol;
        }
        inner.current_bucket_vol += vol;

        // Check if bucket is full
        if inner.current_bucket_vol >= inner.bucket_volume {
            let buy = inner.current_buy_vol;
            let sell = inner.current_sell_vol;
            inner.buckets.push_back((buy, sell));
            inner.current_buy_vol = 0.0;
            inner.current_sell_vol = 0.0;
            inner.current_bucket_vol = 0.0;

            // Trim to n_buckets (FIFO)
            while inner.buckets.len() > inner.n_buckets {
                inner.buckets.pop_front();
            }

            // Recompute VPIN
            if !inner.buckets.is_empty() {
                let total_imb: f64 = inner
                    .buckets
                    .iter()
                    .map(|(b, s)| (b - s).abs())
                    .sum();
                let total_vol: f64 = inner.buckets.iter().map(|(b, s)| b + s).sum();
                inner.vpin = if total_vol > 0.0 {
                    total_imb / total_vol
                } else {
                    0.0
                };
            }
        }

        inner.vpin
    }

    /// Current VPIN value.
    fn current_vpin(&self) -> f64 {
        self.inner.lock().unwrap_or_else(|e| e.into_inner()).vpin
    }

    /// Reset all state.
    fn reset(&self) {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        inner.current_buy_vol = 0.0;
        inner.current_sell_vol = 0.0;
        inner.current_bucket_vol = 0.0;
        inner.buckets.clear();
        inner.vpin = 0.0;
    }

    fn __repr__(&self) -> String {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        format!(
            "VpinDetector(buckets={}/{}, vpin={:.4})",
            inner.buckets.len(),
            inner.n_buckets,
            inner.vpin
        )
    }
}

// -- CUSUM --

struct CusumInner {
    threshold: f64,
    drift: f64,
    // Welford online mean/variance
    count: u64,
    mean: f64,
    m2: f64,
    // CUSUM state
    s_upper: f64,
    s_lower: f64,
}

/// CUSUM change-point detector with Welford online mean/variance.
///
/// Detects structural breaks in a time series using cumulative sum control chart.
#[pyclass]
pub struct CusumDetector {
    inner: Mutex<CusumInner>,
}

#[pymethods]
impl CusumDetector {
    #[new]
    #[pyo3(signature = (threshold=5.0, drift=0.0))]
    fn new(threshold: f64, drift: f64) -> Self {
        CusumDetector {
            inner: Mutex::new(CusumInner {
                threshold: threshold.max(0.01),
                drift,
                count: 0,
                mean: 0.0,
                m2: 0.0,
                s_upper: 0.0,
                s_lower: 0.0,
            }),
        }
    }

    /// Update with a new observation. Returns true if change-point detected.
    fn update(&self, value: f64) -> bool {
        let mut s = self.inner.lock().unwrap_or_else(|e| e.into_inner());

        // Welford online update
        s.count += 1;
        let delta = value - s.mean;
        s.mean += delta / s.count as f64;
        let delta2 = value - s.mean;
        s.m2 += delta * delta2;

        let std = if s.count > 1 {
            (s.m2 / (s.count - 1) as f64).sqrt()
        } else {
            1.0
        };

        let normalized = if std > 1e-15 {
            (value - s.mean) / std
        } else {
            0.0
        };

        // CUSUM update
        s.s_upper = (s.s_upper + normalized - s.drift).max(0.0);
        s.s_lower = (s.s_lower - normalized - s.drift).max(0.0);

        let triggered = s.s_upper > s.threshold || s.s_lower > s.threshold;

        if triggered {
            s.s_upper = 0.0;
            s.s_lower = 0.0;
        }

        triggered
    }

    /// Current upper CUSUM value.
    fn upper(&self) -> f64 {
        self.inner.lock().unwrap_or_else(|e| e.into_inner()).s_upper
    }

    /// Current lower CUSUM value.
    fn lower(&self) -> f64 {
        self.inner.lock().unwrap_or_else(|e| e.into_inner()).s_lower
    }

    /// Reset all state.
    fn reset(&self) {
        let mut s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        s.count = 0;
        s.mean = 0.0;
        s.m2 = 0.0;
        s.s_upper = 0.0;
        s.s_lower = 0.0;
    }

    fn __repr__(&self) -> String {
        let s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        format!(
            "CusumDetector(upper={:.4}, lower={:.4}, threshold={:.1})",
            s.s_upper, s.s_lower, s.threshold
        )
    }
}

// -- OFI Tracker --

struct OfiInner {
    prev_bid_qty: f64,
    prev_ask_qty: f64,
    cumulative: f64,
    has_prev: bool,
}

/// Order Flow Imbalance tracker.
///
/// Tracks changes in best bid/ask quantities to measure order flow direction.
#[pyclass]
pub struct OfiTracker {
    inner: Mutex<OfiInner>,
}

#[pymethods]
impl OfiTracker {
    #[new]
    fn new() -> Self {
        OfiTracker {
            inner: Mutex::new(OfiInner {
                prev_bid_qty: 0.0,
                prev_ask_qty: 0.0,
                cumulative: 0.0,
                has_prev: false,
            }),
        }
    }

    /// Update with current best bid/ask quantities. Returns OFI delta.
    fn update(&self, best_bid_qty: f64, best_ask_qty: f64) -> f64 {
        let mut s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        if !s.has_prev {
            s.prev_bid_qty = best_bid_qty;
            s.prev_ask_qty = best_ask_qty;
            s.has_prev = true;
            return 0.0;
        }

        let delta_bid = best_bid_qty - s.prev_bid_qty;
        let delta_ask = best_ask_qty - s.prev_ask_qty;
        let ofi = delta_bid - delta_ask;

        s.prev_bid_qty = best_bid_qty;
        s.prev_ask_qty = best_ask_qty;
        s.cumulative += ofi;

        if ofi.is_finite() {
            ofi
        } else {
            0.0
        }
    }

    /// Cumulative OFI since last reset.
    fn cumulative(&self) -> f64 {
        self.inner.lock().unwrap_or_else(|e| e.into_inner()).cumulative
    }

    /// Reset all state.
    fn reset(&self) {
        let mut s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        s.prev_bid_qty = 0.0;
        s.prev_ask_qty = 0.0;
        s.cumulative = 0.0;
        s.has_prev = false;
    }

    fn __repr__(&self) -> String {
        let s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        format!("OfiTracker(cumulative={:.4})", s.cumulative)
    }
}

// -- Hawkes Process --

struct HawkesInner {
    mu: f64,
    alpha: f64,
    beta: f64,
    events: VecDeque<f64>,
    max_events: usize,
}

/// Self-exciting Hawkes point process for trade arrival intensity estimation.
///
/// Models bursty activity in prediction markets: each event increases the
/// probability of further events, decaying exponentially over time.
/// Intensity: lambda(t) = mu + sum_i alpha * exp(-beta * (t - t_i))
#[pyclass]
pub struct HawkesProcess {
    inner: Mutex<HawkesInner>,
}

#[pymethods]
impl HawkesProcess {
    #[new]
    #[pyo3(signature = (mu=0.1, alpha=0.5, beta=1.0, max_events=1000))]
    fn new(mu: f64, alpha: f64, beta: f64, max_events: usize) -> Self {
        HawkesProcess {
            inner: Mutex::new(HawkesInner {
                mu: mu.max(0.0),
                alpha: alpha.max(0.0),
                beta: beta.max(1e-10),
                events: VecDeque::with_capacity(max_events.max(10)),
                max_events: max_events.max(10),
            }),
        }
    }

    /// Record an event at the given timestamp.
    fn add_event(&self, timestamp: f64) {
        let mut s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        s.events.push_back(timestamp);
        while s.events.len() > s.max_events {
            s.events.pop_front();
        }
    }

    /// Compute intensity at time t: lambda(t) = mu + sum alpha * exp(-beta * (t - ti)).
    fn intensity(&self, t: f64) -> f64 {
        let s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        let mut lam = s.mu;
        for &ti in &s.events {
            let dt = t - ti;
            if dt > 0.0 {
                lam += s.alpha * (-s.beta * dt).exp();
            }
        }
        finite_or_zero(lam)
    }

    /// Compute intensity at current wall-clock time.
    fn intensity_now(&self) -> f64 {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs_f64();
        self.intensity(now)
    }

    /// Expected number of events in [t, t+horizon] given current state.
    fn expected_events(&self, t: f64, horizon: f64) -> f64 {
        let s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        // Integral of lambda(t') from t to t+horizon
        // = mu * horizon + sum alpha/beta * (exp(-beta*(t-ti)) - exp(-beta*(t+horizon-ti)))
        let mut integral = s.mu * horizon;
        for &ti in &s.events {
            let dt = t - ti;
            if dt >= 0.0 {
                let a = (-s.beta * dt).exp();
                let b = (-s.beta * (dt + horizon)).exp();
                integral += (s.alpha / s.beta) * (a - b);
            }
        }
        finite_or_zero(integral.max(0.0))
    }

    /// Branching ratio alpha/beta: >1 means explosive, <1 means stable.
    fn branching_ratio(&self) -> f64 {
        let s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        if s.beta > 1e-15 { s.alpha / s.beta } else { 0.0 }
    }

    /// Number of recorded events.
    fn event_count(&self) -> usize {
        self.inner.lock().unwrap_or_else(|e| e.into_inner()).events.len()
    }

    /// Reset all state.
    fn reset(&self) {
        self.inner.lock().unwrap_or_else(|e| e.into_inner()).events.clear();
    }

    fn __repr__(&self) -> String {
        let s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        format!(
            "HawkesProcess(mu={:.4}, alpha={:.4}, beta={:.4}, events={})",
            s.mu, s.alpha, s.beta, s.events.len()
        )
    }
}

// -- Ledoit-Wolf Shrinkage --

/// Ledoit-Wolf shrinkage estimator for covariance matrices.
///
/// Shrinks the sample covariance toward a structured target (scaled identity)
/// to produce a better-conditioned estimate. Returns (shrunk_cov, shrinkage_intensity).
///
/// Args:
///     returns: T x N matrix (each inner Vec is one time-step of N assets).
///
/// Reference: Ledoit & Wolf (2004) "A well-conditioned estimator for
/// large-dimensional covariance matrices."
#[pyfunction]
fn ledoit_wolf_shrinkage(returns: Vec<Vec<f64>>) -> PyResult<(Vec<Vec<f64>>, f64)> {
    let t = returns.len();
    if t < 2 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "Need at least 2 observations",
        ));
    }
    let n = returns[0].len();
    if n == 0 {
        return Err(pyo3::exceptions::PyValueError::new_err("Empty returns"));
    }
    for row in &returns {
        if row.len() != n {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "All rows must have the same length",
            ));
        }
    }

    // Compute means
    let mut means = vec![0.0; n];
    for row in &returns {
        for j in 0..n {
            means[j] += row[j];
        }
    }
    for m in &mut means {
        *m /= t as f64;
    }

    // Compute sample covariance S (unbiased, 1/(T-1))
    let mut s_mat = vec![vec![0.0; n]; n];
    for row in &returns {
        for i in 0..n {
            let di = row[i] - means[i];
            for j in i..n {
                let dj = row[j] - means[j];
                s_mat[i][j] += di * dj;
            }
        }
    }
    let denom = (t - 1) as f64;
    for i in 0..n {
        for j in i..n {
            s_mat[i][j] /= denom;
            if j != i {
                s_mat[j][i] = s_mat[i][j];
            }
        }
    }

    // Target: mu * I where mu = trace(S)/N
    let trace: f64 = (0..n).map(|i| s_mat[i][i]).sum();
    let mu_target = trace / n as f64;

    // Compute optimal shrinkage intensity (Ledoit-Wolf formula)
    // delta = ||S - mu*I||^2_F / N
    let mut delta = 0.0;
    for i in 0..n {
        for j in 0..n {
            let target_ij = if i == j { mu_target } else { 0.0 };
            let diff = s_mat[i][j] - target_ij;
            delta += diff * diff;
        }
    }

    // beta_bar = 1/(T*N) * sum_k ||x_k x_k^T - S||^2_F
    let mut beta_sum = 0.0;
    for row in &returns {
        let mut sq_norm = 0.0;
        for i in 0..n {
            let di = row[i] - means[i];
            for j in 0..n {
                let dj = row[j] - means[j];
                let diff = di * dj - s_mat[i][j];
                sq_norm += diff * diff;
            }
        }
        beta_sum += sq_norm;
    }
    let beta_bar = beta_sum / (t as f64 * t as f64);

    // Shrinkage intensity: clamp to [0, 1]
    let shrinkage = if delta > 1e-15 {
        (beta_bar / delta).clamp(0.0, 1.0)
    } else {
        1.0
    };

    // Shrunk covariance: shrinkage * mu*I + (1 - shrinkage) * S
    let mut result = vec![vec![0.0; n]; n];
    for i in 0..n {
        for j in 0..n {
            let target_ij = if i == j { mu_target } else { 0.0 };
            result[i][j] = shrinkage * target_ij + (1.0 - shrinkage) * s_mat[i][j];
        }
    }

    Ok((result, shrinkage))
}

// ===========================================================================
// Helpers
// ===========================================================================

#[inline]
fn finite_or_zero(v: f64) -> f64 {
    if v.is_finite() {
        v
    } else {
        0.0
    }
}

fn min_max(v: &[f64]) -> (f64, f64) {
    let mut mn = f64::INFINITY;
    let mut mx = f64::NEG_INFINITY;
    for &x in v {
        if x < mn {
            mn = x;
        }
        if x > mx {
            mx = x;
        }
    }
    (mn, mx)
}

// ===========================================================================
// Registration
// ===========================================================================

/// Register all quant types and functions in the Python module.
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    // Information theory
    m.add_function(wrap_pyfunction!(shannon_entropy, m)?)?;
    m.add_function(wrap_pyfunction!(joint_entropy, m)?)?;
    m.add_function(wrap_pyfunction!(kl_divergence, m)?)?;
    m.add_function(wrap_pyfunction!(mutual_information, m)?)?;
    m.add_function(wrap_pyfunction!(transfer_entropy, m)?)?;

    // Microstructure
    m.add_function(wrap_pyfunction!(kyles_lambda, m)?)?;
    m.add_function(wrap_pyfunction!(amihud_ratio, m)?)?;
    m.add_function(wrap_pyfunction!(roll_spread, m)?)?;
    m.add_function(wrap_pyfunction!(effective_spread, m)?)?;
    m.add_function(wrap_pyfunction!(realized_spread, m)?)?;
    m.add_function(wrap_pyfunction!(lob_imbalance, m)?)?;
    m.add_function(wrap_pyfunction!(weighted_mid, m)?)?;

    // Risk analytics
    m.add_function(wrap_pyfunction!(cornish_fisher_var, m)?)?;
    m.add_function(wrap_pyfunction!(cornish_fisher_cvar, m)?)?;
    m.add_function(wrap_pyfunction!(prediction_greeks, m)?)?;
    m.add_class::<PredictionGreeks>()?;

    // Signal analysis
    m.add_function(wrap_pyfunction!(information_coefficient, m)?)?;
    m.add_function(wrap_pyfunction!(signal_half_life, m)?)?;
    m.add_function(wrap_pyfunction!(hurst_exponent, m)?)?;
    m.add_function(wrap_pyfunction!(variance_ratio, m)?)?;

    // Statistical testing
    m.add_function(wrap_pyfunction!(deflated_sharpe, m)?)?;
    m.add_function(wrap_pyfunction!(bonferroni_threshold, m)?)?;
    m.add_function(wrap_pyfunction!(benjamini_hochberg, m)?)?;

    // Streaming detectors
    m.add_class::<VpinDetector>()?;
    m.add_class::<CusumDetector>()?;
    m.add_class::<OfiTracker>()?;
    m.add_class::<HawkesProcess>()?;

    // Covariance estimation
    m.add_function(wrap_pyfunction!(ledoit_wolf_shrinkage, m)?)?;

    Ok(())
}

// ===========================================================================
// Tests
// ===========================================================================

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

    // -- Helpers --

    #[test]
    fn test_normal_pdf_at_zero() {
        let p = normal_pdf(0.0);
        assert!((p - 0.39894228).abs() < 1e-5);
    }

    #[test]
    fn test_normal_pdf_symmetry() {
        assert!((normal_pdf(1.0) - normal_pdf(-1.0)).abs() < 1e-10);
    }

    #[test]
    fn test_normal_cdf_at_zero() {
        assert!((normal_cdf(0.0) - 0.5).abs() < 1e-6);
    }

    #[test]
    fn test_normal_cdf_monotonic() {
        assert!(normal_cdf(-2.0) < normal_cdf(-1.0));
        assert!(normal_cdf(-1.0) < normal_cdf(0.0));
        assert!(normal_cdf(0.0) < normal_cdf(1.0));
        assert!(normal_cdf(1.0) < normal_cdf(2.0));
    }

    #[test]
    fn test_normal_cdf_inv_roundtrip() {
        for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
            let z = normal_cdf_inv(p);
            let back = normal_cdf(z);
            assert!((back - p).abs() < 0.01, "p={}, z={}, back={}", p, z, back);
        }
    }

    #[test]
    fn test_rank_vector_basic() {
        let ranks = rank_vector(&[10.0, 30.0, 20.0]);
        assert!((ranks[0] - 1.0).abs() < 1e-10);
        assert!((ranks[1] - 3.0).abs() < 1e-10);
        assert!((ranks[2] - 2.0).abs() < 1e-10);
    }

    #[test]
    fn test_rank_vector_ties() {
        let ranks = rank_vector(&[10.0, 10.0, 20.0]);
        assert!((ranks[0] - 1.5).abs() < 1e-10);
        assert!((ranks[1] - 1.5).abs() < 1e-10);
        assert!((ranks[2] - 3.0).abs() < 1e-10);
    }

    // -- Information Theory --

    #[test]
    fn test_shannon_entropy_half() {
        let h = shannon_entropy(0.5);
        assert!((h - 1.0).abs() < 1e-10, "h={}", h); // max entropy for binary
    }

    #[test]
    fn test_shannon_entropy_zero() {
        assert_eq!(shannon_entropy(0.0), 0.0);
        assert_eq!(shannon_entropy(1.0), 0.0);
    }

    #[test]
    fn test_shannon_entropy_nan_inf() {
        assert_eq!(shannon_entropy(f64::NAN), 0.0);
        assert_eq!(shannon_entropy(f64::INFINITY), 0.0);
        assert_eq!(shannon_entropy(-1.0), 0.0);
    }

    #[test]
    fn test_joint_entropy_uniform() {
        let h = joint_entropy(vec![0.25, 0.25, 0.25, 0.25]);
        assert!((h - 2.0).abs() < 1e-10); // log2(4) = 2
    }

    #[test]
    fn test_joint_entropy_certain() {
        let h = joint_entropy(vec![1.0]);
        assert!((h - 0.0).abs() < 1e-10);
    }

    #[test]
    fn test_kl_divergence_same() {
        let kl = kl_divergence(vec![0.5, 0.5], vec![0.5, 0.5]).unwrap();
        assert!(kl.abs() < 1e-10);
    }

    #[test]
    fn test_kl_divergence_different() {
        let kl = kl_divergence(vec![0.9, 0.1], vec![0.1, 0.9]).unwrap();
        assert!(kl > 0.0);
    }

    #[test]
    fn test_kl_divergence_length_mismatch() {
        assert!(kl_divergence(vec![0.5], vec![0.5, 0.5]).is_err());
    }

    #[test]
    fn test_kl_divergence_empty() {
        assert_eq!(kl_divergence(vec![], vec![]).unwrap(), 0.0);
    }

    #[test]
    fn test_mutual_information_independent() {
        // Independent uniform: MI should be near 0
        let x: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).sin()).collect();
        let y: Vec<f64> = (0..100).map(|i| (i as f64 * 0.3 + 2.0).cos()).collect();
        let mi = mutual_information(x, y, 5).unwrap();
        assert!(mi < 0.5, "MI={}", mi);
    }

    #[test]
    fn test_mutual_information_identical() {
        let x: Vec<f64> = (0..200).map(|i| i as f64).collect();
        let mi = mutual_information(x.clone(), x, 10).unwrap();
        assert!(mi > 0.5, "MI={}", mi);
    }

    #[test]
    fn test_mutual_information_length_mismatch() {
        assert!(mutual_information(vec![1.0], vec![1.0, 2.0], 5).is_err());
    }

    #[test]
    fn test_transfer_entropy_basic() {
        // Causal: target = source lagged
        let source: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).sin()).collect();
        let mut target = vec![0.0];
        target.extend_from_slice(&source[..99]);
        let te = transfer_entropy(source, target, 1, 5).unwrap();
        assert!(te >= 0.0);
    }

    #[test]
    fn test_transfer_entropy_short() {
        assert_eq!(transfer_entropy(vec![1.0], vec![2.0], 1, 5).unwrap(), 0.0);
    }

    // -- Microstructure --

    #[test]
    fn test_kyles_lambda_positive_impact() {
        // Price goes up when buying volume is positive
        let dp = vec![0.01, 0.02, -0.01, -0.02, 0.015];
        let sv = vec![100.0, 200.0, -100.0, -200.0, 150.0];
        let lam = kyles_lambda(dp, sv).unwrap();
        assert!(lam > 0.0, "lambda={}", lam);
    }

    #[test]
    fn test_kyles_lambda_length_mismatch() {
        assert!(kyles_lambda(vec![0.01], vec![1.0, 2.0]).is_err());
    }

    #[test]
    fn test_kyles_lambda_short() {
        assert_eq!(kyles_lambda(vec![0.01], vec![100.0]).unwrap(), 0.0);
    }

    #[test]
    fn test_amihud_basic() {
        let r = vec![0.01, -0.02, 0.03];
        let v = vec![1000.0, 2000.0, 1500.0];
        let a = amihud_ratio(r, v).unwrap();
        // mean(|0.01|/1000, |0.02|/2000, |0.03|/1500) = mean(1e-5, 1e-5, 2e-5) = 1.333e-5
        assert!(a > 0.0 && a < 0.001, "amihud={}", a);
    }

    #[test]
    fn test_amihud_zero_volume() {
        let a = amihud_ratio(vec![0.01], vec![0.0]).unwrap();
        assert_eq!(a, 0.0);
    }

    #[test]
    fn test_roll_spread_negative_autocov() {
        // Alternating returns: should have negative autocovariance
        let r = vec![0.01, -0.01, 0.01, -0.01, 0.01, -0.01];
        let s = roll_spread(r);
        assert!(s > 0.0, "spread={}", s);
    }

    #[test]
    fn test_roll_spread_trending() {
        // Trending returns: positive autocovariance → 0 spread
        let r = vec![0.01, 0.01, 0.01, 0.01, 0.01];
        assert_eq!(roll_spread(r), 0.0);
    }

    #[test]
    fn test_roll_spread_short() {
        assert_eq!(roll_spread(vec![0.01]), 0.0);
    }

    #[test]
    fn test_effective_spread_buy() {
        let es = effective_spread(0.52, 0.50, true);
        assert!((es - 0.04).abs() < 1e-10); // 2 * (0.52 - 0.50)
    }

    #[test]
    fn test_effective_spread_sell() {
        let es = effective_spread(0.48, 0.50, false);
        assert!((es - 0.04).abs() < 1e-10); // 2 * -1 * (0.48 - 0.50) = 0.04
    }

    #[test]
    fn test_realized_spread_buy() {
        let rs = realized_spread(0.52, 0.51, true);
        // 2 * 1 * (0.52 - 0.51) = 0.02
        assert!((rs - 0.02).abs() < 1e-10);
    }

    #[test]
    fn test_lob_imbalance_equal() {
        let bids = vec![(0.50, 100.0)];
        let asks = vec![(0.51, 100.0)];
        let imb = lob_imbalance(bids, asks, 5);
        assert!(imb.abs() < 1e-10);
    }

    #[test]
    fn test_lob_imbalance_bid_heavy() {
        let bids = vec![(0.50, 200.0)];
        let asks = vec![(0.51, 100.0)];
        let imb = lob_imbalance(bids, asks, 5);
        // (200 - 100) / 300 = 0.333
        assert!((imb - 1.0 / 3.0).abs() < 1e-10);
    }

    #[test]
    fn test_lob_imbalance_empty() {
        assert_eq!(lob_imbalance(vec![], vec![], 5), 0.0);
    }

    #[test]
    fn test_weighted_mid_equal_qty() {
        let bids = vec![(0.50, 100.0)];
        let asks = vec![(0.52, 100.0)];
        let mid = weighted_mid(bids, asks);
        // (100*0.50 + 100*0.52) / 200 = 0.51 = arithmetic mid
        assert!((mid - 0.51).abs() < 1e-10);
    }

    #[test]
    fn test_weighted_mid_skewed() {
        let bids = vec![(0.50, 200.0)];
        let asks = vec![(0.52, 100.0)];
        let mid = weighted_mid(bids, asks);
        // (100*0.50 + 200*0.52) / 300 = (50+104)/300 = 154/300 = 0.51333
        assert!((mid - 154.0 / 300.0).abs() < 1e-10);
    }

    #[test]
    fn test_weighted_mid_empty() {
        assert_eq!(weighted_mid(vec![], vec![(0.5, 10.0)]), 0.0);
    }

    // -- Risk Analytics --

    #[test]
    fn test_cornish_fisher_var_normal() {
        // For normal returns (skew=0, kurt=0), CF-VaR ≈ standard VaR
        let returns: Vec<f64> = (0..1000)
            .map(|i| {
                let x = (i as f64 * 0.123).sin() * 0.01;
                x
            })
            .collect();
        let v = cornish_fisher_var(returns, 0.95);
        assert!(v.is_finite());
    }

    #[test]
    fn test_cornish_fisher_var_short() {
        assert_eq!(cornish_fisher_var(vec![0.01, 0.02], 0.95), 0.0);
    }

    #[test]
    fn test_cornish_fisher_cvar_ge_var() {
        let returns: Vec<f64> = (0..500).map(|i| (i as f64 * 0.1).sin() * 0.02).collect();
        let v = cornish_fisher_var(returns.clone(), 0.95);
        let cv = cornish_fisher_cvar(returns, 0.95);
        // CVaR should be >= VaR (expected shortfall is further in the tail)
        assert!(cv >= v - 1e-10, "cvar={} var={}", cv, v);
    }

    #[test]
    fn test_prediction_greeks_basic() {
        let g = prediction_greeks(0.5, 100.0, true, 24.0, 0.2);
        assert!(g.delta != 0.0);
        assert!(g.gamma >= 0.0);
        assert!(g.theta <= 0.0); // Theta should be negative (time decay)
        assert!(g.vega >= 0.0);
    }

    #[test]
    fn test_prediction_greeks_no_side() {
        let yes = prediction_greeks(0.5, 100.0, true, 24.0, 0.2);
        let no = prediction_greeks(0.5, 100.0, false, 24.0, 0.2);
        // Deltas should have opposite signs
        assert!((yes.delta + no.delta).abs() < 1e-10);
    }

    #[test]
    fn test_prediction_greeks_repr() {
        let g = prediction_greeks(0.5, 100.0, true, 24.0, 0.2);
        let r = g.__repr__();
        assert!(r.contains("PredictionGreeks"));
    }

    // -- Signal Analysis --

    #[test]
    fn test_ic_perfect() {
        let pred = vec![0.1, 0.2, 0.3, 0.4, 0.5];
        let out = vec![0.2, 0.4, 0.6, 0.8, 1.0];
        let ic = information_coefficient(pred, out).unwrap();
        assert!((ic - 1.0).abs() < 1e-10, "ic={}", ic);
    }

    #[test]
    fn test_ic_inverse() {
        let pred = vec![0.5, 0.4, 0.3, 0.2, 0.1];
        let out = vec![0.2, 0.4, 0.6, 0.8, 1.0];
        let ic = information_coefficient(pred, out).unwrap();
        assert!((ic - (-1.0)).abs() < 1e-10, "ic={}", ic);
    }

    #[test]
    fn test_ic_length_mismatch() {
        assert!(information_coefficient(vec![0.1], vec![0.1, 0.2]).is_err());
    }

    #[test]
    fn test_signal_half_life_mean_reverting() {
        // AR(1) with phi = 0.5 → half-life = -ln(2)/ln(0.5) = 1.0
        let mut series = vec![1.0];
        for _ in 0..200 {
            let last = series.last().unwrap();
            series.push(0.5 * last + 0.01); // phi = 0.5
        }
        let hl = signal_half_life(series);
        assert!(hl > 0.0, "hl={}", hl);
    }

    #[test]
    fn test_signal_half_life_short() {
        assert_eq!(signal_half_life(vec![1.0, 2.0]), 0.0);
    }

    #[test]
    fn test_hurst_random_walk() {
        // Random walk approximation: Hurst should be ~0.5
        let series: Vec<f64> = (0..500)
            .map(|i| (i as f64 * 0.1234).sin())
            .collect();
        let h = hurst_exponent(series);
        assert!(h > 0.0 && h < 1.0, "hurst={}", h);
    }

    #[test]
    fn test_hurst_short_series() {
        assert_eq!(hurst_exponent(vec![1.0; 10]), 0.5);
    }

    #[test]
    fn test_variance_ratio_random_walk() {
        // For a random walk, VR should be ~1.0
        let returns: Vec<f64> = (0..1000)
            .map(|i| (i as f64 * 0.0987).sin() * 0.01)
            .collect();
        let vr = variance_ratio(returns, 2);
        assert!(vr > 0.0, "vr={}", vr);
    }

    #[test]
    fn test_variance_ratio_short() {
        assert_eq!(variance_ratio(vec![0.01], 2), 1.0);
    }

    // -- Statistical Testing --

    #[test]
    fn test_deflated_sharpe_insignificant() {
        // Low Sharpe with many trials → high p-value
        let p = deflated_sharpe(0.5, 100, 100, 0.0, 3.0);
        assert!(p > 0.05, "p={}", p);
    }

    #[test]
    fn test_deflated_sharpe_strong() {
        // Very high Sharpe should give low p-value
        let p = deflated_sharpe(5.0, 1000, 10, 0.0, 3.0);
        assert!(p < 0.05, "p={}", p);
    }

    #[test]
    fn test_deflated_sharpe_edge_cases() {
        assert_eq!(deflated_sharpe(1.0, 1, 1, 0.0, 3.0), 1.0);
        assert_eq!(deflated_sharpe(1.0, 100, 0, 0.0, 3.0), 1.0);
    }

    #[test]
    fn test_bonferroni_basic() {
        let t = bonferroni_threshold(0.05, 10);
        assert!((t - 0.005).abs() < 1e-10);
    }

    #[test]
    fn test_bonferroni_zero_trials() {
        assert_eq!(bonferroni_threshold(0.05, 0), 0.05);
    }

    #[test]
    fn test_bh_basic() {
        let pvals = vec![0.01, 0.03, 0.04, 0.5, 0.8];
        let rejected = benjamini_hochberg(pvals, 0.05);
        // First three should be rejected at alpha=0.05
        assert!(rejected[0]); // 0.01 <= 1/5 * 0.05 = 0.01 ✓
        assert!(rejected[1]); // 0.03 <= 2/5 * 0.05 = 0.02 ✗ but BH includes all up to max k
        assert!(rejected[2]); // 0.04 <= 3/5 * 0.05 = 0.03 ✗
        // rejected[3] and [4] should not be rejected
        assert!(!rejected[3]);
        assert!(!rejected[4]);
    }

    #[test]
    fn test_bh_empty() {
        assert!(benjamini_hochberg(vec![], 0.05).is_empty());
    }

    #[test]
    fn test_bh_all_significant() {
        let pvals = vec![0.001, 0.002, 0.003];
        let rejected = benjamini_hochberg(pvals, 0.05);
        assert!(rejected.iter().all(|&r| r));
    }

    #[test]
    fn test_bh_none_significant() {
        let pvals = vec![0.9, 0.95, 0.99];
        let rejected = benjamini_hochberg(pvals, 0.05);
        assert!(rejected.iter().all(|&r| !r));
    }

    // -- Streaming Detectors --

    #[test]
    fn test_vpin_basic() {
        let vpin = VpinDetector::new(100.0, 5);
        // Fill one bucket
        for _ in 0..10 {
            vpin.update(0.5, 10.0, true);
        }
        let v = vpin.current_vpin();
        // All buy → VPIN should be 1.0
        assert!((v - 1.0).abs() < 1e-10, "vpin={}", v);
    }

    #[test]
    fn test_vpin_balanced() {
        let vpin = VpinDetector::new(100.0, 5);
        for _ in 0..5 {
            vpin.update(0.5, 10.0, true);
            vpin.update(0.5, 10.0, false);
        }
        let v = vpin.current_vpin();
        // Balanced → VPIN should be 0
        assert!(v.abs() < 1e-10, "vpin={}", v);
    }

    #[test]
    fn test_vpin_reset() {
        let vpin = VpinDetector::new(100.0, 5);
        for _ in 0..10 {
            vpin.update(0.5, 10.0, true);
        }
        assert!(vpin.current_vpin() > 0.0);
        vpin.reset();
        assert_eq!(vpin.current_vpin(), 0.0);
    }

    #[test]
    fn test_vpin_repr() {
        let vpin = VpinDetector::new(100.0, 5);
        assert!(vpin.__repr__().contains("VpinDetector"));
    }

    #[test]
    fn test_cusum_no_change() {
        let cusum = CusumDetector::new(5.0, 0.0);
        let mut triggered = false;
        for i in 0..50 {
            // Small constant value
            if cusum.update(1.0 + (i as f64 * 0.001)) {
                triggered = true;
            }
        }
        // Should not trigger for nearly constant input
        assert!(!triggered);
    }

    #[test]
    fn test_cusum_shift() {
        let cusum = CusumDetector::new(3.0, 0.0);
        // Feed stable data
        for _ in 0..100 {
            cusum.update(1.0);
        }
        // Sudden shift
        let mut triggered = false;
        for _ in 0..50 {
            if cusum.update(10.0) {
                triggered = true;
                break;
            }
        }
        assert!(triggered, "CUSUM should detect shift");
    }

    #[test]
    fn test_cusum_reset() {
        let cusum = CusumDetector::new(5.0, 0.0);
        for _ in 0..20 {
            cusum.update(1.0);
        }
        cusum.reset();
        assert_eq!(cusum.upper(), 0.0);
        assert_eq!(cusum.lower(), 0.0);
    }

    #[test]
    fn test_cusum_repr() {
        let cusum = CusumDetector::new(5.0, 0.0);
        assert!(cusum.__repr__().contains("CusumDetector"));
    }

    #[test]
    fn test_ofi_basic() {
        let ofi = OfiTracker::new();
        let d1 = ofi.update(100.0, 100.0); // First: no previous
        assert_eq!(d1, 0.0);
        let d2 = ofi.update(120.0, 90.0); // Bid up, ask down → OFI positive
        // delta_bid=20, delta_ask=-10 → OFI = 20 - (-10) = 30
        assert!((d2 - 30.0).abs() < 1e-10, "ofi={}", d2);
        assert!((ofi.cumulative() - 30.0).abs() < 1e-10);
    }

    #[test]
    fn test_ofi_reset() {
        let ofi = OfiTracker::new();
        ofi.update(100.0, 100.0);
        ofi.update(120.0, 90.0);
        ofi.reset();
        assert_eq!(ofi.cumulative(), 0.0);
    }

    #[test]
    fn test_ofi_repr() {
        let ofi = OfiTracker::new();
        assert!(ofi.__repr__().contains("OfiTracker"));
    }
}
