//! Automated market making math for prediction markets.
//!
//! Implements Avellaneda-Stoikov style inventory-skewed quoting.
//! All functions are `#[inline]` for zero-overhead inlining at call sites.

use pyo3::prelude::*;

/// Internal implementation (no tier check).
#[inline]
fn reservation_price_impl(
    mid: f64,
    inventory: f64,
    gamma: f64,
    volatility: f64,
    time_horizon: f64,
) -> f64 {
    if !mid.is_finite()
        || !inventory.is_finite()
        || !gamma.is_finite()
        || !volatility.is_finite()
        || !time_horizon.is_finite()
    {
        return if mid.is_finite() { mid } else { 0.0 };
    }
    mid - inventory * gamma * volatility * volatility * time_horizon
}

/// Avellaneda-Stoikov reservation price: inventory-skewed fair value.
///
/// Formula: r = mid - inventory * gamma * volatility^2 * time_horizon
///
/// - `mid`: current mid price
/// - `inventory`: net inventory (positive = long, negative = short)
/// - `gamma`: risk aversion parameter (higher = more aggressive skew)
/// - `volatility`: estimated price volatility (std dev of returns)
/// - `time_horizon`: time horizon in arbitrary units (e.g., 1.0 = full session)
///
/// Returns mid price if any input is NaN/Inf.
#[pyfunction]
#[inline]
pub fn reservation_price(
    mid: f64,
    inventory: f64,
    gamma: f64,
    volatility: f64,
    time_horizon: f64,
) -> PyResult<f64> {
    Ok(reservation_price_impl(mid, inventory, gamma, volatility, time_horizon))
}

/// Internal implementation (no tier check).
#[inline]
fn optimal_spread_impl(
    volatility: f64,
    inventory: f64,
    gamma: f64,
    kappa: f64,
    time_horizon: f64,
) -> f64 {
    if !volatility.is_finite()
        || !inventory.is_finite()
        || !gamma.is_finite()
        || !kappa.is_finite()
        || !time_horizon.is_finite()
        || gamma <= 0.0
        || kappa <= 0.0
    {
        return 0.0;
    }
    let variance_component = gamma * volatility * volatility * time_horizon;
    let arrival_component = (2.0 / gamma) * (1.0 + gamma / kappa).ln();
    let spread = variance_component + arrival_component;
    spread.max(0.0)
}

/// Optimal bid-ask spread width (Avellaneda-Stoikov).
///
/// Formula: spread = gamma * sigma^2 * T + (2/gamma) * ln(1 + gamma/kappa)
///
/// - `volatility`: estimated price volatility
/// - `inventory`: net inventory (used indirectly via gamma)
/// - `gamma`: risk aversion parameter
/// - `kappa`: order arrival intensity (higher = more frequent fills)
/// - `time_horizon`: time horizon
///
/// Returns 0.0 if any input is NaN/Inf or spread would be negative.
#[pyfunction]
#[inline]
pub fn optimal_spread(
    volatility: f64,
    inventory: f64,
    gamma: f64,
    kappa: f64,
    time_horizon: f64,
) -> PyResult<f64> {
    Ok(optimal_spread_impl(volatility, inventory, gamma, kappa, time_horizon))
}

/// Internal implementation (no tier check).
#[inline]
fn competitive_spread_impl(
    base_spread: f64,
    book_spread: f64,
    book_imbalance: f64,
    aggression: f64,
) -> f64 {
    if !base_spread.is_finite()
        || !book_spread.is_finite()
        || !book_imbalance.is_finite()
        || !aggression.is_finite()
    {
        return if base_spread.is_finite() {
            base_spread.clamp(0.001, 1.0)
        } else {
            0.04
        };
    }
    let agg = aggression.clamp(0.0, 1.0);
    let imb = book_imbalance.clamp(-1.0, 1.0);

    // Blend model and market spread
    let blended = (1.0 - agg) * base_spread + agg * book_spread;

    // Imbalance adjustment: tighten on the strong side, widen on the weak side
    let adjustment = 1.0 - imb.abs() * 0.1;
    let spread = blended * adjustment;

    spread.clamp(0.001, 1.0)
}

