//! Orthogonal real-data audit tests for Hurst estimator benchmark conclusions.
//!
//! These tests validate the benchmark findings from independent perspectives
//! using real BTCUSDT/ETHUSDT/SOLUSDT aggTrade data, NOT synthetic fBm.
//!
//! Run: cargo test -p opendeviationbar-hurst --test real_data_audit -- --nocapture
//!
//! Audit dimensions:
//! 1. Cross-symbol consistency (same regime should produce similar H across symbols)
//! 2. Time-of-day regime variation (H should vary with market microstructure)
//! 3. Window size stability (estimator rankings should be stable across n=64..512)
//! 4. Return vs price input sensitivity (some estimators may be input-dependent)
//! 5. Outlier robustness (crypto has fat tails — how do estimators handle them?)
//! 6. Sliding window autocorrelation (do consecutive H values make sense?)
//! 7. Known regime detection (trending/consolidation periods in real data)
//! 8. Cross-estimator correlation matrix (which estimators agree?)

use opendeviationbar_hurst::{dfa, ghe_q1, ghe_q2, hurst_higuchi, hurst_wavelet, rssimple};

type EstFn = fn(&[f64]) -> f64;

const ESTIMATORS: &[(&str, EstFn)] = &[
    ("rs_simple", rssimple as EstFn),
    ("dfa", dfa as EstFn),
    ("wavelet", hurst_wavelet as EstFn),
    ("ghe_q1", ghe_q1 as EstFn),
    ("ghe_q2", ghe_q2 as EstFn),
    ("higuchi", hurst_higuchi as EstFn),
];

// ============================================================================
// Data loading
// ============================================================================

fn load_prices_no_header(filename: &str) -> Option<Vec<f64>> {
    let manifest = env!("CARGO_MANIFEST_DIR");
    // The hurst crate is at crates/opendeviationbar-hurst, fixtures at repo root
    let path = format!("{manifest}/../../tests/fixtures/{filename}");
    if !std::path::Path::new(&path).exists() {
        return None;
    }
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(false)
        .from_path(&path)
        .ok()?;
    let mut prices = Vec::with_capacity(900_000);
    for result in rdr.records() {
        if let Ok(record) = result {
            if let Some(price_str) = record.get(1) {
                if let Ok(price) = price_str.parse::<f64>() {
                    prices.push(price);
                }
            }
        }
    }
    Some(prices)
}

fn load_btcusdt_10k() -> Vec<f64> {
    let manifest = env!("CARGO_MANIFEST_DIR");
    let path = format!("{manifest}/../../tests/fixtures/BTCUSDT-aggTrades-sample-10k.csv");
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(true)
        .from_path(&path)
        .expect("10K fixture must exist");
    let mut prices = Vec::with_capacity(10_000);
    for result in rdr.records() {
        let record = result.expect("Bad CSV record");
        let price: f64 = record.get(1).unwrap().parse().unwrap();
        prices.push(price);
    }
    prices
}

fn require_real_data() -> Vec<(&'static str, Vec<f64>)> {
    let mut symbols = Vec::new();

    if let Some(btc) = load_prices_no_header("BTCUSDT-aggTrades-2024-01-01.csv") {
        symbols.push(("BTCUSDT", btc));
    } else {
        symbols.push(("BTCUSDT_10k", load_btcusdt_10k()));
    }

    if let Some(eth) = load_prices_no_header("ETHUSDT-aggTrades-2024-01-01.csv") {
        symbols.push(("ETHUSDT", eth));
    }
    if let Some(sol) = load_prices_no_header("SOLUSDT-aggTrades-2024-01-01.csv") {
        symbols.push(("SOLUSDT", sol));
    }
    symbols
}

// ============================================================================
// Audit 1: Cross-symbol consistency
// ============================================================================

