//! Stock market hedging module.
//!
//! Provides hedge ratio calculation, effectiveness metrics, multi-ticker optimization,
//! scenario analysis, cost tracking, and Greeks-style sensitivities for hedging
//! prediction market positions with correlated stock positions.

use pyo3::prelude::*;

/// Guard: return 0.0 if value is NaN or infinite.
#[inline]
fn finite_or_zero(v: f64) -> f64 {
    if v.is_finite() { v } else { 0.0 }
}

/// Compute log returns from a price series.
fn log_returns(prices: &[f64]) -> Vec<f64> {
    if prices.len() < 2 {
        return Vec::new();
    }
    prices
        .windows(2)
        .map(|w| {
            if w[0] > 0.0 && w[1] > 0.0 {
                (w[1] / w[0]).ln()
            } else {
                0.0
            }
        })
        .collect()
}

/// Compute the mean of a slice.
fn mean(xs: &[f64]) -> f64 {
    if xs.is_empty() {
        return 0.0;
    }
    xs.iter().sum::<f64>() / xs.len() as f64
}

/// Compute the variance of a slice.
fn variance(xs: &[f64]) -> f64 {
    if xs.len() < 2 {
        return 0.0;
    }
    let m = mean(xs);
    xs.iter().map(|&x| (x - m).powi(2)).sum::<f64>() / (xs.len() - 1) as f64
}

/// Compute the covariance of two slices.
fn covariance(xs: &[f64], ys: &[f64]) -> f64 {
    let n = xs.len().min(ys.len());
    if n < 2 {
        return 0.0;
    }
    let mx = mean(&xs[..n]);
    let my = mean(&ys[..n]);
    xs[..n]
        .iter()
        .zip(ys[..n].iter())
        .map(|(&x, &y)| (x - mx) * (y - my))
        .sum::<f64>()
        / (n - 1) as f64
}

/// Compute the standard deviation of a slice.
fn std_dev(xs: &[f64]) -> f64 {
    variance(xs).sqrt()
}

/// OLS hedge ratio: h* = Cov(dS, dF) / Var(dF)
///
/// Uses rolling returns computed from the last `window` prices.
/// The minimum-variance hedge ratio minimizes the variance of the hedged portfolio.
#[pyfunction]
#[pyo3(signature = (spot_prices, hedge_prices, window=60))]
fn hedge_ratio_ols(spot_prices: Vec<f64>, hedge_prices: Vec<f64>, window: usize) -> f64 {
    let spot_ret = log_returns(&spot_prices);
    let hedge_ret = log_returns(&hedge_prices);

    let n = spot_ret.len().min(hedge_ret.len());
    if n < 2 {
        return 0.0;
    }

    let w = window.min(n);
    let start = n - w;
    let sr = &spot_ret[start..n];
    let hr = &hedge_ret[start..n];

    let cov = covariance(sr, hr);
    let var_h = variance(hr);

    if var_h < 1e-15 {
        return 0.0;
    }

    finite_or_zero(cov / var_h)
}

/// Rolling Pearson correlation between two series.
///
/// Returns a vector of correlations, one for each point where at least `window` values exist.
#[pyfunction]
#[pyo3(signature = (series_a, series_b, window=60))]
fn rolling_correlation(series_a: Vec<f64>, series_b: Vec<f64>, window: usize) -> Vec<f64> {
    let n = series_a.len().min(series_b.len());
    let w = window.max(2);
    if n < w {
        return Vec::new();
    }

    let mut result = Vec::with_capacity(n - w + 1);
    for i in 0..=(n - w) {
        let a = &series_a[i..i + w];
        let b = &series_b[i..i + w];
        let cov = covariance(a, b);
        let sa = std_dev(a);
        let sb = std_dev(b);
        let denom = sa * sb;
        if denom < 1e-15 {
            result.push(0.0);
        } else {
            result.push(finite_or_zero(cov / denom));
        }
    }

    result
}