/// Competitive spread: blend model spread with observed market spread.
///
/// - `base_spread`: model-derived optimal spread
/// - `book_spread`: observed bid-ask spread from orderbook
/// - `book_imbalance`: orderbook imbalance in [-1, 1] (>0 = buy pressure)
/// - `aggression`: 0.0 = use model spread, 1.0 = use book spread
///
/// Returns clamped to [0.001, 1.0].
#[pyfunction]
#[inline]
pub fn competitive_spread(
    base_spread: f64,
    book_spread: f64,
    book_imbalance: f64,
    aggression: f64,
) -> PyResult<f64> {
    Ok(competitive_spread_impl(base_spread, book_spread, book_imbalance, aggression))
}

/// Internal implementation (no tier check).
#[inline]
fn mm_size_impl(
    base_size: f64,
    inventory: f64,
    max_position: f64,
    fill_rate: f64,
    skew_factor: f64,
) -> (f64, f64) {
    if !base_size.is_finite()
        || !inventory.is_finite()
        || !max_position.is_finite()
        || !fill_rate.is_finite()
        || !skew_factor.is_finite()
        || base_size <= 0.0
        || max_position <= 0.0
    {
        return (0.0, 0.0);
    }

    let skew = skew_factor.clamp(0.0, 1.0);
    let fill = fill_rate.clamp(0.0, 1.0);

    // Inventory ratio: -1 (max short) to +1 (max long)
    let inv_ratio = (inventory / max_position).clamp(-1.0, 1.0);

    // Skew factor: reduce size on the side that would increase inventory
    let bid_skew = 1.0 - skew * inv_ratio.max(0.0);
    let ask_skew = 1.0 + skew * inv_ratio.min(0.0);

    // Fill rate adjustment
    let fill_adj = 1.0 + fill * 0.5;

    let bid_size = (base_size * bid_skew * fill_adj).max(0.0);
    let ask_size = (base_size * ask_skew * fill_adj).max(0.0);

    (bid_size, ask_size)
}

/// Inventory-skewed bid/ask sizes.
///
/// Returns (bid_size, ask_size) adjusted for inventory.
/// When long, reduce bid size and increase ask size to reduce inventory.
///
/// - `base_size`: starting size for both sides
/// - `inventory`: net inventory (positive = long)
/// - `max_position`: maximum allowed position
/// - `fill_rate`: estimated fill rate (0-1, higher = more aggressive sizing)
/// - `skew_factor`: how aggressively to skew sizes (0 = no skew, 1 = max skew)
#[pyfunction]
#[inline]
pub fn mm_size(
    base_size: f64,
    inventory: f64,
    max_position: f64,
    fill_rate: f64,
    skew_factor: f64,
) -> PyResult<(f64, f64)> {
    Ok(mm_size_impl(base_size, inventory, max_position, fill_rate, skew_factor))
}

/// Internal implementation (no tier check).
#[inline]
fn estimate_volatility_impl(prices: &[f64]) -> f64 {
    if prices.len() < 2 {
        return 0.0;
    }

    // Compute log returns, filtering out invalid values
    let mut returns = Vec::with_capacity(prices.len() - 1);
    for i in 1..prices.len() {
        let prev = prices[i - 1];
        let curr = prices[i];
        if prev > 0.0 && curr > 0.0 && prev.is_finite() && curr.is_finite() {
            returns.push((curr / prev).ln());
        }
    }

    if returns.is_empty() {
        return 0.0;
    }

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

    variance.sqrt()
}