/// If an estimator is good, its mean H across BTCUSDT, ETHUSDT, SOLUSDT
/// should be similar (same day, correlated markets). A bad estimator may
/// produce wildly different H values for the same macro regime.
#[test]
fn audit_cross_symbol_consistency() {
    let symbols = require_real_data();
    if symbols.len() < 2 {
        println!("SKIP: need ≥2 symbols for cross-symbol audit");
        return;
    }

    let win = 256;
    let n_windows = 200;

    println!("\n=== AUDIT 1: Cross-Symbol Consistency (n={win}, {n_windows} windows) ===");
    println!(
        "{:<12} {:>12} {:>12} {:>12} {:>12}",
        "Estimator", "Mean Range", "Max Diff", "CV", "Verdict"
    );

    for &(est_name, est_fn) in ESTIMATORS {
        let mut symbol_means: Vec<f64> = Vec::new();

        for (_sym_name, prices) in &symbols {
            if prices.len() < win * n_windows {
                continue;
            }
            let windows: Vec<&[f64]> = prices.windows(win).step_by(win).take(n_windows).collect();
            let mean_h: f64 = windows.iter().map(|w| est_fn(w)).sum::<f64>() / windows.len() as f64;
            symbol_means.push(mean_h);
        }

        if symbol_means.len() >= 2 {
            let global_mean =
                symbol_means.iter().sum::<f64>() / symbol_means.len() as f64;
            let min_val = symbol_means.iter().cloned().fold(f64::INFINITY, f64::min);
            let max_val = symbol_means.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
            let range = (max_val - min_val).abs();
            let max_diff = symbol_means
                .iter()
                .map(|&m| (m - global_mean).abs())
                .fold(0.0_f64, f64::max);
            let cv = if global_mean.abs() > 1e-10 {
                let std = (symbol_means
                    .iter()
                    .map(|&m| (m - global_mean) * (m - global_mean))
                    .sum::<f64>()
                    / symbol_means.len() as f64)
                    .sqrt();
                std / global_mean.abs()
            } else {
                0.0
            };

            let verdict = if cv < 0.10 { "STABLE" } else { "UNSTABLE" };
            println!(
                "{:<12} {:>12.4} {:>12.4} {:>12.4} {:>12}",
                est_name, range, max_diff, cv, verdict
            );
        }
    }
}

// ============================================================================
// Audit 2: Time-of-day regime variation
// ============================================================================

/// Real markets have microstructure regime shifts (Asian/European/US sessions).
/// A good estimator should show temporal variation (NOT constant). If an estimator
/// gives identical H for every hour, it's not capturing structure.
#[test]
fn audit_time_of_day_variation() {
    let symbols = require_real_data();

    println!("\n=== AUDIT 2: Time-of-Day H Variation (hourly blocks) ===");

    // Use BTCUSDT (most trades)
    let (sym_name, prices) = &symbols[0];
    let win = 256;

    // Divide the day into 24 hourly blocks (each ~36K trades for BTCUSDT)
    let trades_per_hour = prices.len() / 24;
    if trades_per_hour < win * 10 {
        println!("SKIP: not enough trades per hour");
        return;
    }

    println!("Symbol: {sym_name}, trades/hour: ~{trades_per_hour}");
    println!(
        "{:<12} {:>8} {:>8} {:>8} {:>12}",
        "Estimator", "Min H", "Max H", "Range", "Verdict"
    );

    for &(est_name, est_fn) in ESTIMATORS {
        let mut hourly_means = Vec::with_capacity(24);

        for hour in 0..24 {
            let start = hour * trades_per_hour;
            let end = ((hour + 1) * trades_per_hour).min(prices.len());
            let block = &prices[start..end];

            let windows: Vec<&[f64]> = block.windows(win).step_by(win).collect();
            if windows.is_empty() {
                continue;
            }
            let mean_h = windows.iter().map(|w| est_fn(w)).sum::<f64>() / windows.len() as f64;
            hourly_means.push(mean_h);
        }

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

        // A good estimator should show at least SOME variation (range > 0.01)
        // but not nonsensical variation (range < 0.5)
        let verdict = if range > 0.01 && range < 0.5 {
            "GOOD"
        } else if range <= 0.01 {
            "FLAT"
        } else {
            "NOISY"
        };

        println!(
            "{:<12} {:>8.4} {:>8.4} {:>8.4} {:>12}",
            est_name, min_h, max_h, range, verdict
        );
    }
}

// ============================================================================
// Audit 3: Window size stability of rankings
// ============================================================================