/// Hedge effectiveness: HE = 1 - Var(hedged) / Var(unhedged)
///
/// Where hedged_return = spot_return - h * hedge_return.
/// A value close to 1.0 means the hedge explains most of the spot variance.
#[pyfunction]
fn hedge_effectiveness(
    spot_returns: Vec<f64>,
    hedge_returns: Vec<f64>,
    hedge_ratio: f64,
) -> f64 {
    let n = spot_returns.len().min(hedge_returns.len());
    if n < 2 {
        return 0.0;
    }

    let hedged: Vec<f64> = (0..n)
        .map(|i| spot_returns[i] - hedge_ratio * hedge_returns[i])
        .collect();

    let var_spot = variance(&spot_returns[..n]);
    let var_hedged = variance(&hedged);

    if var_spot < 1e-15 {
        return 0.0;
    }

    finite_or_zero(1.0 - var_hedged / var_spot)
}

/// Basis risk: Var(spot_return - h * hedge_return)
///
/// The residual variance after hedging. Lower is better.
#[pyfunction]
fn basis_risk(
    spot_returns: Vec<f64>,
    hedge_returns: Vec<f64>,
    hedge_ratio: f64,
) -> f64 {
    let n = spot_returns.len().min(hedge_returns.len());
    if n < 2 {
        return 0.0;
    }

    let hedged: Vec<f64> = (0..n)
        .map(|i| spot_returns[i] - hedge_ratio * hedge_returns[i])
        .collect();

    finite_or_zero(variance(&hedged))
}

/// Optimal hedge size given spot notional and hedge ratio.
///
/// Caps the result at `max_hedge_notional` if provided.
#[pyfunction]
#[pyo3(signature = (spot_notional, hedge_ratio, max_hedge_notional=f64::MAX))]
fn optimal_hedge_size(spot_notional: f64, hedge_ratio: f64, max_hedge_notional: f64) -> f64 {
    let raw = (spot_notional * hedge_ratio).abs();
    finite_or_zero(raw.min(max_hedge_notional.abs()))
}

/// Greeks-style sensitivities for a hedged position.
#[pyclass(frozen, get_all)]
#[derive(Clone, Debug)]
struct HedgeSensitivities {
    /// dV/dS — sensitivity of hedge PnL to spot price change
    delta: f64,
    /// d2V/dSdF — cross-gamma between spot and hedge
    cross_gamma: f64,
    /// OLS beta of hedge returns on spot returns
    beta: f64,
    /// Annualized tracking error: std(residuals) * sqrt(252)
    tracking_error: f64,
    /// How much the hedge ratio has drifted from its rolling average
    hedge_decay: f64,
}

#[pymethods]
impl HedgeSensitivities {
    fn __repr__(&self) -> String {
        format!(
            "HedgeSensitivities(delta={:.4}, cross_gamma={:.6}, beta={:.4}, tracking_error={:.4}, hedge_decay={:.4})",
            self.delta, self.cross_gamma, self.beta, self.tracking_error, self.hedge_decay
        )
    }
}