/// Estimate volatility from a price series using log-return standard deviation.
///
/// Returns 0.0 if fewer than 2 prices or all prices are the same.
/// When `annualize` is true, multiplies by sqrt(365) for prediction market calendar.
#[pyfunction]
#[pyo3(signature = (prices, annualize=false))]
#[inline]
pub fn estimate_volatility(prices: Vec<f64>, annualize: bool) -> PyResult<f64> {
    let mut vol = estimate_volatility_impl(&prices);
    if annualize {
        vol *= ANNUALIZATION_FACTOR;
    }
    Ok(vol)
}

// ---------------------------------------------------------------------------
// Annualization constant: sqrt(365) for prediction markets (continuous calendar)
// ---------------------------------------------------------------------------

const ANNUALIZATION_FACTOR: f64 = 19.104973174542800; // sqrt(365)

// ---------------------------------------------------------------------------
// Parkinson volatility estimator (high/low range)
// ---------------------------------------------------------------------------

#[inline]
fn parkinson_vol_impl(highs: &[f64], lows: &[f64]) -> f64 {
    let n = highs.len().min(lows.len());
    if n == 0 {
        return 0.0;
    }

    let scale = 1.0 / (4.0 * n as f64 * core::f64::consts::LN_2);
    let mut sum = 0.0;
    for i in 0..n {
        let h = highs[i];
        let l = lows[i];
        if !h.is_finite() || !l.is_finite() || h <= 0.0 || l <= 0.0 || l > h {
            continue;
        }
        let log_hl = (h / l).ln();
        sum += log_hl * log_hl;
    }
    (scale * sum).sqrt()
}

/// Parkinson high/low range volatility estimator.
///
/// More efficient than close-to-close by using intra-period range.
/// Returns 0.0 on empty/invalid input.
#[pyfunction]
#[pyo3(signature = (highs, lows, annualize=true))]
#[inline]
pub fn parkinson_vol(highs: Vec<f64>, lows: Vec<f64>, annualize: bool) -> PyResult<f64> {
    let mut vol = parkinson_vol_impl(&highs, &lows);
    if annualize {
        vol *= ANNUALIZATION_FACTOR;
    }
    Ok(vol)
}

// ---------------------------------------------------------------------------
// Garman-Klass volatility estimator (OHLC)
// ---------------------------------------------------------------------------

#[inline]
fn garman_klass_vol_impl(opens: &[f64], highs: &[f64], lows: &[f64], closes: &[f64]) -> f64 {
    let n = opens.len().min(highs.len()).min(lows.len()).min(closes.len());
    if n == 0 {
        return 0.0;
    }

    let mut sum = 0.0;
    let mut valid = 0usize;
    for i in 0..n {
        let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
        if !o.is_finite() || !h.is_finite() || !l.is_finite() || !c.is_finite()
            || o <= 0.0 || h <= 0.0 || l <= 0.0 || c <= 0.0
        {
            continue;
        }
        let log_hl = (h / l).ln();
        let log_co = (c / o).ln();
        sum += 0.5 * log_hl * log_hl - (2.0 * core::f64::consts::LN_2 - 1.0) * log_co * log_co;
        valid += 1;
    }

    if valid == 0 {
        return 0.0;
    }
    (sum / valid as f64).max(0.0).sqrt()
}

/// Garman-Klass OHLC volatility estimator.
///
/// The most efficient standard estimator using open, high, low, close data.
/// Returns 0.0 on empty/invalid input.
#[pyfunction]
#[pyo3(signature = (opens, highs, lows, closes, annualize=true))]
#[inline]
pub fn garman_klass_vol(
    opens: Vec<f64>,
    highs: Vec<f64>,
    lows: Vec<f64>,
    closes: Vec<f64>,
    annualize: bool,
) -> PyResult<f64> {
    let mut vol = garman_klass_vol_impl(&opens, &highs, &lows, &closes);
    if annualize {
        vol *= ANNUALIZATION_FACTOR;
    }
    Ok(vol)
}