/// Our benchmark used n=256. Do the estimator rankings hold at n=64, 128, 512?
/// If Higuchi wins at n=256 but loses at n=64 (our min operational size), that's a problem.
#[test]
fn audit_window_size_ranking_stability() {
    let symbols = require_real_data();
    let (sym_name, prices) = &symbols[0];

    let window_sizes = [64, 128, 256, 512];
    let n_windows = 200;

    println!("\n=== AUDIT 3: Ranking Stability Across Window Sizes ({sym_name}) ===");

    for &win in &window_sizes {
        if prices.len() < win * n_windows {
            continue;
        }

        let windows: Vec<&[f64]> = prices.windows(win).step_by(win).take(n_windows).collect();

        // Compute std of H estimates (lower = more stable estimator)
        println!("\n  n={win}:");
        println!(
            "  {:<12} {:>8} {:>8} {:>8}",
            "Estimator", "Mean H", "Std H", "Range H"
        );

        for &(est_name, est_fn) in ESTIMATORS {
            let estimates: Vec<f64> = windows.iter().map(|w| est_fn(w)).collect();
            let mean = estimates.iter().sum::<f64>() / estimates.len() as f64;
            let std = (estimates.iter().map(|&h| (h - mean) * (h - mean)).sum::<f64>()
                / estimates.len() as f64)
                .sqrt();
            let min = estimates.iter().cloned().fold(f64::INFINITY, f64::min);
            let max = estimates
                .iter()
                .cloned()
                .fold(f64::NEG_INFINITY, f64::max);

            println!(
                "  {:<12} {:>8.4} {:>8.4} {:>8.4}",
                est_name,
                mean,
                std,
                max - min
            );
        }
    }
}

// ============================================================================
// Audit 4: Return vs price input sensitivity
// ============================================================================

/// Some estimators behave differently on prices vs log-returns.
/// Since our pipeline feeds raw prices, verify this doesn't bias results.
#[test]
fn audit_price_vs_return_sensitivity() {
    let symbols = require_real_data();
    let (_sym_name, prices) = &symbols[0];

    let win = 256;
    let n_windows = 100;

    println!("\n=== AUDIT 4: Price vs Return Input Sensitivity ===");
    println!(
        "{:<12} {:>10} {:>10} {:>10} {:>12}",
        "Estimator", "H(price)", "H(return)", "Delta", "Verdict"
    );

    // Extract matching windows of prices and returns
    let price_windows: Vec<&[f64]> = prices.windows(win).step_by(win).take(n_windows).collect();

    // Compute returns
    let returns: Vec<f64> = prices.windows(2).map(|w| (w[1] / w[0]).ln()).collect();
    let return_windows: Vec<&[f64]> = returns.windows(win).step_by(win).take(n_windows).collect();

    for &(est_name, est_fn) in ESTIMATORS {
        let h_price: f64 =
            price_windows.iter().map(|w| est_fn(w)).sum::<f64>() / n_windows as f64;
        let h_return: f64 =
            return_windows.iter().map(|w| est_fn(w)).sum::<f64>() / n_windows as f64;
        let delta = (h_price - h_return).abs();

        let verdict = if delta < 0.1 {
            "CONSISTENT"
        } else if delta < 0.3 {
            "MODERATE"
        } else {
            "SENSITIVE"
        };

        println!(
            "{:<12} {:>10.4} {:>10.4} {:>10.4} {:>12}",
            est_name, h_price, h_return, delta, verdict
        );
    }
}

// ============================================================================
// Audit 5: Outlier robustness (inject fat-tail spikes)
// ============================================================================