/// Compute Greeks-style hedge sensitivities from price series.
#[pyfunction]
#[pyo3(signature = (spot_prices, hedge_prices, position_size=1.0, window=60))]
fn compute_hedge_sensitivities(
    spot_prices: Vec<f64>,
    hedge_prices: Vec<f64>,
    position_size: f64,
    window: usize,
) -> HedgeSensitivities {
    let spot_ret = log_returns(&spot_prices);
    let hedge_ret = log_returns(&hedge_prices);
    let n = spot_ret.len().min(hedge_ret.len());

    if n < 3 {
        return HedgeSensitivities {
            delta: 0.0,
            cross_gamma: 0.0,
            beta: 0.0,
            tracking_error: 0.0,
            hedge_decay: 0.0,
        };
    }

    let w = window.min(n);
    let start = n - w;
    let sr = &spot_ret[start..n];
    let hr = &hedge_ret[start..n];

    // Beta (OLS slope of hedge on spot)
    let cov_sh = covariance(sr, hr);
    let var_s = variance(sr);
    let beta = if var_s > 1e-15 {
        finite_or_zero(cov_sh / var_s)
    } else {
        0.0
    };

    // Hedge ratio for residual calc
    let var_h = variance(hr);
    let h = if var_h > 1e-15 {
        finite_or_zero(cov_sh / var_h)
    } else {
        0.0
    };

    // Residuals for tracking error
    let residuals: Vec<f64> = (0..w).map(|i| sr[i] - h * hr[i]).collect();
    let tracking_error = finite_or_zero(std_dev(&residuals) * (252.0_f64).sqrt());

    // Delta: finite-difference estimate using last two prices
    let last_s = spot_prices.last().copied().unwrap_or(0.0);
    let prev_s = if spot_prices.len() >= 2 {
        spot_prices[spot_prices.len() - 2]
    } else {
        last_s
    };
    let last_h = hedge_prices.last().copied().unwrap_or(0.0);
    let prev_h = if hedge_prices.len() >= 2 {
        hedge_prices[hedge_prices.len() - 2]
    } else {
        last_h
    };
    let ds = last_s - prev_s;
    let dh = last_h - prev_h;
    let delta = if ds.abs() > 1e-12 {
        finite_or_zero(position_size * dh / ds)
    } else {
        0.0
    };

    // Cross-gamma: change in delta (finite second difference)
    let cross_gamma = if n >= 4 && spot_prices.len() >= 3 && hedge_prices.len() >= 3 {
        let ds2 = spot_prices[spot_prices.len() - 2] - spot_prices[spot_prices.len() - 3];
        let dh2 = hedge_prices[hedge_prices.len() - 2] - hedge_prices[hedge_prices.len() - 3];
        let delta_prev = if ds2.abs() > 1e-12 {
            position_size * dh2 / ds2
        } else {
            delta
        };
        finite_or_zero(delta - delta_prev)
    } else {
        0.0
    };

    // Hedge decay: drift of recent h from full-window h
    let hedge_decay = if w > 10 {
        let half = w / 2;
        let recent_sr = &sr[half..];
        let recent_hr = &hr[half..];
        let recent_cov = covariance(recent_sr, recent_hr);
        let recent_var = variance(recent_hr);
        let recent_h = if recent_var > 1e-15 {
            recent_cov / recent_var
        } else {
            0.0
        };
        finite_or_zero(recent_h - h)
    } else {
        0.0
    };

    HedgeSensitivities {
        delta,
        cross_gamma,
        beta,
        tracking_error,
        hedge_decay,
    }
}

/// Multi-ticker hedge optimization result.
#[pyclass(frozen, get_all)]
#[derive(Clone, Debug)]
struct MultiHedgeResult {
    /// Optimal weights for each hedge ticker
    weights: Vec<f64>,
    /// Ticker names corresponding to weights
    tickers: Vec<String>,
    /// Hedge effectiveness (R-squared)
    hedge_effectiveness: f64,
    /// Portfolio variance with hedge
    portfolio_variance: f64,
    /// Annualized tracking error
    tracking_error: f64,
}

#[pymethods]
impl MultiHedgeResult {
    fn __repr__(&self) -> String {
        let pairs: Vec<String> = self
            .tickers
            .iter()
            .zip(self.weights.iter())
            .map(|(t, w)| format!("{}={:.4}", t, w))
            .collect();
        format!(
            "MultiHedgeResult(weights=[{}], HE={:.4}, tracking_error={:.4})",
            pairs.join(", "),
            self.hedge_effectiveness,
            self.tracking_error,
        )
    }
}