// ---------------------------------------------------------------------------
// Yang-Zhang volatility estimator (overnight jump + Rogers-Satchell)
// ---------------------------------------------------------------------------

#[inline]
fn yang_zhang_vol_impl(opens: &[f64], highs: &[f64], lows: &[f64], closes: &[f64]) -> f64 {
    let n = opens.len().min(highs.len()).min(lows.len()).min(closes.len());
    if n < 2 {
        return 0.0;
    }

    // Overnight returns: ln(open_i / close_{i-1})
    let mut overnight_returns = Vec::with_capacity(n - 1);
    // Open-to-close returns: ln(close_i / open_i)
    let mut oc_returns = Vec::with_capacity(n - 1);
    // Rogers-Satchell component
    let mut rs_sum = 0.0;
    let mut rs_count = 0usize;

    for i in 1..n {
        let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
        let prev_c = closes[i - 1];

        if !o.is_finite() || !h.is_finite() || !l.is_finite() || !c.is_finite()
            || !prev_c.is_finite()
            || o <= 0.0 || h <= 0.0 || l <= 0.0 || c <= 0.0 || prev_c <= 0.0
        {
            continue;
        }

        overnight_returns.push((o / prev_c).ln());
        oc_returns.push((c / o).ln());

        // Rogers-Satchell: ln(h/c)*ln(h/o) + ln(l/c)*ln(l/o)
        let log_hc = (h / c).ln();
        let log_ho = (h / o).ln();
        let log_lc = (l / c).ln();
        let log_lo = (l / o).ln();
        rs_sum += log_hc * log_ho + log_lc * log_lo;
        rs_count += 1;
    }

    if overnight_returns.is_empty() || rs_count == 0 {
        return 0.0;
    }

    let nn = overnight_returns.len() as f64;
    // Use population variance (nn) when only 1 sample, sample variance (nn-1) otherwise
    let var_denom = if nn > 1.0 { nn - 1.0 } else { nn };

    // Variance of overnight returns
    let on_mean = overnight_returns.iter().sum::<f64>() / nn;
    let sigma_open = overnight_returns.iter().map(|r| (r - on_mean).powi(2)).sum::<f64>() / var_denom;

    // Variance of open-to-close returns
    let oc_mean = oc_returns.iter().sum::<f64>() / nn;
    let sigma_close = oc_returns.iter().map(|r| (r - oc_mean).powi(2)).sum::<f64>() / var_denom;

    // Rogers-Satchell variance
    let sigma_rs = rs_sum / rs_count as f64;

    // Yang-Zhang: weighted combination
    let k = 0.34 / (1.34 + (nn + 1.0) / (nn - 1.0));
    let variance = sigma_open + k * sigma_close + (1.0 - k) * sigma_rs;

    variance.max(0.0).sqrt()
}

/// Yang-Zhang volatility estimator.
///
/// Combines overnight jump, open-to-close, and Rogers-Satchell components.
/// Handles opening jumps — the most robust OHLC estimator.
/// Returns 0.0 on insufficient/invalid input (needs >= 2 bars).
#[pyfunction]
#[pyo3(signature = (opens, highs, lows, closes, annualize=true))]
#[inline]
pub fn yang_zhang_vol(
    opens: Vec<f64>,
    highs: Vec<f64>,
    lows: Vec<f64>,
    closes: Vec<f64>,
    annualize: bool,
) -> PyResult<f64> {
    let mut vol = yang_zhang_vol_impl(&opens, &highs, &lows, &closes);
    if annualize {
        vol *= ANNUALIZATION_FACTOR;
    }
    Ok(vol)
}

// ---------------------------------------------------------------------------
// EWMA volatility estimator
// ---------------------------------------------------------------------------