/// Crypto data has fat tails. Test: inject 5% extreme outliers into real windows
/// and measure how much H changes. Robust estimators should be relatively stable.
#[test]
fn audit_outlier_robustness() {
    let symbols = require_real_data();
    let (_sym_name, prices) = &symbols[0];

    let win = 256;
    let n_windows = 100;
    let outlier_fraction = 0.05; // 5% of points become outliers

    println!("\n=== AUDIT 5: Outlier Robustness (5% extreme spikes) ===");
    println!(
        "{:<12} {:>10} {:>10} {:>10} {:>12}",
        "Estimator", "H(clean)", "H(dirty)", "Delta", "Verdict"
    );

    let windows: Vec<Vec<f64>> = prices
        .windows(win)
        .step_by(win)
        .take(n_windows)
        .map(|w| w.to_vec())
        .collect();

    for &(est_name, est_fn) in ESTIMATORS {
        let mut h_clean_sum = 0.0;
        let mut h_dirty_sum = 0.0;

        for window in &windows {
            h_clean_sum += est_fn(window);

            // Inject outliers
            let mut dirty = window.clone();
            let n_outliers = (win as f64 * outlier_fraction) as usize;
            let mean = dirty.iter().sum::<f64>() / dirty.len() as f64;
            let std = (dirty
                .iter()
                .map(|&p| (p - mean) * (p - mean))
                .sum::<f64>()
                / dirty.len() as f64)
                .sqrt();

            let mut seed: u64 = 12345;
            for _ in 0..n_outliers {
                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
                let idx = (seed >> 33) as usize % dirty.len();
                // Add spike of ±5 standard deviations
                let direction = if (seed >> 32) & 1 == 0 { 1.0 } else { -1.0 };
                dirty[idx] = mean + direction * 5.0 * std;
            }

            h_dirty_sum += est_fn(&dirty);
        }

        let h_clean = h_clean_sum / n_windows as f64;
        let h_dirty = h_dirty_sum / n_windows as f64;
        let delta = (h_clean - h_dirty).abs();

        let verdict = if delta < 0.02 {
            "ROBUST"
        } else if delta < 0.05 {
            "MODERATE"
        } else {
            "FRAGILE"
        };

        println!(
            "{:<12} {:>10.4} {:>10.4} {:>10.4} {:>12}",
            est_name, h_clean, h_dirty, delta, verdict
        );
    }
}

// ============================================================================
// Audit 6: Sliding window autocorrelation
// ============================================================================

/// Consecutive H estimates should have positive autocorrelation (market regime
/// doesn't flip every 256 trades). If lag-1 autocorrelation is near zero,
/// the estimator may be dominated by noise.
#[test]
fn audit_sliding_window_autocorrelation() {
    let symbols = require_real_data();
    let (_sym_name, prices) = &symbols[0];

    let win = 256;
    let step = win / 2; // 50% overlap for smoother series
    let n_max = 500;

    println!("\n=== AUDIT 6: Sliding Window Autocorrelation (lag-1) ===");
    println!(
        "{:<12} {:>12} {:>12}",
        "Estimator", "Lag-1 ACF", "Verdict"
    );

    let windows: Vec<&[f64]> = prices.windows(win).step_by(step).take(n_max).collect();

    for &(est_name, est_fn) in ESTIMATORS {
        let h_series: Vec<f64> = windows.iter().map(|w| est_fn(w)).collect();
        let n = h_series.len();
        if n < 10 {
            continue;
        }

        let mean = h_series.iter().sum::<f64>() / n as f64;
        let var: f64 = h_series.iter().map(|&h| (h - mean) * (h - mean)).sum::<f64>() / n as f64;

        let acf1 = if var > 1e-15 {
            let cov: f64 = (0..n - 1)
                .map(|i| (h_series[i] - mean) * (h_series[i + 1] - mean))
                .sum::<f64>()
                / (n - 1) as f64;
            cov / var
        } else {
            0.0
        };

        // Good estimator: positive autocorrelation (regime persistence)
        let verdict = if acf1 > 0.5 {
            "SMOOTH"
        } else if acf1 > 0.2 {
            "OK"
        } else {
            "NOISY"
        };

        println!("{:<12} {:>12.4} {:>12}", est_name, acf1, verdict);
    }
}

// ============================================================================
// Audit 7: Known regime detection in real data
// ============================================================================