/// Multi-ticker hedge optimization via OLS.
///
/// Solves w = Sigma_FF^{-1} * Sigma_FS using Cholesky decomposition
/// for the case of multiple hedge instruments.
#[pyfunction]
fn multi_ticker_hedge(
    spot_returns: Vec<f64>,
    hedge_returns_matrix: Vec<Vec<f64>>,
    tickers: Vec<String>,
) -> MultiHedgeResult {
    let k = hedge_returns_matrix.len(); // number of hedge tickers
    let n = spot_returns.len();

    if k == 0 || n < 3 || tickers.len() != k {
        return MultiHedgeResult {
            weights: vec![0.0; k],
            tickers,
            hedge_effectiveness: 0.0,
            portfolio_variance: 0.0,
            tracking_error: 0.0,
        };
    }

    // Ensure all hedge return series have the same length
    let min_len = hedge_returns_matrix
        .iter()
        .map(|r| r.len())
        .min()
        .unwrap_or(0)
        .min(n);

    if min_len < 3 {
        return MultiHedgeResult {
            weights: vec![0.0; k],
            tickers,
            hedge_effectiveness: 0.0,
            portfolio_variance: 0.0,
            tracking_error: 0.0,
        };
    }

    // Build covariance matrix Sigma_FF (k x k) and Sigma_FS (k x 1)
    let mut sigma_ff = vec![vec![0.0; k]; k];
    let mut sigma_fs = vec![0.0; k];

    for i in 0..k {
        sigma_fs[i] = covariance(&hedge_returns_matrix[i][..min_len], &spot_returns[..min_len]);
        for j in 0..k {
            sigma_ff[i][j] = covariance(
                &hedge_returns_matrix[i][..min_len],
                &hedge_returns_matrix[j][..min_len],
            );
        }
    }

    // Solve via Cholesky decomposition: L * L^T * w = sigma_fs
    let weights = cholesky_solve(&sigma_ff, &sigma_fs);

    // Compute hedged returns and effectiveness
    let hedged: Vec<f64> = (0..min_len)
        .map(|t| {
            let hedge_component: f64 = (0..k)
                .map(|i| weights[i] * hedge_returns_matrix[i][t])
                .sum();
            spot_returns[t] - hedge_component
        })
        .collect();

    let var_spot = variance(&spot_returns[..min_len]);
    let var_hedged = variance(&hedged);
    let he = if var_spot > 1e-15 {
        finite_or_zero(1.0 - var_hedged / var_spot)
    } else {
        0.0
    };

    let te = finite_or_zero(std_dev(&hedged) * (252.0_f64).sqrt());

    MultiHedgeResult {
        weights: weights.iter().map(|&w| finite_or_zero(w)).collect(),
        tickers,
        hedge_effectiveness: he,
        portfolio_variance: finite_or_zero(var_hedged),
        tracking_error: te,
    }
}

/// Cholesky decomposition solve: A * x = b where A is symmetric positive definite.
/// Falls back to diagonal solve if decomposition fails.
fn cholesky_solve(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
    let n = a.len();
    if n == 0 {
        return Vec::new();
    }

    // Build lower triangular L such that A = L * L^T
    let mut l = vec![vec![0.0; n]; n];

    for i in 0..n {
        for j in 0..=i {
            let mut sum = 0.0;
            for k in 0..j {
                sum += l[i][k] * l[j][k];
            }
            if i == j {
                let diag = a[i][i] - sum;
                if diag <= 0.0 {
                    // Not positive definite — fallback to diagonal
                    return diagonal_solve(a, b);
                }
                l[i][j] = diag.sqrt();
            } else {
                if l[j][j].abs() < 1e-15 {
                    return diagonal_solve(a, b);
                }
                l[i][j] = (a[i][j] - sum) / l[j][j];
            }
        }
    }

    // Forward substitution: L * y = b
    let mut y = vec![0.0; n];
    for i in 0..n {
        let mut sum = 0.0;
        for j in 0..i {
            sum += l[i][j] * y[j];
        }
        if l[i][i].abs() < 1e-15 {
            y[i] = 0.0;
        } else {
            y[i] = (b[i] - sum) / l[i][i];
        }
    }

    // Back substitution: L^T * x = y
    let mut x = vec![0.0; n];
    for i in (0..n).rev() {
        let mut sum = 0.0;
        for j in (i + 1)..n {
            sum += l[j][i] * x[j];
        }
        if l[i][i].abs() < 1e-15 {
            x[i] = 0.0;
        } else {
            x[i] = (y[i] - sum) / l[i][i];
        }
    }

    x
}

/// Diagonal fallback solver when Cholesky fails.
fn diagonal_solve(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
    let n = a.len();
    (0..n)
        .map(|i| {
            if a[i][i].abs() > 1e-15 {
                finite_or_zero(b[i] / a[i][i])
            } else {
                0.0
            }
        })
        .collect()
}

/// Scenario analysis outcome.
#[pyclass(frozen, get_all)]
#[derive(Clone, Debug)]
struct ScenarioOutcome {
    /// Spot price move percentage
    spot_move_pct: f64,
    /// Expected hedge instrument move percentage
    hedge_move_pct: f64,
    /// Portfolio P&L (spot + hedge)
    portfolio_pnl: f64,
    /// Unhedged P&L (spot only)
    unhedged_pnl: f64,
    /// Benefit of hedging (unhedged_pnl - portfolio_pnl when adverse)
    hedge_benefit: f64,
}