#[inline]
fn ewma_vol_impl(prices: &[f64], span: usize) -> f64 {
    if prices.len() < 2 || span == 0 {
        return 0.0;
    }

    let alpha = 2.0 / (span as f64 + 1.0);

    // Compute log returns
    let mut returns = Vec::with_capacity(prices.len() - 1);
    for i in 1..prices.len() {
        let prev = prices[i - 1];
        let curr = prices[i];
        if prev > 0.0 && curr > 0.0 && prev.is_finite() && curr.is_finite() {
            returns.push((curr / prev).ln());
        }
    }

    if returns.is_empty() {
        return 0.0;
    }

    // Seed EWMA variance with first squared return
    let mut ewma_var = returns[0] * returns[0];
    for r in &returns[1..] {
        ewma_var = alpha * r * r + (1.0 - alpha) * ewma_var;
    }

    ewma_var.max(0.0).sqrt()
}

/// Exponentially weighted moving average (EWMA) volatility.
///
/// Reactive to recent price changes. Uses RiskMetrics-style EWMA of squared returns.
///
/// - `prices`: price series
/// - `span`: EWMA span (default 20, like a 20-period half-life)
/// - `annualize`: multiply by sqrt(365) for prediction markets
///
/// Returns 0.0 on insufficient/invalid input.
#[pyfunction]
#[pyo3(signature = (prices, span=20, annualize=true))]
#[inline]
pub fn ewma_vol(prices: Vec<f64>, span: usize, annualize: bool) -> PyResult<f64> {
    let mut vol = ewma_vol_impl(&prices, span);
    if annualize {
        vol *= ANNUALIZATION_FACTOR;
    }
    Ok(vol)
}

// ---------------------------------------------------------------------------
// Rolling volatility (windowed realized vol series)
// ---------------------------------------------------------------------------

#[inline]
fn rolling_vol_impl(prices: &[f64], window: usize) -> Vec<f64> {
    if prices.len() < 2 || window < 2 {
        return vec![];
    }

    // Compute all log returns first
    let mut returns = Vec::with_capacity(prices.len() - 1);
    for i in 1..prices.len() {
        let prev = prices[i - 1];
        let curr = prices[i];
        if prev > 0.0 && curr > 0.0 && prev.is_finite() && curr.is_finite() {
            returns.push((curr / prev).ln());
        } else {
            returns.push(0.0);
        }
    }

    if returns.len() < window {
        return vec![];
    }

    let mut result = Vec::with_capacity(returns.len() - window + 1);
    for start in 0..=(returns.len() - window) {
        let slice = &returns[start..start + window];
        let n = slice.len() as f64;
        let mean = slice.iter().sum::<f64>() / n;
        let var = slice.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n;
        result.push(var.sqrt());
    }
    result
}

/// Rolling realized volatility series.
///
/// Computes windowed standard deviation of log returns.
///
/// - `prices`: price series
/// - `window`: rolling window size (default 20)
/// - `annualize`: multiply each value by sqrt(365)
///
/// Returns empty vec on insufficient input.
#[pyfunction]
#[pyo3(signature = (prices, window=20, annualize=true))]
#[inline]
pub fn rolling_vol(prices: Vec<f64>, window: usize, annualize: bool) -> PyResult<Vec<f64>> {
    let mut vols = rolling_vol_impl(&prices, window);
    if annualize {
        for v in &mut vols {
            *v *= ANNUALIZATION_FACTOR;
        }
    }
    Ok(vols)
}

// ---------------------------------------------------------------------------
// Toxicity-adjusted spread (VPIN → spread wiring)
// ---------------------------------------------------------------------------

#[inline]
fn toxicity_adjusted_spread_impl(base_spread: f64, vpin: f64, sensitivity: f64) -> f64 {
    if !base_spread.is_finite() || !vpin.is_finite() || !sensitivity.is_finite() {
        return base_spread.clamp(0.0, 1.0);
    }
    let vpin_c = vpin.clamp(0.0, 1.0);
    let sens_c = sensitivity.clamp(0.0, 10.0);
    let adjusted = base_spread * (1.0 + sens_c * vpin_c);
    adjusted.clamp(0.0, 1.0)
}