/// Find the most trending and most mean-reverting segments in real data
/// (by simple price direction ratio), then verify estimators rank them correctly.
#[test]
fn audit_known_regime_detection() {
    let symbols = require_real_data();
    let (_sym_name, prices) = &symbols[0];

    let win = 256;

    println!("\n=== AUDIT 7: Known Regime Detection (real data) ===");

    // Compute "trendiness" for each window: |net move| / sum(|moves|) = Kaufman ER
    let windows: Vec<&[f64]> = prices.windows(win).step_by(win / 4).collect();
    if windows.len() < 20 {
        println!("SKIP: not enough windows");
        return;
    }

    let mut scored: Vec<(f64, &[f64])> = windows
        .iter()
        .map(|&w| {
            let net = (w[w.len() - 1] - w[0]).abs();
            let volatility: f64 = w.windows(2).map(|p| (p[1] - p[0]).abs()).sum();
            let er = if volatility > 0.0 {
                net / volatility
            } else {
                0.0
            };
            (er, w)
        })
        .collect();

    scored.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());

    // Bottom 10% = most mean-reverting, top 10% = most trending
    let n_extreme = scored.len() / 10;
    let reverting_windows = &scored[..n_extreme];
    let trending_windows = &scored[scored.len() - n_extreme..];

    println!(
        "Total windows: {}, using top/bottom {} for regime comparison",
        scored.len(),
        n_extreme
    );
    println!(
        "Trending ER range: {:.4}–{:.4}, Reverting ER range: {:.4}–{:.4}",
        trending_windows.first().unwrap().0,
        trending_windows.last().unwrap().0,
        reverting_windows.first().unwrap().0,
        reverting_windows.last().unwrap().0,
    );

    println!(
        "\n{:<12} {:>10} {:>10} {:>10} {:>12}",
        "Estimator", "H(trend)", "H(revert)", "Delta", "Correct?"
    );

    for &(est_name, est_fn) in ESTIMATORS {
        let h_trend: f64 = trending_windows.iter().map(|&(_, w)| est_fn(w)).sum::<f64>()
            / n_extreme as f64;
        let h_revert: f64 = reverting_windows.iter().map(|&(_, w)| est_fn(w)).sum::<f64>()
            / n_extreme as f64;
        let delta = h_trend - h_revert;

        // Correct: trending windows should have higher H
        let verdict = if delta > 0.01 {
            "YES"
        } else if delta > -0.01 {
            "MARGINAL"
        } else {
            "WRONG"
        };

        println!(
            "{:<12} {:>10.4} {:>10.4} {:>10.4} {:>12}",
            est_name, h_trend, h_revert, delta, verdict
        );
    }
}

// ============================================================================
// Audit 8: Cross-estimator correlation matrix
// ============================================================================

/// Which estimators agree with each other? High correlation between Higuchi
/// and others validates its results. If Higuchi is uncorrelated with everything,
/// it might be measuring something different.
#[test]
fn audit_cross_estimator_correlation() {
    let symbols = require_real_data();
    let (_sym_name, prices) = &symbols[0];

    let win = 256;
    let n_windows = 300;

    println!("\n=== AUDIT 8: Cross-Estimator Correlation Matrix ===");

    let windows: Vec<&[f64]> = prices.windows(win).step_by(win).take(n_windows).collect();

    // Compute H for each estimator on each window
    let mut all_h: Vec<Vec<f64>> = Vec::new();
    let mut names: Vec<&str> = Vec::new();

    for &(est_name, est_fn) in ESTIMATORS {
        let h_values: Vec<f64> = windows.iter().map(|w| est_fn(w)).collect();
        all_h.push(h_values);
        names.push(est_name);
    }

    let k = ESTIMATORS.len();

    // Print header
    print!("{:<12}", "");
    for name in &names {
        print!("{:>12}", name);
    }
    println!();

    // Compute Pearson correlation matrix
    for i in 0..k {
        print!("{:<12}", names[i]);
        let mean_i = all_h[i].iter().sum::<f64>() / n_windows as f64;
        let std_i = (all_h[i]
            .iter()
            .map(|&h| (h - mean_i) * (h - mean_i))
            .sum::<f64>()
            / n_windows as f64)
            .sqrt();

        for j in 0..k {
            let mean_j = all_h[j].iter().sum::<f64>() / n_windows as f64;
            let std_j = (all_h[j]
                .iter()
                .map(|&h| (h - mean_j) * (h - mean_j))
                .sum::<f64>()
                / n_windows as f64)
                .sqrt();

            let corr = if std_i > 1e-10 && std_j > 1e-10 {
                let cov: f64 = all_h[i]
                    .iter()
                    .zip(all_h[j].iter())
                    .map(|(&a, &b)| (a - mean_i) * (b - mean_j))
                    .sum::<f64>()
                    / n_windows as f64;
                cov / (std_i * std_j)
            } else {
                0.0
            };

            print!("{:>12.3}", corr);
        }
        println!();
    }
}

// ============================================================================
// Audit 9: Preprocessed estimators vs R/S Simple — the definitive comparison
// ============================================================================