#[pymethods]
impl ScenarioOutcome {
    fn __repr__(&self) -> String {
        format!(
            "ScenarioOutcome(spot={:+.2}%, hedge={:+.2}%, portfolio_pnl={:.2}, unhedged_pnl={:.2}, benefit={:.2})",
            self.spot_move_pct * 100.0,
            self.hedge_move_pct * 100.0,
            self.portfolio_pnl,
            self.unhedged_pnl,
            self.hedge_benefit,
        )
    }
}

/// Run a scenario analysis on a hedged position.
///
/// Models the hedge instrument's expected move given a spot move,
/// accounting for correlation and relative volatilities.
#[pyfunction]
#[pyo3(signature = (spot_position, spot_price, hedge_position, hedge_price, spot_move_pct, correlation=0.8, hedge_vol=0.2, spot_vol=0.2))]
fn hedge_scenario(
    spot_position: f64,
    spot_price: f64,
    hedge_position: f64,
    hedge_price: f64,
    spot_move_pct: f64,
    correlation: f64,
    hedge_vol: f64,
    spot_vol: f64,
) -> ScenarioOutcome {
    // Expected hedge move: corr * (hedge_vol / spot_vol) * spot_move
    let vol_ratio = if spot_vol.abs() > 1e-15 {
        hedge_vol / spot_vol
    } else {
        1.0
    };
    let hedge_move_pct = finite_or_zero(correlation * vol_ratio * spot_move_pct);

    let spot_pnl = spot_position * spot_price * spot_move_pct;
    let hedge_pnl = hedge_position * hedge_price * hedge_move_pct;
    let portfolio_pnl = finite_or_zero(spot_pnl + hedge_pnl);
    let unhedged_pnl = finite_or_zero(spot_pnl);
    let hedge_benefit = finite_or_zero(unhedged_pnl.abs() - portfolio_pnl.abs());

    ScenarioOutcome {
        spot_move_pct: finite_or_zero(spot_move_pct),
        hedge_move_pct,
        portfolio_pnl,
        unhedged_pnl,
        hedge_benefit,
    }
}

/// Tracks cumulative hedge trading costs for rebalancing analysis.
#[pyclass]
#[derive(Clone, Debug)]
struct HedgeCostTracker {
    total_commission: f64,
    total_slippage: f64,
    total_notional: f64,
    trade_count: u64,
}

#[pymethods]
impl HedgeCostTracker {
    #[new]
    fn new() -> Self {
        Self {
            total_commission: 0.0,
            total_slippage: 0.0,
            total_notional: 0.0,
            trade_count: 0,
        }
    }

    /// Record a trade's cost components.
    fn record_trade(&mut self, notional: f64, commission: f64, slippage: f64) {
        self.total_notional += notional.abs();
        self.total_commission += commission.abs();
        self.total_slippage += slippage.abs();
        self.trade_count += 1;
    }

    /// Total cost (commission + slippage).
    fn total_cost(&self) -> f64 {
        finite_or_zero(self.total_commission + self.total_slippage)
    }

    /// Total cost as a percentage of total notional traded.
    fn cost_as_pct_of_notional(&self) -> f64 {
        if self.total_notional < 1e-15 {
            return 0.0;
        }
        finite_or_zero(self.total_cost() / self.total_notional * 100.0)
    }

    /// Average cost per rebalance trade.
    fn avg_cost_per_rebalance(&self) -> f64 {
        if self.trade_count == 0 {
            return 0.0;
        }
        finite_or_zero(self.total_cost() / self.trade_count as f64)
    }

    fn __repr__(&self) -> String {
        format!(
            "HedgeCostTracker(trades={}, total_cost={:.4}, cost_pct={:.4}%)",
            self.trade_count,
            self.total_cost(),
            self.cost_as_pct_of_notional(),
        )
    }
}