/// Adjust spread based on VPIN toxicity signal.
///
/// - `base_spread`: baseline spread (0..1)
/// - `vpin`: VPIN value from VpinDetector (0..1)
/// - `sensitivity`: how aggressively to widen (0..10, default 1.0)
///
/// Returns `base_spread * (1 + sensitivity * vpin)`, clamped to [0, 1].
#[pyfunction]
#[pyo3(signature = (base_spread, vpin, sensitivity=1.0))]
#[inline]
pub fn toxicity_adjusted_spread(base_spread: f64, vpin: f64, sensitivity: f64) -> f64 {
    toxicity_adjusted_spread_impl(base_spread, vpin, sensitivity)
}

/// Register all market making functions in the Python module.
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(reservation_price, m)?)?;
    m.add_function(wrap_pyfunction!(optimal_spread, m)?)?;
    m.add_function(wrap_pyfunction!(competitive_spread, m)?)?;
    m.add_function(wrap_pyfunction!(mm_size, m)?)?;
    m.add_function(wrap_pyfunction!(estimate_volatility, m)?)?;
    m.add_function(wrap_pyfunction!(parkinson_vol, m)?)?;
    m.add_function(wrap_pyfunction!(garman_klass_vol, m)?)?;
    m.add_function(wrap_pyfunction!(yang_zhang_vol, m)?)?;
    m.add_function(wrap_pyfunction!(ewma_vol, m)?)?;
    m.add_function(wrap_pyfunction!(rolling_vol, m)?)?;
    m.add_function(wrap_pyfunction!(toxicity_adjusted_spread, m)?)?;
    Ok(())
}

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

    #[test]
    fn reservation_price_no_inventory() {
        let r = reservation_price_impl(0.50, 0.0, 0.5, 0.02, 1.0);
        assert!((r - 0.50).abs() < 1e-10);
    }

    #[test]
    fn reservation_price_long_inventory_skews_down() {
        let r = reservation_price_impl(0.50, 10.0, 0.5, 0.02, 1.0);
        assert!(r < 0.50);
    }

    #[test]
    fn reservation_price_short_inventory_skews_up() {
        let r = reservation_price_impl(0.50, -10.0, 0.5, 0.02, 1.0);
        assert!(r > 0.50);
    }

    #[test]
    fn reservation_price_nan_returns_mid() {
        let r = reservation_price_impl(0.50, f64::NAN, 0.5, 0.02, 1.0);
        assert!((r - 0.50).abs() < 1e-10);
    }

    #[test]
    fn optimal_spread_positive() {
        let s = optimal_spread_impl(0.02, 10.0, 0.5, 1.5, 1.0);
        assert!(s > 0.0);
    }

    #[test]
    fn optimal_spread_zero_gamma() {
        assert_eq!(optimal_spread_impl(0.02, 10.0, 0.0, 1.5, 1.0), 0.0);
    }

    #[test]
    fn optimal_spread_zero_kappa() {
        assert_eq!(optimal_spread_impl(0.02, 10.0, 0.5, 0.0, 1.0), 0.0);
    }

    #[test]
    fn optimal_spread_nan() {
        assert_eq!(optimal_spread_impl(f64::NAN, 10.0, 0.5, 1.5, 1.0), 0.0);
    }

    #[test]
    fn competitive_spread_pure_model() {
        let s = competitive_spread_impl(0.04, 0.02, 0.0, 0.0);
        assert!((s - 0.04).abs() < 1e-10);
    }

    #[test]
    fn competitive_spread_pure_book() {
        let s = competitive_spread_impl(0.04, 0.02, 0.0, 1.0);
        assert!((s - 0.02).abs() < 1e-10);
    }

    #[test]
    fn competitive_spread_clamped() {
        let s = competitive_spread_impl(0.0001, 0.0001, 0.0, 0.5);
        assert!(s >= 0.001);
    }

    #[test]
    fn mm_size_no_inventory() {
        let (bid, ask) = mm_size_impl(5.0, 0.0, 100.0, 0.3, 0.5);
        // With zero inventory, both sides should be equal
        assert!((bid - ask).abs() < 1e-10);
        assert!(bid > 0.0);
    }

    #[test]
    fn mm_size_long_inventory_skews() {
        let (bid, ask) = mm_size_impl(5.0, 50.0, 100.0, 0.3, 0.5);
        // Long inventory -> bid_size < ask_size (encourage selling)
        assert!(bid < ask);
    }

    #[test]
    fn mm_size_short_inventory_skews() {
        let (bid, ask) = mm_size_impl(5.0, -50.0, 100.0, 0.3, 0.5);
        // Short inventory -> bid_size > ask_size (encourage buying)
        assert!(bid > ask);
    }

    #[test]
    fn mm_size_zero_base() {
        let (bid, ask) = mm_size_impl(0.0, 10.0, 100.0, 0.3, 0.5);
        assert_eq!(bid, 0.0);
        assert_eq!(ask, 0.0);
    }

    #[test]
    fn mm_size_nan() {
        let (bid, ask) = mm_size_impl(f64::NAN, 10.0, 100.0, 0.3, 0.5);
        assert_eq!(bid, 0.0);
        assert_eq!(ask, 0.0);
    }

    #[test]
    fn estimate_volatility_constant() {
        assert_eq!(estimate_volatility_impl(&[1.0, 1.0, 1.0, 1.0]), 0.0);
    }

    #[test]
    fn estimate_volatility_positive() {
        let vol = estimate_volatility_impl(&[1.0, 1.01, 0.99, 1.02, 0.98]);
        assert!(vol > 0.0);
    }

    #[test]
    fn estimate_volatility_too_few() {
        assert_eq!(estimate_volatility_impl(&[1.0]), 0.0);
        assert_eq!(estimate_volatility_impl(&[]), 0.0);
    }

    #[test]
    fn estimate_volatility_filters_invalid() {
        let vol = estimate_volatility_impl(&[1.0, 0.0, 1.0]); // 0.0 makes ln undefined
        // Should skip the 0.0 -> 1.0 pair and the 1.0 -> 0.0 pair
        assert_eq!(vol, 0.0); // no valid returns
    }

    #[test]
    fn reservation_price_formula() {
        // r = 0.50 - 10 * 0.5 * 0.02^2 * 1.0 = 0.50 - 0.002 = 0.498
        let r = reservation_price_impl(0.50, 10.0, 0.5, 0.02, 1.0);
        assert!((r - 0.498).abs() < 1e-10);
    }

    #[test]
    fn optimal_spread_formula() {
        // gamma=0.5, sigma=0.02, T=1.0, kappa=1.5
        // variance_component = 0.5 * 0.0004 * 1.0 = 0.0002
        // arrival_component = (2/0.5) * ln(1 + 0.5/1.5) = 4 * ln(1.333..) ~ 4 * 0.28768 ~ 1.15073
        let s = optimal_spread_impl(0.02, 10.0, 0.5, 1.5, 1.0);
        let expected = 0.0002 + 4.0 * (1.0 + 0.5 / 1.5_f64).ln();
        assert!((s - expected).abs() < 1e-6);
    }

    // ---- Parkinson vol ----

    #[test]
    fn parkinson_vol_basic() {
        let highs = vec![1.05, 1.06, 1.04];
        let lows = vec![0.95, 0.94, 0.96];
        let vol = parkinson_vol_impl(&highs, &lows);
        assert!(vol > 0.0);
    }

    #[test]
    fn parkinson_vol_empty() {
        assert_eq!(parkinson_vol_impl(&[], &[]), 0.0);
    }

    #[test]
    fn parkinson_vol_equal_hl() {
        let highs = vec![1.0, 1.0];
        let lows = vec![1.0, 1.0];
        assert_eq!(parkinson_vol_impl(&highs, &lows), 0.0);
    }

    // ---- Garman-Klass vol ----

    #[test]
    fn garman_klass_vol_basic() {
        let opens = vec![1.00, 1.01, 0.99];
        let highs = vec![1.05, 1.06, 1.04];
        let lows = vec![0.95, 0.94, 0.96];
        let closes = vec![1.02, 0.98, 1.01];
        let vol = garman_klass_vol_impl(&opens, &highs, &lows, &closes);
        assert!(vol > 0.0);
    }

    #[test]
    fn garman_klass_vol_empty() {
        assert_eq!(garman_klass_vol_impl(&[], &[], &[], &[]), 0.0);
    }

    // ---- Yang-Zhang vol ----

    #[test]
    fn yang_zhang_vol_basic() {
        let opens = vec![1.00, 1.01, 0.99, 1.02];
        let highs = vec![1.05, 1.06, 1.04, 1.07];
        let lows = vec![0.95, 0.94, 0.96, 0.93];
        let closes = vec![1.02, 0.98, 1.01, 1.03];
        let vol = yang_zhang_vol_impl(&opens, &highs, &lows, &closes);
        assert!(vol > 0.0);
    }

    #[test]
    fn yang_zhang_vol_too_few() {
        assert_eq!(yang_zhang_vol_impl(&[1.0], &[1.1], &[0.9], &[1.0]), 0.0);
    }

    // ---- EWMA vol ----

    #[test]
    fn ewma_vol_basic() {
        let prices = vec![1.0, 1.01, 0.99, 1.02, 0.98, 1.03];
        let vol = ewma_vol_impl(&prices, 5);
        assert!(vol > 0.0);
    }

    #[test]
    fn ewma_vol_constant() {
        let prices = vec![1.0, 1.0, 1.0, 1.0];
        assert_eq!(ewma_vol_impl(&prices, 3), 0.0);
    }

    #[test]
    fn ewma_vol_too_few() {
        assert_eq!(ewma_vol_impl(&[1.0], 5), 0.0);
    }

    #[test]
    fn ewma_vol_zero_span() {
        assert_eq!(ewma_vol_impl(&[1.0, 2.0], 0), 0.0);
    }

    // ---- Rolling vol ----

    #[test]
    fn rolling_vol_basic() {
        let prices = vec![1.0, 1.01, 0.99, 1.02, 0.98, 1.03, 0.97];
        let vols = rolling_vol_impl(&prices, 3);
        assert!(!vols.is_empty());
        for v in &vols {
            assert!(*v >= 0.0);
        }
    }

    #[test]
    fn rolling_vol_too_few() {
        assert!(rolling_vol_impl(&[1.0, 1.01], 5).is_empty());
    }

    #[test]
    fn rolling_vol_constant() {
        let prices = vec![1.0, 1.0, 1.0, 1.0, 1.0];
        let vols = rolling_vol_impl(&prices, 3);
        for v in &vols {
            assert!(v.abs() < 1e-15);
        }
    }

    // ---- Toxicity-adjusted spread ----

    #[test]
    fn toxicity_adjusted_no_toxicity() {
        assert!((toxicity_adjusted_spread_impl(0.04, 0.0, 1.0) - 0.04).abs() < 1e-10);
    }

    #[test]
    fn toxicity_adjusted_full_toxicity() {
        // 0.04 * (1 + 1.0 * 1.0) = 0.08
        assert!((toxicity_adjusted_spread_impl(0.04, 1.0, 1.0) - 0.08).abs() < 1e-10);
    }

    #[test]
    fn toxicity_adjusted_clamped() {
        assert!(toxicity_adjusted_spread_impl(0.9, 1.0, 10.0) <= 1.0);
    }
}