/// The critical question: if we preprocess raw prices into log-returns,
/// do DFA/Higuchi/GHE beat R/S Simple for regime detection?
///
/// DFA/Wavelet produce alpha ≈ H+1 on raw prices because prices are integrated
/// (cumulative) series. Literature says: compute returns first, then estimate H.
///
/// This test compares:
/// - R/S Simple on raw prices (current production default)
/// - All estimators on log-returns (preprocessed)
///
/// Metrics: regime separation (Kaufman ER trending vs reverting), cross-symbol
/// consistency, outlier robustness — the 3 dimensions where R/S won on raw data.
#[test]
fn audit_preprocessed_vs_rs_simple_raw() {
    let symbols = require_real_data();

    let win = 256;

    println!("\n=== AUDIT 9: Preprocessed (log-returns) vs R/S Simple (raw prices) ===");
    println!("Question: Do advanced estimators on returns beat R/S Simple on prices?\n");

    // --- Part A: Regime Detection (Kaufman ER) ---
    println!("--- Part A: Regime Separation (Kaufman ER, top/bottom 10%) ---");
    println!(
        "{:<20} {:>10} {:>10} {:>10} {:>8}",
        "Estimator+Input", "H(trend)", "H(revert)", "Delta", "Correct?"
    );

    // Build sliding windows of returns for each symbol
    for (sym_name, prices) in &symbols {
        // Log-returns
        let returns: Vec<f64> = prices.windows(2).map(|w| (w[1] / w[0]).ln()).collect();

        // Price windows for Kaufman ER classification + R/S Simple
        let price_windows: Vec<Vec<f64>> =
            prices.windows(win).step_by(win / 4).map(|w| w.to_vec()).collect();

        // Return windows (same offset pattern, but 1 shorter per window)
        let return_windows: Vec<Vec<f64>> = returns
            .windows(win)
            .step_by(win / 4)
            .map(|w| w.to_vec())
            .collect();

        let n_windows = price_windows.len().min(return_windows.len());
        if n_windows < 20 {
            continue;
        }

        // Compute Kaufman ER for each window (using prices for classification)
        let mut er_indices: Vec<(f64, usize)> = Vec::with_capacity(n_windows);
        for (i, pw) in price_windows.iter().take(n_windows).enumerate() {
            let net = (pw.last().unwrap() - pw.first().unwrap()).abs();
            let path: f64 = pw.windows(2).map(|w| (w[1] - w[0]).abs()).sum();
            let er = if path > 1e-10 { net / path } else { 0.0 };
            er_indices.push((er, i));
        }
        er_indices.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());

        let n_regime = n_windows / 10; // top/bottom 10%
        if n_regime < 5 {
            continue;
        }

        let reverting_indices: Vec<usize> =
            er_indices[..n_regime].iter().map(|&(_, i)| i).collect();
        let trending_indices: Vec<usize> = er_indices[n_windows - n_regime..]
            .iter()
            .map(|&(_, i)| i)
            .collect();

        // R/S Simple on RAW PRICES (current production)
        let h_trend_rs: f64 = trending_indices
            .iter()
            .map(|&i| rssimple(&price_windows[i]))
            .sum::<f64>()
            / n_regime as f64;
        let h_revert_rs: f64 = reverting_indices
            .iter()
            .map(|&i| rssimple(&price_windows[i]))
            .sum::<f64>()
            / n_regime as f64;
        let delta_rs = h_trend_rs - h_revert_rs;
        let correct_rs = if delta_rs > 0.0 { "YES" } else { "NO" };

        println!(
            "{:<20} {:>10.4} {:>10.4} {:>10.4} {:>8}",
            format!("rs_prices_{sym_name}"),
            h_trend_rs,
            h_revert_rs,
            delta_rs,
            correct_rs
        );

        // All estimators on LOG-RETURNS (preprocessed)
        for &(est_name, est_fn) in ESTIMATORS {
            let h_trend: f64 = trending_indices
                .iter()
                .map(|&i| est_fn(&return_windows[i]))
                .sum::<f64>()
                / n_regime as f64;
            let h_revert: f64 = reverting_indices
                .iter()
                .map(|&i| est_fn(&return_windows[i]))
                .sum::<f64>()
                / n_regime as f64;
            let delta = h_trend - h_revert;
            let correct = if delta > 0.0 { "YES" } else { "NO" };

            println!(
                "{:<20} {:>10.4} {:>10.4} {:>10.4} {:>8}",
                format!("{est_name}_ret_{sym_name}"),
                h_trend,
                h_revert,
                delta,
                correct
            );
        }
        println!();
    }

    // --- Part B: Cross-symbol consistency on returns ---
    println!("--- Part B: Cross-Symbol Consistency (returns, n=256, 200 windows) ---");
    println!(
        "{:<15} {:>10} {:>10} {:>10}",
        "Estimator", "CV(prices)", "CV(returns)", "Better?"
    );

    let n_windows_b = 200;
    for &(est_name, est_fn) in ESTIMATORS {
        let mut price_means = Vec::new();
        let mut return_means = Vec::new();

        for (_sym_name, prices) in &symbols {
            let returns: Vec<f64> = prices.windows(2).map(|w| (w[1] / w[0]).ln()).collect();

            // H on prices
            let pw: Vec<&[f64]> = prices.windows(win).step_by(win).take(n_windows_b).collect();
            if pw.len() >= 50 {
                let mean_h: f64 = pw.iter().map(|w| est_fn(w)).sum::<f64>() / pw.len() as f64;
                price_means.push(mean_h);
            }

            // H on returns
            let rw: Vec<&[f64]> = returns.windows(win).step_by(win).take(n_windows_b).collect();
            if rw.len() >= 50 {
                let mean_h: f64 = rw.iter().map(|w| est_fn(w)).sum::<f64>() / rw.len() as f64;
                return_means.push(mean_h);
            }
        }

        let cv_price = if price_means.len() >= 2 {
            let mean = price_means.iter().sum::<f64>() / price_means.len() as f64;
            let std = (price_means
                .iter()
                .map(|&m| (m - mean) * (m - mean))
                .sum::<f64>()
                / price_means.len() as f64)
                .sqrt();
            if mean.abs() > 1e-10 {
                std / mean.abs()
            } else {
                f64::INFINITY
            }
        } else {
            f64::NAN
        };

        let cv_return = if return_means.len() >= 2 {
            let mean = return_means.iter().sum::<f64>() / return_means.len() as f64;
            let std = (return_means
                .iter()
                .map(|&m| (m - mean) * (m - mean))
                .sum::<f64>()
                / return_means.len() as f64)
                .sqrt();
            if mean.abs() > 1e-10 {
                std / mean.abs()
            } else {
                f64::INFINITY
            }
        } else {
            f64::NAN
        };

        let better = if cv_return < cv_price {
            "RETURNS"
        } else {
            "PRICES"
        };

        println!(
            "{:<15} {:>10.4} {:>10.4} {:>10}",
            est_name, cv_price, cv_return, better
        );
    }

    // --- Part C: Outlier robustness on returns ---
    println!("\n--- Part C: Outlier Robustness (returns, 5% spikes) ---");
    println!(
        "{:<15} {:>10} {:>10} {:>10} {:>10}",
        "Estimator", "H(clean)", "H(dirty)", "Delta", "Verdict"
    );

    let (_, prices) = &symbols[0];
    let returns: Vec<f64> = prices.windows(2).map(|w| (w[1] / w[0]).ln()).collect();
    let n_windows_c = 100;
    let return_windows: Vec<Vec<f64>> = returns
        .windows(win)
        .step_by(win)
        .take(n_windows_c)
        .map(|w| w.to_vec())
        .collect();

    for &(est_name, est_fn) in ESTIMATORS {
        let h_clean: f64 = return_windows.iter().map(|w| est_fn(w)).sum::<f64>()
            / return_windows.len() as f64;

        // Inject 5% outlier spikes into returns
        let mut h_dirty_sum = 0.0;
        for window in &return_windows {
            let mut dirty = window.clone();
            let mut state = 42u64;
            for _ in 0..(dirty.len() / 20) {
                state = state
                    .wrapping_mul(6_364_136_223_846_793_005)
                    .wrapping_add(1);
                let idx = (state >> 33) as usize % dirty.len();
                let spike_sign = if (state >> 32) & 1 == 0 { 1.0 } else { -1.0 };
                dirty[idx] *= 10.0 * spike_sign; // 10x spike in return
            }
            h_dirty_sum += est_fn(&dirty);
        }
        let h_dirty = h_dirty_sum / return_windows.len() as f64;
        let delta = (h_clean - h_dirty).abs();

        let verdict = if delta < 0.05 {
            "ROBUST"
        } else if delta < 0.15 {
            "MODERATE"
        } else {
            "FRAGILE"
        };

        println!(
            "{:<15} {:>10.4} {:>10.4} {:>10.4} {:>10}",
            est_name, h_clean, h_dirty, delta, verdict
        );
    }
}