/// Register hedge module functions and classes with the Python module.
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(hedge_ratio_ols, m)?)?;
    m.add_function(wrap_pyfunction!(rolling_correlation, m)?)?;
    m.add_function(wrap_pyfunction!(hedge_effectiveness, m)?)?;
    m.add_function(wrap_pyfunction!(basis_risk, m)?)?;
    m.add_function(wrap_pyfunction!(optimal_hedge_size, m)?)?;
    m.add_function(wrap_pyfunction!(compute_hedge_sensitivities, m)?)?;
    m.add_function(wrap_pyfunction!(multi_ticker_hedge, m)?)?;
    m.add_function(wrap_pyfunction!(hedge_scenario, m)?)?;
    m.add_class::<HedgeSensitivities>()?;
    m.add_class::<MultiHedgeResult>()?;
    m.add_class::<ScenarioOutcome>()?;
    m.add_class::<HedgeCostTracker>()?;
    Ok(())
}

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

    #[test]
    fn test_log_returns_basic() {
        let prices = vec![100.0, 110.0, 121.0];
        let rets = log_returns(&prices);
        assert_eq!(rets.len(), 2);
        assert!((rets[0] - (1.1_f64).ln()).abs() < 1e-10);
    }

    #[test]
    fn test_log_returns_empty() {
        assert!(log_returns(&[]).is_empty());
        assert!(log_returns(&[100.0]).is_empty());
    }

    #[test]
    fn test_hedge_ratio_perfect_correlation() {
        // If hedge prices move exactly 2x spot prices, ratio should be ~0.5
        let spot: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64) * 0.1).collect();
        let hedge: Vec<f64> = (0..100).map(|i| 100.0 + (i as f64) * 0.2).collect();
        let h = hedge_ratio_ols(spot, hedge, 60);
        assert!(h > 0.3 && h < 0.7, "hedge ratio was {}", h);
    }

    #[test]
    fn test_hedge_ratio_empty() {
        assert_eq!(hedge_ratio_ols(vec![], vec![], 60), 0.0);
        assert_eq!(hedge_ratio_ols(vec![1.0], vec![1.0], 60), 0.0);
    }

    #[test]
    fn test_rolling_correlation_length() {
        let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
        let b: Vec<f64> = (0..50).map(|i| (i as f64) * 2.0).collect();
        let corr = rolling_correlation(a, b, 10);
        assert_eq!(corr.len(), 41); // 50 - 10 + 1
    }

    #[test]
    fn test_rolling_correlation_perfect() {
        let a: Vec<f64> = (0..20).map(|i| i as f64).collect();
        let b: Vec<f64> = (0..20).map(|i| (i as f64) * 3.0 + 5.0).collect();
        let corr = rolling_correlation(a, b, 10);
        for c in &corr {
            assert!((*c - 1.0).abs() < 1e-10, "correlation was {}", c);
        }
    }

    #[test]
    fn test_hedge_effectiveness_perfect() {
        let spot = vec![0.01, -0.02, 0.015, -0.01, 0.03, -0.02, 0.01, -0.015, 0.02, -0.01];
        let hedge = spot.clone(); // perfect hedge
        let he = hedge_effectiveness(spot, hedge, 1.0);
        assert!((he - 1.0).abs() < 1e-10, "HE was {}", he);
    }

    #[test]
    fn test_hedge_effectiveness_no_hedge() {
        let spot = vec![0.01, -0.02, 0.015, -0.01];
        let hedge = vec![0.0, 0.0, 0.0, 0.0];
        let he = hedge_effectiveness(spot, hedge, 0.0);
        assert!((he - 0.0).abs() < 1e-10, "HE was {}", he);
    }

    #[test]
    fn test_basis_risk_zero_for_perfect_hedge() {
        let spot = vec![0.01, -0.02, 0.015, -0.01, 0.03];
        let hedge = spot.clone();
        let br = basis_risk(spot, hedge, 1.0);
        assert!(br < 1e-20, "basis risk was {}", br);
    }

    #[test]
    fn test_optimal_hedge_size_cap() {
        let size = optimal_hedge_size(10000.0, 0.8, 5000.0);
        assert!((size - 5000.0).abs() < 1e-10);
    }

    #[test]
    fn test_optimal_hedge_size_uncapped() {
        let size = optimal_hedge_size(10000.0, 0.5, f64::MAX);
        assert!((size - 5000.0).abs() < 1e-10);
    }

    #[test]
    fn test_scenario_basic() {
        let outcome = hedge_scenario(100.0, 50.0, -80.0, 100.0, -0.10, 0.8, 0.2, 0.2);
        // Spot PnL: 100 * 50 * (-0.10) = -500
        assert!((outcome.unhedged_pnl - (-500.0)).abs() < 1.0);
        // Hedge move: 0.8 * 1.0 * (-0.10) = -0.08
        assert!((outcome.hedge_move_pct - (-0.08)).abs() < 1e-10);
        // Hedge PnL: -80 * 100 * (-0.08) = +640
        // Portfolio: -500 + 640 = 140
        assert!((outcome.portfolio_pnl - 140.0).abs() < 1.0);
    }

    #[test]
    fn test_cost_tracker() {
        let mut ct = HedgeCostTracker::new();
        ct.record_trade(10000.0, 5.0, 2.0);
        ct.record_trade(20000.0, 10.0, 4.0);
        assert_eq!(ct.trade_count, 2);
        assert!((ct.total_cost() - 21.0).abs() < 1e-10);
        assert!((ct.cost_as_pct_of_notional() - 0.07).abs() < 1e-10);
        assert!((ct.avg_cost_per_rebalance() - 10.5).abs() < 1e-10);
    }

    #[test]
    fn test_cost_tracker_empty() {
        let ct = HedgeCostTracker::new();
        assert_eq!(ct.total_cost(), 0.0);
        assert_eq!(ct.cost_as_pct_of_notional(), 0.0);
        assert_eq!(ct.avg_cost_per_rebalance(), 0.0);
    }

    #[test]
    fn test_multi_hedge_single_ticker() {
        let spot = vec![0.01, -0.02, 0.015, -0.01, 0.03, -0.02, 0.01, -0.015, 0.02, -0.01];
        let hedge = vec![vec![0.01, -0.02, 0.015, -0.01, 0.03, -0.02, 0.01, -0.015, 0.02, -0.01]];
        let result = multi_ticker_hedge(spot, hedge, vec!["SPY".to_string()]);
        assert_eq!(result.weights.len(), 1);
        assert!((result.weights[0] - 1.0).abs() < 0.01);
        assert!(result.hedge_effectiveness > 0.99);
    }

    #[test]
    fn test_multi_hedge_empty() {
        let result = multi_ticker_hedge(vec![], vec![], vec![]);
        assert!(result.weights.is_empty());
        assert_eq!(result.hedge_effectiveness, 0.0);
    }

    #[test]
    fn test_sensitivities_basic() {
        let spot: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64) * 0.1).collect();
        let hedge: Vec<f64> = (0..100).map(|i| 100.0 + (i as f64) * 0.15).collect();
        let sens = compute_hedge_sensitivities(spot, hedge, 100.0, 60);
        // Should have nonzero beta and finite tracking error
        assert!(sens.beta != 0.0 || sens.tracking_error >= 0.0);
        assert!(sens.tracking_error.is_finite());
    }

    #[test]
    fn test_sensitivities_empty() {
        let sens = compute_hedge_sensitivities(vec![], vec![], 1.0, 60);
        assert_eq!(sens.delta, 0.0);
        assert_eq!(sens.beta, 0.0);
    }

    #[test]
    fn test_cholesky_2x2() {
        // [[4, 2], [2, 3]] * x = [8, 7]  -> x = [1, 5/3]
        let a = vec![vec![4.0, 2.0], vec![2.0, 3.0]];
        let b = vec![8.0, 7.0];
        let x = cholesky_solve(&a, &b);
        assert!((x[0] - 1.25).abs() < 1e-10);
        assert!((x[1] - 1.5).abs() < 1e-10);
    }

    #[test]
    fn test_finite_or_zero() {
        assert_eq!(finite_or_zero(1.5), 1.5);
        assert_eq!(finite_or_zero(f64::NAN), 0.0);
        assert_eq!(finite_or_zero(f64::INFINITY), 0.0);
        assert_eq!(finite_or_zero(f64::NEG_INFINITY), 0.0);
    }
}
