#[cfg(feature = "python")]
use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::PyDict;

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use serde::{Deserialize, Serialize};
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use wasm_bindgen::prelude::*;

use crate::indicators::moving_averages::ma::{ma, ma_with_kernel, MaData};
use crate::utilities::data_loader::{source_type, Candles};
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{
    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
    make_uninit_matrix,
};
#[cfg(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;
use aligned_vec::{AVec, CACHELINE_ALIGN};
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
use core::arch::x86_64::*;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::convert::AsRef;
use std::error::Error;
use std::mem::MaybeUninit;
use thiserror::Error;

#[derive(Debug, Clone)]
pub enum VwmacdData<'a> {
    Candles {
        candles: &'a Candles,
        close_source: &'a str,
        volume_source: &'a str,
    },
    Slices {
        close: &'a [f64],
        volume: &'a [f64],
    },
}

#[derive(Debug, Clone)]
pub struct VwmacdOutput {
    pub macd: Vec<f64>,
    pub signal: Vec<f64>,
    pub hist: Vec<f64>,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
#[derive(Serialize, Deserialize)]
pub struct VwmacdJsOutput {
    #[wasm_bindgen(getter_with_clone)]
    pub macd: Vec<f64>,
    #[wasm_bindgen(getter_with_clone)]
    pub signal: Vec<f64>,
    #[wasm_bindgen(getter_with_clone)]
    pub hist: Vec<f64>,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct VwmacdBatchConfig {
    pub fast_range: (usize, usize, usize),
    pub slow_range: (usize, usize, usize),
    pub signal_range: (usize, usize, usize),
    pub fast_ma_type: Option<String>,
    pub slow_ma_type: Option<String>,
    pub signal_ma_type: Option<String>,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct VwmacdBatchJsOutput {
    pub values: Vec<f64>,
    pub combos: Vec<VwmacdParams>,
    pub rows: usize,
    pub cols: usize,
}

#[derive(Debug, Clone)]
#[cfg_attr(
    all(target_arch = "wasm32", feature = "wasm"),
    derive(Serialize, Deserialize)
)]
pub struct VwmacdParams {
    pub fast_period: Option<usize>,
    pub slow_period: Option<usize>,
    pub signal_period: Option<usize>,
    pub fast_ma_type: Option<String>,
    pub slow_ma_type: Option<String>,
    pub signal_ma_type: Option<String>,
}

impl Default for VwmacdParams {
    fn default() -> Self {
        Self {
            fast_period: Some(12),
            slow_period: Some(26),
            signal_period: Some(9),
            fast_ma_type: Some("sma".to_string()),
            slow_ma_type: Some("sma".to_string()),
            signal_ma_type: Some("ema".to_string()),
        }
    }
}

#[derive(Debug, Clone)]
pub struct VwmacdInput<'a> {
    pub data: VwmacdData<'a>,
    pub params: VwmacdParams,
}

impl<'a> VwmacdInput<'a> {
    #[inline]
    pub fn from_candles(
        candles: &'a Candles,
        close_source: &'a str,
        volume_source: &'a str,
        params: VwmacdParams,
    ) -> Self {
        Self {
            data: VwmacdData::Candles {
                candles,
                close_source,
                volume_source,
            },
            params,
        }
    }
    #[inline]
    pub fn from_slices(close: &'a [f64], volume: &'a [f64], params: VwmacdParams) -> Self {
        Self {
            data: VwmacdData::Slices { close, volume },
            params,
        }
    }
    #[inline]
    pub fn with_default_candles(candles: &'a Candles) -> Self {
        Self::from_candles(candles, "close", "volume", VwmacdParams::default())
    }
    #[inline]
    pub fn get_fast(&self) -> usize {
        self.params.fast_period.unwrap_or(12)
    }
    #[inline]
    pub fn get_slow(&self) -> usize {
        self.params.slow_period.unwrap_or(26)
    }
    #[inline]
    pub fn get_signal(&self) -> usize {
        self.params.signal_period.unwrap_or(9)
    }
    #[inline]
    pub fn get_fast_ma_type(&self) -> &str {
        self.params.fast_ma_type.as_deref().unwrap_or("sma")
    }
    #[inline]
    pub fn get_slow_ma_type(&self) -> &str {
        self.params.slow_ma_type.as_deref().unwrap_or("sma")
    }
    #[inline]
    pub fn get_signal_ma_type(&self) -> &str {
        self.params.signal_ma_type.as_deref().unwrap_or("ema")
    }
}

#[derive(Clone, Debug)]
pub struct VwmacdBuilder {
    fast: Option<usize>,
    slow: Option<usize>,
    signal: Option<usize>,
    fast_ma_type: Option<String>,
    slow_ma_type: Option<String>,
    signal_ma_type: Option<String>,
    kernel: Kernel,
}

impl Default for VwmacdBuilder {
    fn default() -> Self {
        Self {
            fast: None,
            slow: None,
            signal: None,
            fast_ma_type: None,
            slow_ma_type: None,
            signal_ma_type: None,
            kernel: Kernel::Auto,
        }
    }
}

impl VwmacdBuilder {
    #[inline(always)]
    pub fn new() -> Self {
        Self::default()
    }
    #[inline(always)]
    pub fn fast(mut self, n: usize) -> Self {
        self.fast = Some(n);
        self
    }
    #[inline(always)]
    pub fn slow(mut self, n: usize) -> Self {
        self.slow = Some(n);
        self
    }
    #[inline(always)]
    pub fn signal(mut self, n: usize) -> Self {
        self.signal = Some(n);
        self
    }
    #[inline(always)]
    pub fn fast_ma_type(mut self, ma_type: String) -> Self {
        self.fast_ma_type = Some(ma_type);
        self
    }
    #[inline(always)]
    pub fn slow_ma_type(mut self, ma_type: String) -> Self {
        self.slow_ma_type = Some(ma_type);
        self
    }
    #[inline(always)]
    pub fn signal_ma_type(mut self, ma_type: String) -> Self {
        self.signal_ma_type = Some(ma_type);
        self
    }
    #[inline(always)]
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }
    #[inline(always)]
    pub fn apply(self, c: &Candles) -> Result<VwmacdOutput, VwmacdError> {
        let p = VwmacdParams {
            fast_period: self.fast,
            slow_period: self.slow,
            signal_period: self.signal,
            fast_ma_type: self.fast_ma_type,
            slow_ma_type: self.slow_ma_type,
            signal_ma_type: self.signal_ma_type,
        };
        let i = VwmacdInput::from_candles(c, "close", "volume", p);
        vwmacd_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn apply_slices(self, close: &[f64], volume: &[f64]) -> Result<VwmacdOutput, VwmacdError> {
        let p = VwmacdParams {
            fast_period: self.fast,
            slow_period: self.slow,
            signal_period: self.signal,
            fast_ma_type: self.fast_ma_type,
            slow_ma_type: self.slow_ma_type,
            signal_ma_type: self.signal_ma_type,
        };
        let i = VwmacdInput::from_slices(close, volume, p);
        vwmacd_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn into_stream(self) -> Result<VwmacdStream, VwmacdError> {
        let p = VwmacdParams {
            fast_period: self.fast,
            slow_period: self.slow,
            signal_period: self.signal,
            fast_ma_type: self.fast_ma_type,
            slow_ma_type: self.slow_ma_type,
            signal_ma_type: self.signal_ma_type,
        };
        VwmacdStream::try_new(p)
    }
}

#[derive(Debug, Error)]
pub enum VwmacdError {
    #[error("vwmacd: Input data slice is empty.")]
    EmptyInputData,
    #[error("vwmacd: All values are NaN.")]
    AllValuesNaN,
    #[error(
        "vwmacd: Invalid period: fast={fast}, slow={slow}, signal={signal}, data_len={data_len}"
    )]
    InvalidPeriod {
        fast: usize,
        slow: usize,
        signal: usize,
        data_len: usize,
    },
    #[error("vwmacd: Not enough valid data: needed={needed}, valid={valid}")]
    NotEnoughValidData { needed: usize, valid: usize },
    #[error("vwmacd: Output length mismatch: expected={expected}, got={got}")]
    OutputLengthMismatch { expected: usize, got: usize },
    #[error("vwmacd: Invalid range: start={start}, end={end}, step={step}")]
    InvalidRange {
        start: String,
        end: String,
        step: String,
    },
    #[error("vwmacd: Invalid kernel for batch: {0:?}")]
    InvalidKernelForBatch(Kernel),
    #[error("vwmacd: MA calculation error: {0}")]
    MaError(String),
}

#[inline(always)]
fn first_valid_pair(close: &[f64], volume: &[f64]) -> Option<usize> {
    close
        .iter()
        .zip(volume)
        .position(|(c, v)| !c.is_nan() && !v.is_nan())
}

#[inline]
pub fn vwmacd(input: &VwmacdInput) -> Result<VwmacdOutput, VwmacdError> {
    vwmacd_with_kernel(input, Kernel::Auto)
}

pub fn vwmacd_with_kernel(
    input: &VwmacdInput,
    kernel: Kernel,
) -> Result<VwmacdOutput, VwmacdError> {
    let (
        close,
        volume,
        fast,
        slow,
        signal_period,
        fmt,
        smt,
        sigmt,
        first,
        macd_warmup_abs,
        total_warmup_abs,
        chosen,
    ) = vwmacd_prepare(input, kernel)?;

    let mut macd = alloc_with_nan_prefix(close.len(), macd_warmup_abs);
    let mut signal = alloc_with_nan_prefix(close.len(), total_warmup_abs);
    let mut hist = alloc_with_nan_prefix(close.len(), total_warmup_abs);

    vwmacd_compute_into(
        close,
        volume,
        fast,
        slow,
        signal_period,
        fmt,
        smt,
        sigmt,
        first,
        macd_warmup_abs,
        total_warmup_abs,
        chosen,
        &mut macd,
        &mut signal,
        &mut hist,
    )?;

    Ok(VwmacdOutput { macd, signal, hist })
}

pub fn vwmacd_into_slice(
    dst_macd: &mut [f64],
    dst_signal: &mut [f64],
    dst_hist: &mut [f64],
    input: &VwmacdInput,
    kern: Kernel,
) -> Result<(), VwmacdError> {
    let (
        close,
        volume,
        fast,
        slow,
        signal_period,
        fmt,
        smt,
        sigmt,
        first,
        macd_warmup_abs,
        total_warmup_abs,
        chosen,
    ) = vwmacd_prepare(input, kern)?;
    let len = close.len();
    if dst_macd.len() != len || dst_signal.len() != len || dst_hist.len() != len {
        if dst_macd.len() != len {
            return Err(VwmacdError::OutputLengthMismatch {
                expected: len,
                got: dst_macd.len(),
            });
        }
        if dst_signal.len() != len {
            return Err(VwmacdError::OutputLengthMismatch {
                expected: len,
                got: dst_signal.len(),
            });
        }
        return Err(VwmacdError::OutputLengthMismatch {
            expected: len,
            got: dst_hist.len(),
        });
    }

    vwmacd_compute_into(
        close,
        volume,
        fast,
        slow,
        signal_period,
        fmt,
        smt,
        sigmt,
        first,
        macd_warmup_abs,
        total_warmup_abs,
        chosen,
        dst_macd,
        dst_signal,
        dst_hist,
    )
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
pub fn vwmacd_into(
    input: &VwmacdInput,
    macd_out: &mut [f64],
    signal_out: &mut [f64],
    hist_out: &mut [f64],
) -> Result<(), VwmacdError> {
    let (
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        first,
        macd_warmup_abs,
        total_warmup_abs,
        chosen,
    ) = vwmacd_prepare(input, Kernel::Auto)?;

    let len = close.len();
    if macd_out.len() != len || signal_out.len() != len || hist_out.len() != len {
        if macd_out.len() != len {
            return Err(VwmacdError::OutputLengthMismatch {
                expected: len,
                got: macd_out.len(),
            });
        }
        if signal_out.len() != len {
            return Err(VwmacdError::OutputLengthMismatch {
                expected: len,
                got: signal_out.len(),
            });
        }
        return Err(VwmacdError::OutputLengthMismatch {
            expected: len,
            got: hist_out.len(),
        });
    }

    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
    for i in 0..macd_warmup_abs.min(len) {
        macd_out[i] = qnan;
    }
    for i in 0..total_warmup_abs.min(len) {
        signal_out[i] = qnan;
        hist_out[i] = qnan;
    }

    vwmacd_compute_into(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        first,
        macd_warmup_abs,
        total_warmup_abs,
        chosen,
        macd_out,
        signal_out,
        hist_out,
    )
}

#[inline]
pub unsafe fn vwmacd_scalar(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<VwmacdOutput, VwmacdError> {
    let len = close.len();
    let mut close_x_volume = alloc_with_nan_prefix(len, 0);
    for i in 0..len {
        if !close[i].is_nan() && !volume[i].is_nan() {
            close_x_volume[i] = close[i] * volume[i];
        }
    }

    let slow_ma_cv = ma(slow_ma_type, MaData::Slice(&close_x_volume), slow)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
    let slow_ma_v = ma(slow_ma_type, MaData::Slice(&volume), slow)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;

    let mut vwma_slow = alloc_with_nan_prefix(len, slow - 1);
    for i in 0..len {
        let denom = slow_ma_v[i];
        if !denom.is_nan() && denom != 0.0 {
            vwma_slow[i] = slow_ma_cv[i] / denom;
        }
    }

    let fast_ma_cv = ma(fast_ma_type, MaData::Slice(&close_x_volume), fast)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
    let fast_ma_v = ma(fast_ma_type, MaData::Slice(&volume), fast)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;

    let mut vwma_fast = alloc_with_nan_prefix(len, fast - 1);
    for i in 0..len {
        let denom = fast_ma_v[i];
        if !denom.is_nan() && denom != 0.0 {
            vwma_fast[i] = fast_ma_cv[i] / denom;
        }
    }

    let mut macd = alloc_with_nan_prefix(len, slow - 1);
    for i in 0..len {
        if !vwma_fast[i].is_nan() && !vwma_slow[i].is_nan() {
            macd[i] = vwma_fast[i] - vwma_slow[i];
        }
    }

    let mut signal_vec = ma(signal_ma_type, MaData::Slice(&macd), signal)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;

    let total_warmup = slow + signal - 2;
    for i in 0..total_warmup {
        signal_vec[i] = f64::NAN;
    }

    let mut hist = alloc_with_nan_prefix(len, total_warmup);
    for i in 0..len {
        if !macd[i].is_nan() && !signal_vec[i].is_nan() {
            hist[i] = macd[i] - signal_vec[i];
        }
    }
    Ok(VwmacdOutput {
        macd,
        signal: signal_vec,
        hist,
    })
}

pub unsafe fn vwmacd_scalar_classic(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    first_valid_idx: usize,
    macd_warmup_abs: usize,
    total_warmup_abs: usize,
    dst_macd: &mut [f64],
    dst_signal: &mut [f64],
    dst_hist: &mut [f64],
) -> Result<(), VwmacdError> {
    let len = close.len();

    for i in 0..macd_warmup_abs.min(len) {
        dst_macd[i] = f64::NAN;
    }

    if first_valid_idx < len {
        let mut f_cv = 0.0f64;
        let mut f_v = 0.0f64;
        let mut s_cv = 0.0f64;
        let mut s_v = 0.0f64;

        let mut i = first_valid_idx;
        while i < len {
            let v_i = volume[i];
            let cv_i = close[i] * v_i;

            f_cv += cv_i;
            f_v += v_i;
            s_cv += cv_i;
            s_v += v_i;

            let n_since_first = i - first_valid_idx + 1;
            if n_since_first > fast {
                let j = i - fast;
                let v_o = volume[j];
                let cv_o = close[j] * v_o;
                f_cv -= cv_o;
                f_v -= v_o;
            }
            if n_since_first > slow {
                let j = i - slow;
                let v_o = volume[j];
                let cv_o = close[j] * v_o;
                s_cv -= cv_o;
                s_v -= v_o;
            }

            if i >= macd_warmup_abs {
                if f_v != 0.0 && s_v != 0.0 {
                    let fast_vwma = f_cv / f_v;
                    let slow_vwma = s_cv / s_v;
                    dst_macd[i] = fast_vwma - slow_vwma;
                } else {
                    dst_macd[i] = f64::NAN;
                }
            }
            i += 1;
        }
    }

    if macd_warmup_abs < len {
        let alpha = 2.0 / (signal as f64 + 1.0);
        let beta = 1.0 - alpha;

        let start = macd_warmup_abs;
        let warmup_end = (start + signal).min(len);
        if start < len {
            let mut mean = dst_macd[start];
            dst_signal[start] = mean;
            let mut count = 1usize;
            for i in (start + 1)..warmup_end {
                let x = dst_macd[i];
                count += 1;
                mean = ((count as f64 - 1.0) * mean + x) / (count as f64);
                dst_signal[i] = mean;
            }

            let mut prev = mean;
            for i in warmup_end..len {
                let x = dst_macd[i];
                prev = beta.mul_add(prev, alpha * x);
                dst_signal[i] = prev;
            }
        }
    }

    for i in 0..total_warmup_abs.min(len) {
        dst_signal[i] = f64::NAN;
    }

    for i in 0..total_warmup_abs.min(len) {
        dst_hist[i] = f64::NAN;
    }
    for i in total_warmup_abs..len {
        if !dst_macd[i].is_nan() && !dst_signal[i].is_nan() {
            dst_hist[i] = dst_macd[i] - dst_signal[i];
        } else {
            dst_hist[i] = f64::NAN;
        }
    }

    Ok(())
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn vwmacd_avx2(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<VwmacdOutput, VwmacdError> {
    vwmacd_scalar(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
    )
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn vwmacd_avx512(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<VwmacdOutput, VwmacdError> {
    if slow <= 32 {
        vwmacd_avx512_short(
            close,
            volume,
            fast,
            slow,
            signal,
            fast_ma_type,
            slow_ma_type,
            signal_ma_type,
        )
    } else {
        vwmacd_avx512_long(
            close,
            volume,
            fast,
            slow,
            signal,
            fast_ma_type,
            slow_ma_type,
            signal_ma_type,
        )
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn vwmacd_avx512_short(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<VwmacdOutput, VwmacdError> {
    vwmacd_scalar(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
    )
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn vwmacd_avx512_long(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<VwmacdOutput, VwmacdError> {
    vwmacd_scalar(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
    )
}

#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
#[inline]
pub unsafe fn vwmacd_simd128(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<VwmacdOutput, VwmacdError> {
    vwmacd_scalar(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
    )
}

#[inline]
pub unsafe fn vwmacd_scalar_macd_into(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    out: &mut [f64],
) -> Result<(), VwmacdError> {
    let len = close.len();

    if fast_ma_type.eq_ignore_ascii_case("sma") && slow_ma_type.eq_ignore_ascii_case("sma") {
        if len == 0 {
            return Ok(());
        }
        let first = match first_valid_pair(close, volume) {
            Some(ix) => ix,
            None => return Ok(()),
        };
        let macd_warmup_abs = first + fast.max(slow) - 1;
        for i in 0..macd_warmup_abs.min(len) {
            out[i] = f64::NAN;
        }

        let mut f_cv = 0.0f64;
        let mut f_v = 0.0f64;
        let mut s_cv = 0.0f64;
        let mut s_v = 0.0f64;
        let mut i = first;
        while i < len {
            let v_i = volume[i];
            let cv_i = close[i] * v_i;
            f_cv += cv_i;
            f_v += v_i;
            s_cv += cv_i;
            s_v += v_i;

            let n_since_first = i - first + 1;
            if n_since_first > fast {
                let j = i - fast;
                let v_o = volume[j];
                let cv_o = close[j] * v_o;
                f_cv -= cv_o;
                f_v -= v_o;
            }
            if n_since_first > slow {
                let j = i - slow;
                let v_o = volume[j];
                let cv_o = close[j] * v_o;
                s_cv -= cv_o;
                s_v -= v_o;
            }

            if i >= macd_warmup_abs {
                if f_v != 0.0 && s_v != 0.0 {
                    out[i] = (f_cv / f_v) - (s_cv / s_v);
                } else {
                    out[i] = f64::NAN;
                }
            }
            i += 1;
        }

        return Ok(());
    }

    let mut close_x_volume = alloc_with_nan_prefix(len, 0);
    for i in 0..len {
        if !close[i].is_nan() && !volume[i].is_nan() {
            close_x_volume[i] = close[i] * volume[i];
        }
    }

    let slow_ma_cv = ma_with_kernel(
        slow_ma_type,
        MaData::Slice(&close_x_volume),
        slow,
        Kernel::Scalar,
    )
    .map_err(|e| VwmacdError::MaError(e.to_string()))?;
    let slow_ma_v = ma_with_kernel(slow_ma_type, MaData::Slice(&volume), slow, Kernel::Scalar)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
    let fast_ma_cv = ma_with_kernel(
        fast_ma_type,
        MaData::Slice(&close_x_volume),
        fast,
        Kernel::Scalar,
    )
    .map_err(|e| VwmacdError::MaError(e.to_string()))?;
    let fast_ma_v = ma_with_kernel(fast_ma_type, MaData::Slice(&volume), fast, Kernel::Scalar)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;

    let macd_warmup = slow.max(fast);
    for i in 0..macd_warmup.min(len) {
        out[i] = f64::NAN;
    }
    for i in macd_warmup..len {
        let sd = slow_ma_v[i];
        let fd = fast_ma_v[i];
        if sd != 0.0 && !sd.is_nan() && fd != 0.0 && !fd.is_nan() {
            out[i] = (fast_ma_cv[i] / fd) - (slow_ma_cv[i] / sd);
        } else {
            out[i] = f64::NAN;
        }
    }
    Ok(())
}

#[inline(always)]
pub unsafe fn vwmacd_row_scalar(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    out: &mut [f64],
) {
    let _ = vwmacd_scalar_macd_into(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        out,
    );
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn vwmacd_row_avx2(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    out: &mut [f64],
) {
    vwmacd_row_scalar(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        out,
    );
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn vwmacd_row_avx512(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    out: &mut [f64],
) {
    if slow <= 32 {
        vwmacd_row_avx512_short(
            close,
            volume,
            fast,
            slow,
            signal,
            fast_ma_type,
            slow_ma_type,
            signal_ma_type,
            out,
        );
    } else {
        vwmacd_row_avx512_long(
            close,
            volume,
            fast,
            slow,
            signal,
            fast_ma_type,
            slow_ma_type,
            signal_ma_type,
            out,
        );
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn vwmacd_row_avx512_short(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    out: &mut [f64],
) {
    vwmacd_row_scalar(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        out,
    );
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn vwmacd_row_avx512_long(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    out: &mut [f64],
) {
    vwmacd_row_scalar(
        close,
        volume,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        out,
    );
}

#[inline(always)]
pub unsafe fn vwmacd_streaming_scalar(
    cv_buffer: &[f64],
    v_buffer: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    buffer_size: usize,
    head: usize,
    count: usize,
    fast_cv_sum: f64,
    fast_v_sum: f64,
    slow_cv_sum: f64,
    slow_v_sum: f64,
    macd_buffer: &[f64],
    signal_ema_state: Option<f64>,
) -> (f64, f64, f64) {
    if !(fast_ma_type.eq_ignore_ascii_case("sma")
        && slow_ma_type.eq_ignore_ascii_case("sma")
        && signal_ma_type.eq_ignore_ascii_case("ema"))
    {
        return (f64::NAN, f64::NAN, f64::NAN);
    }

    let fast_ready = count >= fast;
    let slow_ready = count >= slow;

    let mut macd = f64::NAN;

    let vwma_fast = if fast_ready && fast_v_sum != 0.0 {
        fast_cv_sum / fast_v_sum
    } else {
        f64::NAN
    };
    let vwma_slow = if slow_ready && slow_v_sum != 0.0 {
        slow_cv_sum / slow_v_sum
    } else {
        f64::NAN
    };

    if vwma_fast.is_finite() && vwma_slow.is_finite() {
        macd = vwma_fast - vwma_slow;
    }

    let mut signal_val = f64::NAN;
    let have_signal_window = count >= (slow + signal - 1);

    if have_signal_window && macd.is_finite() {
        let alpha = 2.0 / (signal as f64 + 1.0);
        let beta = 1.0 - alpha;

        signal_val = match signal_ema_state {
            Some(prev) => beta.mul_add(prev, alpha * macd),
            None => {
                let macd_idx = (count - 1) % signal;
                let mut sum = 0.0;
                let mut valid = 0usize;
                for i in 0..signal {
                    let val = if i == macd_idx { macd } else { macd_buffer[i] };
                    if val.is_finite() {
                        sum += val;
                        valid += 1;
                    }
                }
                if valid == signal {
                    sum / signal as f64
                } else {
                    f64::NAN
                }
            }
        };
    }

    let hist = if macd.is_finite() && signal_val.is_finite() {
        macd - signal_val
    } else {
        f64::NAN
    };
    (macd, signal_val, hist)
}

#[derive(Debug, Clone)]
pub struct VwmacdStream {
    fast_period: usize,
    slow_period: usize,
    signal_period: usize,
    fast_ma_type: String,
    slow_ma_type: String,
    signal_ma_type: String,

    close_volume_buffer: Vec<f64>,
    volume_buffer: Vec<f64>,

    close_buffer: Vec<f64>,

    macd_buffer: Vec<f64>,

    fast_cv_work: Vec<f64>,
    fast_v_work: Vec<f64>,
    slow_cv_work: Vec<f64>,
    slow_v_work: Vec<f64>,
    signal_work: Vec<f64>,

    fast_cv_sum: f64,
    fast_v_sum: f64,
    slow_cv_sum: f64,
    slow_v_sum: f64,

    signal_ema_state: Option<f64>,

    head: usize,

    count: usize,

    fast_filled: bool,
    slow_filled: bool,
    signal_filled: bool,
}

impl VwmacdStream {
    pub fn try_new(params: VwmacdParams) -> Result<Self, VwmacdError> {
        let fast = params.fast_period.unwrap_or(12);
        let slow = params.slow_period.unwrap_or(26);
        let signal = params.signal_period.unwrap_or(9);
        let fast_ma_type = params.fast_ma_type.unwrap_or_else(|| "sma".to_string());
        let slow_ma_type = params.slow_ma_type.unwrap_or_else(|| "sma".to_string());
        let signal_ma_type = params.signal_ma_type.unwrap_or_else(|| "ema".to_string());

        if fast == 0 || slow == 0 || signal == 0 {
            return Err(VwmacdError::InvalidPeriod {
                fast,
                slow,
                signal,
                data_len: 0,
            });
        }

        let buffer_size = (slow.max(signal) + 10).max(40);

        Ok(Self {
            fast_period: fast,
            slow_period: slow,
            signal_period: signal,
            fast_ma_type,
            slow_ma_type,
            signal_ma_type,
            close_volume_buffer: vec![0.0; buffer_size],
            volume_buffer: vec![0.0; buffer_size],
            close_buffer: vec![0.0; buffer_size],
            fast_cv_sum: 0.0,
            fast_v_sum: 0.0,
            slow_cv_sum: 0.0,
            slow_v_sum: 0.0,
            macd_buffer: vec![f64::NAN; signal],

            fast_cv_work: vec![0.0; fast],
            fast_v_work: vec![0.0; fast],
            slow_cv_work: vec![0.0; slow],
            slow_v_work: vec![0.0; slow],
            signal_work: vec![0.0; signal],
            signal_ema_state: None,
            head: 0,
            count: 0,
            fast_filled: false,
            slow_filled: false,
            signal_filled: false,
        })
    }

    pub fn update(&mut self, close: f64, volume: f64) -> Option<(f64, f64, f64)> {
        let cv = close * volume;
        let buf_len = self.close_volume_buffer.len();
        let idx = self.count % buf_len;
        self.close_volume_buffer[idx] = cv;
        self.volume_buffer[idx] = volume;
        self.close_buffer[idx] = close;

        let default_ma = self.fast_ma_type.eq_ignore_ascii_case("sma")
            && self.slow_ma_type.eq_ignore_ascii_case("sma")
            && self.signal_ma_type.eq_ignore_ascii_case("ema");

        let mut vwma_fast = f64::NAN;
        let mut vwma_slow = f64::NAN;

        if default_ma {
            self.fast_cv_sum += cv;
            self.fast_v_sum += volume;
            self.slow_cv_sum += cv;
            self.slow_v_sum += volume;
            let new_count = self.count + 1;

            if new_count > self.fast_period {
                let prev_idx = (self.count + buf_len - self.fast_period) % buf_len;
                self.fast_cv_sum -= self.close_volume_buffer[prev_idx];
                self.fast_v_sum -= self.volume_buffer[prev_idx];
            }
            if new_count > self.slow_period {
                let prev_idx = (self.count + buf_len - self.slow_period) % buf_len;
                self.slow_cv_sum -= self.close_volume_buffer[prev_idx];
                self.slow_v_sum -= self.volume_buffer[prev_idx];
            }

            let (macd, signal, hist) = unsafe {
                vwmacd_streaming_scalar(
                    &self.close_volume_buffer,
                    &self.volume_buffer,
                    self.fast_period,
                    self.slow_period,
                    self.signal_period,
                    &self.fast_ma_type,
                    &self.slow_ma_type,
                    &self.signal_ma_type,
                    buf_len,
                    idx,
                    new_count,
                    self.fast_cv_sum,
                    self.fast_v_sum,
                    self.slow_cv_sum,
                    self.slow_v_sum,
                    &self.macd_buffer,
                    self.signal_ema_state,
                )
            };

            let macd_idx = (new_count - 1) % self.signal_period;
            self.macd_buffer[macd_idx] = macd;

            self.count = new_count;

            if self.count >= self.slow_period + self.signal_period - 1 {
                if signal.is_finite() {
                    self.signal_ema_state = Some(signal);
                    self.signal_filled = true;
                }
            }

            if macd.is_finite() {
                return Some((macd, signal, hist));
            } else {
                return None;
            }
        } else {
            self.count += 1;

            if self.count >= self.fast_period {
                let start = if self.count <= buf_len {
                    self.count.saturating_sub(self.fast_period)
                } else {
                    ((idx + 1 + buf_len - self.fast_period) % buf_len)
                };
                for i in 0..self.fast_period {
                    let b = if self.count <= buf_len {
                        start + i
                    } else {
                        (start + i) % buf_len
                    };
                    self.fast_cv_work[i] = self.close_volume_buffer[b];
                    self.fast_v_work[i] = self.volume_buffer[b];
                }
                if let (Ok(cv_ma), Ok(v_ma)) = (
                    ma(
                        &self.fast_ma_type,
                        MaData::Slice(&self.fast_cv_work),
                        self.fast_period,
                    ),
                    ma(
                        &self.fast_ma_type,
                        MaData::Slice(&self.fast_v_work),
                        self.fast_period,
                    ),
                ) {
                    if let (Some(&cv_val), Some(&v_val)) = (cv_ma.last(), v_ma.last()) {
                        if v_val != 0.0 && !v_val.is_nan() {
                            vwma_fast = cv_val / v_val;
                        }
                    }
                }
            }

            if self.count >= self.slow_period {
                let start = if self.count <= buf_len {
                    self.count.saturating_sub(self.slow_period)
                } else {
                    ((idx + 1 + buf_len - self.slow_period) % buf_len)
                };
                for i in 0..self.slow_period {
                    let b = if self.count <= buf_len {
                        start + i
                    } else {
                        (start + i) % buf_len
                    };
                    self.slow_cv_work[i] = self.close_volume_buffer[b];
                    self.slow_v_work[i] = self.volume_buffer[b];
                }
                if let (Ok(cv_ma), Ok(v_ma)) = (
                    ma(
                        &self.slow_ma_type,
                        MaData::Slice(&self.slow_cv_work),
                        self.slow_period,
                    ),
                    ma(
                        &self.slow_ma_type,
                        MaData::Slice(&self.slow_v_work),
                        self.slow_period,
                    ),
                ) {
                    if let (Some(&cv_val), Some(&v_val)) = (cv_ma.last(), v_ma.last()) {
                        if v_val != 0.0 && !v_val.is_nan() {
                            vwma_slow = cv_val / v_val;
                        }
                    }
                }
            }
        }

        if default_ma {
            self.count += 1;
        }

        let macd = if !vwma_fast.is_nan() && !vwma_slow.is_nan() {
            vwma_fast - vwma_slow
        } else {
            f64::NAN
        };

        let macd_idx = (self.count - 1) % self.signal_period;
        self.macd_buffer[macd_idx] = macd;

        let signal = if self.count >= self.slow_period + self.signal_period - 1
            && self.signal_ma_type.eq_ignore_ascii_case("ema")
        {
            if !self.signal_filled {
                let macd_idx = (self.count - 1) % self.signal_period;
                let oldest = (macd_idx + 1) % self.signal_period;
                let mut sum = 0.0;
                for i in 0..self.signal_period {
                    let src = (oldest + i) % self.signal_period;
                    sum += self.macd_buffer[src];
                }
                let mean = sum / self.signal_period as f64;
                self.signal_ema_state = Some(mean);
                self.signal_filled = true;
                mean
            } else {
                let alpha = 2.0 / (self.signal_period as f64 + 1.0);
                let beta = 1.0 - alpha;
                let prev = self.signal_ema_state.unwrap();
                let updated = beta.mul_add(prev, alpha * macd);
                self.signal_ema_state = Some(updated);
                updated
            }
        } else if self.count >= self.slow_period + self.signal_period - 1 {
            let macd_idx = (self.count - 1) % self.signal_period;
            let oldest = (macd_idx + 1) % self.signal_period;
            for i in 0..self.signal_period {
                let src = (oldest + i) % self.signal_period;
                self.signal_work[i] = self.macd_buffer[src];
            }
            if let Ok(signal_ma) = ma(
                &self.signal_ma_type,
                MaData::Slice(&self.signal_work),
                self.signal_period,
            ) {
                signal_ma.last().copied().unwrap_or(f64::NAN)
            } else {
                f64::NAN
            }
        } else {
            f64::NAN
        };

        let hist = if !macd.is_nan() && !signal.is_nan() {
            macd - signal
        } else {
            f64::NAN
        };

        if !macd.is_nan() {
            Some((macd, signal, hist))
        } else {
            None
        }
    }
}

fn vwmacd_prepare<'a>(
    input: &'a VwmacdInput,
    kernel: Kernel,
) -> Result<
    (
        &'a [f64],
        &'a [f64],
        usize,
        usize,
        usize,
        &'a str,
        &'a str,
        &'a str,
        usize,
        usize,
        usize,
        Kernel,
    ),
    VwmacdError,
> {
    let (close, volume) = match &input.data {
        VwmacdData::Candles {
            candles,
            close_source,
            volume_source,
        } => (
            source_type(candles, close_source),
            source_type(candles, volume_source),
        ),
        VwmacdData::Slices { close, volume } => (*close, *volume),
    };

    let len = close.len();
    if len == 0 {
        return Err(VwmacdError::EmptyInputData);
    }
    if volume.len() != len {
        return Err(VwmacdError::OutputLengthMismatch {
            expected: len,
            got: volume.len(),
        });
    }

    if !close.iter().any(|x| !x.is_nan()) || !volume.iter().any(|x| !x.is_nan()) {
        return Err(VwmacdError::AllValuesNaN);
    }

    let fast = input.get_fast();
    let slow = input.get_slow();
    let signal = input.get_signal();

    if fast == 0 || slow == 0 || signal == 0 || fast > len || slow > len || signal > len {
        return Err(VwmacdError::InvalidPeriod {
            fast,
            slow,
            signal,
            data_len: len,
        });
    }

    let first = first_valid_pair(close, volume).ok_or(VwmacdError::AllValuesNaN)?;

    if len - first < slow {
        return Err(VwmacdError::NotEnoughValidData {
            needed: slow,
            valid: len - first,
        });
    }

    let macd_warmup_abs = first + fast.max(slow) - 1;
    let total_warmup_abs = macd_warmup_abs + signal - 1;

    let chosen = match kernel {
        Kernel::Auto => detect_best_kernel(),
        k => k,
    };

    let chosen = if input.get_fast_ma_type().eq_ignore_ascii_case("sma")
        && input.get_slow_ma_type().eq_ignore_ascii_case("sma")
        && input.get_signal_ma_type().eq_ignore_ascii_case("ema")
    {
        Kernel::Scalar
    } else {
        chosen
    };

    Ok((
        close,
        volume,
        fast,
        slow,
        signal,
        input.get_fast_ma_type(),
        input.get_slow_ma_type(),
        input.get_signal_ma_type(),
        first,
        macd_warmup_abs,
        total_warmup_abs,
        chosen,
    ))
}

#[inline(always)]
fn vwmacd_compute_into(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    first: usize,
    macd_warmup_abs: usize,
    total_warmup_abs: usize,
    kernel: Kernel,
    macd_out: &mut [f64],
    signal_out: &mut [f64],
    hist_out: &mut [f64],
) -> Result<(), VwmacdError> {
    let len = close.len();

    if kernel == Kernel::Scalar
        && fast_ma_type.eq_ignore_ascii_case("sma")
        && slow_ma_type.eq_ignore_ascii_case("sma")
        && signal_ma_type.eq_ignore_ascii_case("ema")
    {
        unsafe {
            return vwmacd_scalar_classic(
                close,
                volume,
                fast,
                slow,
                signal,
                fast_ma_type,
                slow_ma_type,
                signal_ma_type,
                first,
                macd_warmup_abs,
                total_warmup_abs,
                macd_out,
                signal_out,
                hist_out,
            );
        }
    }

    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
    if (kernel == Kernel::Avx2 || kernel == Kernel::Avx512)
        && fast_ma_type.eq_ignore_ascii_case("sma")
        && slow_ma_type.eq_ignore_ascii_case("sma")
        && signal_ma_type.eq_ignore_ascii_case("ema")
    {
        unsafe {
            if kernel == Kernel::Avx512 {
                return vwmacd_classic_into_avx512(
                    close,
                    volume,
                    fast,
                    slow,
                    signal,
                    first,
                    macd_warmup_abs,
                    total_warmup_abs,
                    macd_out,
                    signal_out,
                    hist_out,
                );
            } else {
                return vwmacd_classic_into_avx2(
                    close,
                    volume,
                    fast,
                    slow,
                    signal,
                    first,
                    macd_warmup_abs,
                    total_warmup_abs,
                    macd_out,
                    signal_out,
                    hist_out,
                );
            }
        }
    }

    let mut cv = alloc_with_nan_prefix(len, first);
    for i in first..len {
        let c = close[i];
        let v = volume[i];
        if !c.is_nan() && !v.is_nan() {
            cv[i] = c * v;
        }
    }

    let mut slow_cv = alloc_with_nan_prefix(len, first + slow - 1);
    let mut slow_v = alloc_with_nan_prefix(len, first + slow - 1);
    let mut fast_cv = alloc_with_nan_prefix(len, first + fast - 1);
    let mut fast_v = alloc_with_nan_prefix(len, first + fast - 1);

    let slow_cv_result = ma_with_kernel(slow_ma_type, MaData::Slice(&cv), slow, kernel)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
    let slow_v_result = ma_with_kernel(slow_ma_type, MaData::Slice(&volume), slow, kernel)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;

    slow_cv.copy_from_slice(&slow_cv_result);
    slow_v.copy_from_slice(&slow_v_result);

    let fast_cv_result = ma_with_kernel(fast_ma_type, MaData::Slice(&cv), fast, kernel)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
    let fast_v_result = ma_with_kernel(fast_ma_type, MaData::Slice(&volume), fast, kernel)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;

    fast_cv.copy_from_slice(&fast_cv_result);
    fast_v.copy_from_slice(&fast_v_result);

    for i in 0..macd_warmup_abs {
        macd_out[i] = f64::NAN;
    }
    for i in macd_warmup_abs..len {
        let sd = slow_v[i];
        let fd = fast_v[i];
        if sd != 0.0 && !sd.is_nan() && fd != 0.0 && !fd.is_nan() {
            macd_out[i] = (fast_cv[i] / fd) - (slow_cv[i] / sd);
        } else {
            macd_out[i] = f64::NAN;
        }
    }

    let signal_result = ma_with_kernel(signal_ma_type, MaData::Slice(&macd_out), signal, kernel)
        .map_err(|e| VwmacdError::MaError(e.to_string()))?;

    signal_out.copy_from_slice(&signal_result);

    for i in 0..total_warmup_abs {
        signal_out[i] = f64::NAN;
    }

    for i in 0..total_warmup_abs {
        hist_out[i] = f64::NAN;
    }
    for i in total_warmup_abs..len {
        let m = macd_out[i];
        let s = signal_out[i];
        hist_out[i] = if !m.is_nan() && !s.is_nan() {
            m - s
        } else {
            f64::NAN
        };
    }

    Ok(())
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
unsafe fn vwmacd_classic_into_avx2(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    first: usize,
    macd_warmup_abs: usize,
    total_warmup_abs: usize,
    macd_out: &mut [f64],
    signal_out: &mut [f64],
    hist_out: &mut [f64],
) -> Result<(), VwmacdError> {
    let len = close.len();
    for i in 0..macd_warmup_abs.min(len) {
        macd_out[i] = f64::NAN;
    }

    let mut cv = Vec::<f64>::with_capacity(len);
    cv.set_len(len);
    {
        let ptr_c = close.as_ptr();
        let ptr_v = volume.as_ptr();
        let ptr_o = cv.as_mut_ptr();
        let mut i = first;
        let lanes = 4usize;
        let vec_end = first + ((len - first) / lanes) * lanes;
        while i + lanes <= vec_end {
            let c = _mm256_loadu_pd(ptr_c.add(i));
            let v = _mm256_loadu_pd(ptr_v.add(i));
            let prod = _mm256_mul_pd(c, v);
            _mm256_storeu_pd(ptr_o.add(i), prod);
            i += lanes;
        }
        while i < len {
            *ptr_o.add(i) = *ptr_c.add(i) * *ptr_v.add(i);
            i += 1;
        }
    }

    let mut f_cv = 0.0f64;
    let mut f_v = 0.0f64;
    let mut s_cv = 0.0f64;
    let mut s_v = 0.0f64;
    let mut i = first;
    while i < len {
        let v_i = volume[i];
        let cv_i = cv[i];
        f_cv += cv_i;
        f_v += v_i;
        s_cv += cv_i;
        s_v += v_i;

        let n_since_first = i - first + 1;
        if n_since_first > fast {
            let j = i - fast;
            let v_o = volume[j];
            let cv_o = cv[j];
            f_cv -= cv_o;
            f_v -= v_o;
        }
        if n_since_first > slow {
            let j = i - slow;
            let v_o = volume[j];
            let cv_o = cv[j];
            s_cv -= cv_o;
            s_v -= v_o;
        }

        if i >= macd_warmup_abs {
            if f_v != 0.0 && s_v != 0.0 {
                macd_out[i] = (f_cv / f_v) - (s_cv / s_v);
            } else {
                macd_out[i] = f64::NAN;
            }
        }
        i += 1;
    }

    if macd_warmup_abs < len {
        let alpha = 2.0f64 / (signal as f64 + 1.0);
        let beta = 1.0f64 - alpha;
        let start = macd_warmup_abs;
        let warmup_end = (start + signal).min(len);
        if start < len {
            let mut mean = macd_out[start];
            signal_out[start] = mean;
            let mut count = 1usize;
            let mut k = start + 1;
            while k < warmup_end {
                let x = macd_out[k];
                count += 1;
                mean = ((count as f64 - 1.0) * mean + x) / (count as f64);
                signal_out[k] = mean;
                k += 1;
            }
            let mut prev = mean;
            let mut t = warmup_end;
            while t < len {
                let x = macd_out[t];
                prev = beta.mul_add(prev, alpha * x);
                signal_out[t] = prev;
                t += 1;
            }
        }
    }

    for i in 0..total_warmup_abs.min(len) {
        signal_out[i] = f64::NAN;
        hist_out[i] = f64::NAN;
    }
    for i in total_warmup_abs..len {
        let m = macd_out[i];
        let s = signal_out[i];
        hist_out[i] = if !m.is_nan() && !s.is_nan() {
            m - s
        } else {
            f64::NAN
        };
    }

    Ok(())
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
unsafe fn vwmacd_classic_into_avx512(
    close: &[f64],
    volume: &[f64],
    fast: usize,
    slow: usize,
    signal: usize,
    first: usize,
    macd_warmup_abs: usize,
    total_warmup_abs: usize,
    macd_out: &mut [f64],
    signal_out: &mut [f64],
    hist_out: &mut [f64],
) -> Result<(), VwmacdError> {
    let len = close.len();
    for i in 0..macd_warmup_abs.min(len) {
        macd_out[i] = f64::NAN;
    }

    let mut cv = Vec::<f64>::with_capacity(len);
    cv.set_len(len);
    {
        let ptr_c = close.as_ptr();
        let ptr_v = volume.as_ptr();
        let ptr_o = cv.as_mut_ptr();
        let lanes = 8usize;
        let mut i = first;
        let vec_end = first + ((len - first) / lanes) * lanes;
        while i + lanes <= vec_end {
            let c = _mm512_loadu_pd(ptr_c.add(i));
            let v = _mm512_loadu_pd(ptr_v.add(i));
            let prod = _mm512_mul_pd(c, v);
            _mm512_storeu_pd(ptr_o.add(i), prod);
            i += lanes;
        }
        while i < len {
            *ptr_o.add(i) = *ptr_c.add(i) * *ptr_v.add(i);
            i += 1;
        }
    }

    let mut f_cv = 0.0f64;
    let mut f_v = 0.0f64;
    let mut s_cv = 0.0f64;
    let mut s_v = 0.0f64;
    let mut i = first;
    while i < len {
        let v_i = volume[i];
        let cv_i = cv[i];
        f_cv += cv_i;
        f_v += v_i;
        s_cv += cv_i;
        s_v += v_i;

        let n_since_first = i - first + 1;
        if n_since_first > fast {
            let j = i - fast;
            let v_o = volume[j];
            let cv_o = cv[j];
            f_cv -= cv_o;
            f_v -= v_o;
        }
        if n_since_first > slow {
            let j = i - slow;
            let v_o = volume[j];
            let cv_o = cv[j];
            s_cv -= cv_o;
            s_v -= v_o;
        }

        if i >= macd_warmup_abs {
            if f_v != 0.0 && s_v != 0.0 {
                macd_out[i] = (f_cv / f_v) - (s_cv / s_v);
            } else {
                macd_out[i] = f64::NAN;
            }
        }
        i += 1;
    }

    if macd_warmup_abs < len {
        let alpha = 2.0f64 / (signal as f64 + 1.0);
        let beta = 1.0f64 - alpha;
        let start = macd_warmup_abs;
        let warmup_end = (start + signal).min(len);
        if start < len {
            let mut mean = macd_out[start];
            signal_out[start] = mean;
            let mut count = 1usize;
            let mut k = start + 1;
            while k < warmup_end {
                let x = macd_out[k];
                count += 1;
                mean = ((count as f64 - 1.0) * mean + x) / (count as f64);
                signal_out[k] = mean;
                k += 1;
            }
            let mut prev = mean;
            let mut t = warmup_end;
            while t < len {
                let x = macd_out[t];
                prev = beta.mul_add(prev, alpha * x);
                signal_out[t] = prev;
                t += 1;
            }
        }
    }

    for i in 0..total_warmup_abs.min(len) {
        signal_out[i] = f64::NAN;
        hist_out[i] = f64::NAN;
    }
    for i in total_warmup_abs..len {
        let m = macd_out[i];
        let s = signal_out[i];
        hist_out[i] = if !m.is_nan() && !s.is_nan() {
            m - s
        } else {
            f64::NAN
        };
    }

    Ok(())
}

#[derive(Clone, Debug)]
pub struct VwmacdBatchRange {
    pub fast: (usize, usize, usize),
    pub slow: (usize, usize, usize),
    pub signal: (usize, usize, usize),
    pub fast_ma_type: String,
    pub slow_ma_type: String,
    pub signal_ma_type: String,
}

impl Default for VwmacdBatchRange {
    fn default() -> Self {
        Self {
            fast: (12, 12, 0),
            slow: (26, 275, 1),
            signal: (9, 9, 0),
            fast_ma_type: "sma".to_string(),
            slow_ma_type: "sma".to_string(),
            signal_ma_type: "ema".to_string(),
        }
    }
}

#[derive(Clone, Debug, Default)]
pub struct VwmacdBatchBuilder {
    range: VwmacdBatchRange,
    kernel: Kernel,
}

impl VwmacdBatchBuilder {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }
    #[inline]
    pub fn fast_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.fast = (start, end, step);
        self
    }
    #[inline]
    pub fn slow_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.slow = (start, end, step);
        self
    }
    #[inline]
    pub fn signal_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.signal = (start, end, step);
        self
    }
    #[inline]
    pub fn fast_ma_type(mut self, ma_type: String) -> Self {
        self.range.fast_ma_type = ma_type;
        self
    }
    #[inline]
    pub fn slow_ma_type(mut self, ma_type: String) -> Self {
        self.range.slow_ma_type = ma_type;
        self
    }
    #[inline]
    pub fn signal_ma_type(mut self, ma_type: String) -> Self {
        self.range.signal_ma_type = ma_type;
        self
    }
    #[inline]
    pub fn apply_slices(
        self,
        close: &[f64],
        volume: &[f64],
    ) -> Result<VwmacdBatchOutput, VwmacdError> {
        vwmacd_batch_with_kernel(close, volume, &self.range, self.kernel)
    }
}

#[inline(always)]
fn expand_grid(r: &VwmacdBatchRange) -> Result<Vec<VwmacdParams>, VwmacdError> {
    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, VwmacdError> {
        if step == 0 || start == end {
            return Ok(vec![start]);
        }
        if start < end {
            let st = step.max(1);
            let mut v = Vec::new();
            let mut cur = start;
            while cur <= end {
                v.push(cur);
                let next = cur.saturating_add(st);
                if next == cur {
                    break;
                }
                cur = next;
            }
            if v.is_empty() {
                return Err(VwmacdError::InvalidRange {
                    start: start.to_string(),
                    end: end.to_string(),
                    step: step.to_string(),
                });
            }
            return Ok(v);
        }
        let mut v = Vec::new();
        let mut x = start as isize;
        let end_i = end as isize;
        let st = (step as isize).max(1);
        while x >= end_i {
            v.push(x as usize);
            x -= st;
        }
        if v.is_empty() {
            return Err(VwmacdError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }

    let fasts = axis_usize(r.fast)?;
    let slows = axis_usize(r.slow)?;
    let signals = axis_usize(r.signal)?;

    let cap = fasts
        .len()
        .checked_mul(slows.len())
        .and_then(|x| x.checked_mul(signals.len()))
        .ok_or_else(|| VwmacdError::InvalidRange {
            start: "cap".into(),
            end: "overflow".into(),
            step: "mul".into(),
        })?;

    let mut out = Vec::with_capacity(cap);
    for &f in &fasts {
        for &s in &slows {
            for &g in &signals {
                out.push(VwmacdParams {
                    fast_period: Some(f),
                    slow_period: Some(s),
                    signal_period: Some(g),
                    fast_ma_type: Some(r.fast_ma_type.clone()),
                    slow_ma_type: Some(r.slow_ma_type.clone()),
                    signal_ma_type: Some(r.signal_ma_type.clone()),
                });
            }
        }
    }
    Ok(out)
}

#[derive(Clone, Debug)]
pub struct VwmacdBatchOutput {
    pub macd: Vec<f64>,
    pub signal: Vec<f64>,
    pub hist: Vec<f64>,
    pub params: Vec<VwmacdParams>,
    pub rows: usize,
    pub cols: usize,
}

impl VwmacdBatchOutput {
    pub fn values_for(&self, p: &VwmacdParams) -> Option<(&[f64], &[f64], &[f64])> {
        let row = self.params.iter().position(|c| {
            c.fast_period == p.fast_period
                && c.slow_period == p.slow_period
                && c.signal_period == p.signal_period
                && c.fast_ma_type.as_deref() == p.fast_ma_type.as_deref()
                && c.slow_ma_type.as_deref() == p.slow_ma_type.as_deref()
                && c.signal_ma_type.as_deref() == p.signal_ma_type.as_deref()
        })?;
        let start = row * self.cols;
        Some((
            &self.macd[start..start + self.cols],
            &self.signal[start..start + self.cols],
            &self.hist[start..start + self.cols],
        ))
    }
}

pub fn vwmacd_batch_with_kernel(
    close: &[f64],
    volume: &[f64],
    sweep: &VwmacdBatchRange,
    k: Kernel,
) -> Result<VwmacdBatchOutput, VwmacdError> {
    let kernel = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        other => {
            return Err(VwmacdError::InvalidKernelForBatch(other));
        }
    };
    let simd = match kernel {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,

        Kernel::Scalar => Kernel::Scalar,
        Kernel::Avx2 => Kernel::Avx2,
        Kernel::Avx512 => Kernel::Avx512,
        _ => Kernel::Scalar,
    };
    vwmacd_batch_par_slice(close, volume, sweep, simd)
}

#[inline(always)]
pub fn vwmacd_batch_slice(
    close: &[f64],
    volume: &[f64],
    sweep: &VwmacdBatchRange,
    kern: Kernel,
) -> Result<VwmacdBatchOutput, VwmacdError> {
    vwmacd_batch_inner(close, volume, sweep, kern, false)
}

#[inline(always)]
pub fn vwmacd_batch_par_slice(
    close: &[f64],
    volume: &[f64],
    sweep: &VwmacdBatchRange,
    kern: Kernel,
) -> Result<VwmacdBatchOutput, VwmacdError> {
    vwmacd_batch_inner(close, volume, sweep, kern, true)
}

#[inline(always)]
fn vwmacd_batch_inner(
    close: &[f64],
    volume: &[f64],
    sweep: &VwmacdBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<VwmacdBatchOutput, VwmacdError> {
    let params = expand_grid(sweep)?;
    let len = close.len();
    if len == 0 {
        return Err(VwmacdError::EmptyInputData);
    }
    if volume.len() != len {
        return Err(VwmacdError::OutputLengthMismatch {
            expected: len,
            got: volume.len(),
        });
    }
    let rows = params.len();
    let cols = len;
    rows.checked_mul(cols)
        .ok_or_else(|| VwmacdError::InvalidRange {
            start: "rows".into(),
            end: "cols".into(),
            step: "mul".into(),
        })?;

    let first = first_valid_pair(close, volume).ok_or(VwmacdError::AllValuesNaN)?;

    let warmups: Vec<usize> = params
        .iter()
        .map(|p| {
            let f = p.fast_period.unwrap_or(12);
            let s = p.slow_period.unwrap_or(26);
            let g = p.signal_period.unwrap_or(9);
            first + f.max(s) - 1 + g - 1
        })
        .collect();

    let mut macd_mu = make_uninit_matrix(rows, cols);
    let mut signal_mu = make_uninit_matrix(rows, cols);
    let mut hist_mu = make_uninit_matrix(rows, cols);

    unsafe {
        init_matrix_prefixes(
            &mut macd_mu,
            cols,
            &params
                .iter()
                .map(|p| {
                    let f = p.fast_period.unwrap_or(12);
                    let s = p.slow_period.unwrap_or(26);
                    first + f.max(s) - 1
                })
                .collect::<Vec<_>>(),
        );
        init_matrix_prefixes(&mut signal_mu, cols, &warmups);
        init_matrix_prefixes(&mut hist_mu, cols, &warmups);
    }

    let actual = match kern {
        Kernel::Auto => detect_best_batch_kernel(),
        k => k,
    };
    let simd = match actual {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        k => k,
    };

    let do_row = |row: usize,
                  macd_row_mu: &mut [MaybeUninit<f64>],
                  signal_row_mu: &mut [MaybeUninit<f64>],
                  hist_row_mu: &mut [MaybeUninit<f64>]| {
        let p = &params[row];
        let f = p.fast_period.unwrap();
        let s = p.slow_period.unwrap();
        let g = p.signal_period.unwrap();
        let fmt = p.fast_ma_type.as_deref().unwrap_or("sma");
        let smt = p.slow_ma_type.as_deref().unwrap_or("sma");
        let sigt = p.signal_ma_type.as_deref().unwrap_or("ema");

        let macd_row = unsafe {
            std::slice::from_raw_parts_mut(macd_row_mu.as_mut_ptr() as *mut f64, macd_row_mu.len())
        };
        let signal_row = unsafe {
            std::slice::from_raw_parts_mut(
                signal_row_mu.as_mut_ptr() as *mut f64,
                signal_row_mu.len(),
            )
        };
        let hist_row = unsafe {
            std::slice::from_raw_parts_mut(hist_row_mu.as_mut_ptr() as *mut f64, hist_row_mu.len())
        };

        let macd_warmup_abs = first + f.max(s) - 1;
        let total_warmup_abs = macd_warmup_abs + g - 1;

        vwmacd_compute_into(
            close,
            volume,
            f,
            s,
            g,
            fmt,
            smt,
            sigt,
            first,
            macd_warmup_abs,
            total_warmup_abs,
            simd,
            macd_row,
            signal_row,
            hist_row,
        )
        .unwrap();
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            macd_mu
                .par_chunks_mut(cols)
                .zip(signal_mu.par_chunks_mut(cols))
                .zip(hist_mu.par_chunks_mut(cols))
                .enumerate()
                .for_each(|(row, ((m, s), h))| do_row(row, m, s, h));
        }
        #[cfg(target_arch = "wasm32")]
        {
            for (row, ((m, s), h)) in macd_mu
                .chunks_mut(cols)
                .zip(signal_mu.chunks_mut(cols))
                .zip(hist_mu.chunks_mut(cols))
                .enumerate()
            {
                do_row(row, m, s, h);
            }
        }
    } else {
        for (row, ((m, s), h)) in macd_mu
            .chunks_mut(cols)
            .zip(signal_mu.chunks_mut(cols))
            .zip(hist_mu.chunks_mut(cols))
            .enumerate()
        {
            do_row(row, m, s, h);
        }
    }

    let mut mdrop = core::mem::ManuallyDrop::new(macd_mu);
    let macd = unsafe {
        Vec::from_raw_parts(
            mdrop.as_mut_ptr() as *mut f64,
            mdrop.len(),
            mdrop.capacity(),
        )
    };
    let mut sdrop = core::mem::ManuallyDrop::new(signal_mu);
    let signal = unsafe {
        Vec::from_raw_parts(
            sdrop.as_mut_ptr() as *mut f64,
            sdrop.len(),
            sdrop.capacity(),
        )
    };
    let mut hdrop = core::mem::ManuallyDrop::new(hist_mu);
    let hist = unsafe {
        Vec::from_raw_parts(
            hdrop.as_mut_ptr() as *mut f64,
            hdrop.len(),
            hdrop.capacity(),
        )
    };

    Ok(VwmacdBatchOutput {
        macd,
        signal,
        hist,
        params,
        rows,
        cols,
    })
}

#[inline(always)]
fn vwmacd_batch_inner_into(
    close: &[f64],
    volume: &[f64],
    sweep: &VwmacdBatchRange,
    kern: Kernel,
    parallel: bool,
    macd_out: &mut [f64],
    signal_out: &mut [f64],
    hist_out: &mut [f64],
) -> Result<Vec<VwmacdParams>, VwmacdError> {
    let combos = expand_grid(sweep)?;
    let rows = combos.len();
    let cols = close.len();

    if cols == 0 {
        return Err(VwmacdError::EmptyInputData);
    }
    if volume.len() != cols {
        return Err(VwmacdError::OutputLengthMismatch {
            expected: cols,
            got: volume.len(),
        });
    }

    let expected = rows
        .checked_mul(cols)
        .ok_or_else(|| VwmacdError::InvalidRange {
            start: "rows".into(),
            end: "cols".into(),
            step: "mul".into(),
        })?;
    if macd_out.len() != expected || signal_out.len() != expected || hist_out.len() != expected {
        let got = macd_out.len().min(signal_out.len()).min(hist_out.len());
        return Err(VwmacdError::OutputLengthMismatch { expected, got });
    }

    let first = first_valid_pair(close, volume).ok_or(VwmacdError::AllValuesNaN)?;

    let macd_mu = unsafe {
        std::slice::from_raw_parts_mut(
            macd_out.as_mut_ptr() as *mut MaybeUninit<f64>,
            macd_out.len(),
        )
    };
    let signal_mu = unsafe {
        std::slice::from_raw_parts_mut(
            signal_out.as_mut_ptr() as *mut MaybeUninit<f64>,
            signal_out.len(),
        )
    };
    let hist_mu = unsafe {
        std::slice::from_raw_parts_mut(
            hist_out.as_mut_ptr() as *mut MaybeUninit<f64>,
            hist_out.len(),
        )
    };

    let macd_warmups: Vec<usize> = combos
        .iter()
        .map(|p| {
            let f = p.fast_period.unwrap_or(12);
            let s = p.slow_period.unwrap_or(26);
            first + f.max(s) - 1
        })
        .collect();
    let total_warmups: Vec<usize> = combos
        .iter()
        .map(|p| {
            let f = p.fast_period.unwrap_or(12);
            let s = p.slow_period.unwrap_or(26);
            let g = p.signal_period.unwrap_or(9);
            first + f.max(s) - 1 + g - 1
        })
        .collect();

    unsafe {
        init_matrix_prefixes(macd_mu, cols, &macd_warmups);
        init_matrix_prefixes(signal_mu, cols, &total_warmups);
        init_matrix_prefixes(hist_mu, cols, &total_warmups);
    }

    let actual = match kern {
        Kernel::Auto => detect_best_batch_kernel(),
        k => k,
    };
    let simd = match actual {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        k => k,
    };

    let do_row = |row: usize,
                  m: &mut [MaybeUninit<f64>],
                  s: &mut [MaybeUninit<f64>],
                  h: &mut [MaybeUninit<f64>]| {
        let p = &combos[row];
        let f = p.fast_period.unwrap();
        let sl = p.slow_period.unwrap();
        let g = p.signal_period.unwrap();
        let fmt = p.fast_ma_type.as_deref().unwrap_or("sma");
        let smt = p.slow_ma_type.as_deref().unwrap_or("sma");
        let sigt = p.signal_ma_type.as_deref().unwrap_or("ema");

        let macd_row = unsafe { std::slice::from_raw_parts_mut(m.as_mut_ptr() as *mut f64, cols) };
        let signal_row =
            unsafe { std::slice::from_raw_parts_mut(s.as_mut_ptr() as *mut f64, cols) };
        let hist_row = unsafe { std::slice::from_raw_parts_mut(h.as_mut_ptr() as *mut f64, cols) };

        let macd_warmup_abs = macd_warmups[row];
        let total_warmup_abs = total_warmups[row];

        vwmacd_compute_into(
            close,
            volume,
            f,
            sl,
            g,
            fmt,
            smt,
            sigt,
            first,
            macd_warmup_abs,
            total_warmup_abs,
            simd,
            macd_row,
            signal_row,
            hist_row,
        )
        .unwrap();
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            macd_mu
                .par_chunks_mut(cols)
                .zip(signal_mu.par_chunks_mut(cols))
                .zip(hist_mu.par_chunks_mut(cols))
                .enumerate()
                .for_each(|(row, ((m, s), h))| do_row(row, m, s, h));
        }
        #[cfg(target_arch = "wasm32")]
        {
            for (row, ((m, s), h)) in macd_mu
                .chunks_mut(cols)
                .zip(signal_mu.chunks_mut(cols))
                .zip(hist_mu.chunks_mut(cols))
                .enumerate()
            {
                do_row(row, m, s, h);
            }
        }
    } else {
        for (row, ((m, s), h)) in macd_mu
            .chunks_mut(cols)
            .zip(signal_mu.chunks_mut(cols))
            .zip(hist_mu.chunks_mut(cols))
            .enumerate()
        {
            do_row(row, m, s, h);
        }
    }

    Ok(combos)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = vwmacd_unified)]
pub fn vwmacd_unified_js(
    close: &[f64],
    volume: &[f64],
    fast_period: usize,
    slow_period: usize,
    signal_period: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<JsValue, JsValue> {
    let params = VwmacdParams {
        fast_period: Some(fast_period),
        slow_period: Some(slow_period),
        signal_period: Some(signal_period),
        fast_ma_type: Some(fast_ma_type.to_string()),
        slow_ma_type: Some(slow_ma_type.to_string()),
        signal_ma_type: Some(signal_ma_type.to_string()),
    };
    let input = VwmacdInput::from_slices(close, volume, params);
    let (c, v, f, s, g, fmt, smt, sigt, first, macd_warmup_abs, total_warmup_abs, k) =
        vwmacd_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;

    let mut macd = alloc_with_nan_prefix(close.len(), macd_warmup_abs);
    let mut signal = alloc_with_nan_prefix(close.len(), total_warmup_abs);
    let mut hist = alloc_with_nan_prefix(close.len(), total_warmup_abs);

    vwmacd_compute_into(
        c,
        v,
        f,
        s,
        g,
        fmt,
        smt,
        sigt,
        first,
        macd_warmup_abs,
        total_warmup_abs,
        k,
        &mut macd,
        &mut signal,
        &mut hist,
    )
    .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let out = VwmacdJsOutput { macd, signal, hist };
    serde_wasm_bindgen::to_value(&out)
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn vwmacd_js(
    close: &[f64],
    volume: &[f64],
    fast_period: usize,
    slow_period: usize,
    signal_period: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<Vec<f64>, JsValue> {
    if close.len() != volume.len() {
        return Err(JsValue::from_str(
            "Close and volume arrays must have the same length",
        ));
    }

    let params = VwmacdParams {
        fast_period: Some(fast_period),
        slow_period: Some(slow_period),
        signal_period: Some(signal_period),
        fast_ma_type: Some(fast_ma_type.to_string()),
        slow_ma_type: Some(slow_ma_type.to_string()),
        signal_ma_type: Some(signal_ma_type.to_string()),
    };
    let input = VwmacdInput::from_slices(close, volume, params);

    let (
        close_data,
        volume_data,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        first,
        macd_warmup,
        total_warmup,
        kernel_enum,
    ) = vwmacd_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;

    let mut macd = alloc_with_nan_prefix(close.len(), macd_warmup);
    let mut signal_vec = alloc_with_nan_prefix(close.len(), total_warmup);
    let mut hist = alloc_with_nan_prefix(close.len(), total_warmup);

    vwmacd_compute_into(
        close_data,
        volume_data,
        fast,
        slow,
        signal,
        fast_ma_type,
        slow_ma_type,
        signal_ma_type,
        first,
        macd_warmup,
        total_warmup,
        kernel_enum,
        &mut macd,
        &mut signal_vec,
        &mut hist,
    )
    .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let mut result = Vec::with_capacity(close.len() * 3);
    result.extend_from_slice(&macd);
    result.extend_from_slice(&signal_vec);
    result.extend_from_slice(&hist);

    Ok(result)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn vwmacd_alloc(len: usize) -> *mut f64 {
    let mut vec = Vec::<f64>::with_capacity(len);
    let ptr = vec.as_mut_ptr();
    std::mem::forget(vec);
    ptr
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn vwmacd_free(ptr: *mut f64, len: usize) {
    if !ptr.is_null() {
        unsafe {
            let _ = Vec::from_raw_parts(ptr, len, len);
        }
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn vwmacd_into(
    close_ptr: *const f64,
    volume_ptr: *const f64,
    macd_ptr: *mut f64,
    signal_ptr: *mut f64,
    hist_ptr: *mut f64,
    len: usize,
    fast_period: usize,
    slow_period: usize,
    signal_period: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
) -> Result<(), JsValue> {
    if close_ptr.is_null()
        || volume_ptr.is_null()
        || macd_ptr.is_null()
        || signal_ptr.is_null()
        || hist_ptr.is_null()
    {
        return Err(JsValue::from_str("Null pointer provided"));
    }

    unsafe {
        let close = std::slice::from_raw_parts(close_ptr, len);
        let volume = std::slice::from_raw_parts(volume_ptr, len);
        let macd = std::slice::from_raw_parts_mut(macd_ptr, len);
        let signal = std::slice::from_raw_parts_mut(signal_ptr, len);
        let hist = std::slice::from_raw_parts_mut(hist_ptr, len);

        let params = VwmacdParams {
            fast_period: Some(fast_period),
            slow_period: Some(slow_period),
            signal_period: Some(signal_period),
            fast_ma_type: Some(fast_ma_type.to_string()),
            slow_ma_type: Some(slow_ma_type.to_string()),
            signal_ma_type: Some(signal_ma_type.to_string()),
        };
        let input = VwmacdInput::from_slices(close, volume, params);

        let (c, v, f, s, g, fmt, smt, sigt, first, macd_warmup_abs, total_warmup_abs, k) =
            vwmacd_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;

        vwmacd_compute_into(
            c,
            v,
            f,
            s,
            g,
            fmt,
            smt,
            sigt,
            first,
            macd_warmup_abs,
            total_warmup_abs,
            k,
            macd,
            signal,
            hist,
        )
        .map_err(|e| JsValue::from_str(&e.to_string()))
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = vwmacd_batch)]
pub fn vwmacd_batch_unified_js(
    close: &[f64],
    volume: &[f64],
    config: JsValue,
) -> Result<JsValue, JsValue> {
    let cfg: VwmacdBatchConfig = serde_wasm_bindgen::from_value(config)
        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;

    let sweep = VwmacdBatchRange {
        fast: cfg.fast_range,
        slow: cfg.slow_range,
        signal: cfg.signal_range,
        fast_ma_type: cfg.fast_ma_type.unwrap_or_else(|| "sma".into()),
        slow_ma_type: cfg.slow_ma_type.unwrap_or_else(|| "sma".into()),
        signal_ma_type: cfg.signal_ma_type.unwrap_or_else(|| "ema".into()),
    };

    let out = vwmacd_batch_inner(close, volume, &sweep, detect_best_kernel(), false)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let mut values = Vec::with_capacity(out.macd.len() + out.signal.len() + out.hist.len());
    values.extend_from_slice(&out.macd);
    values.extend_from_slice(&out.signal);
    values.extend_from_slice(&out.hist);

    let js = VwmacdBatchJsOutput {
        values,
        combos: out.params,
        rows: out.rows,
        cols: out.cols,
    };
    serde_wasm_bindgen::to_value(&js)
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[cfg(feature = "python")]
#[pyfunction(name = "vwmacd")]
#[pyo3(signature=(close, volume, fast, slow, signal, fast_ma_type="sma", slow_ma_type="sma", signal_ma_type="ema", kernel=None))]
pub fn vwmacd_py<'py>(
    py: Python<'py>,
    close: PyReadonlyArray1<'py, f64>,
    volume: PyReadonlyArray1<'py, f64>,
    fast: usize,
    slow: usize,
    signal: usize,
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    kernel: Option<&str>,
) -> PyResult<(
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
)> {
    let close = close.as_slice()?;
    let volume = volume.as_slice()?;
    let params = VwmacdParams {
        fast_period: Some(fast),
        slow_period: Some(slow),
        signal_period: Some(signal),
        fast_ma_type: Some(fast_ma_type.to_string()),
        slow_ma_type: Some(slow_ma_type.to_string()),
        signal_ma_type: Some(signal_ma_type.to_string()),
    };
    let input = VwmacdInput::from_slices(close, volume, params);
    let kern = validate_kernel(kernel, false)?;

    let macd_arr = unsafe { PyArray1::<f64>::new(py, [close.len()], false) };
    let signal_arr = unsafe { PyArray1::<f64>::new(py, [close.len()], false) };
    let hist_arr = unsafe { PyArray1::<f64>::new(py, [close.len()], false) };

    let macd_slice = unsafe { macd_arr.as_slice_mut()? };
    let signal_slice = unsafe { signal_arr.as_slice_mut()? };
    let hist_slice = unsafe { hist_arr.as_slice_mut()? };

    py.allow_threads(|| vwmacd_into_slice(macd_slice, signal_slice, hist_slice, &input, kern))
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    Ok((macd_arr, signal_arr, hist_arr))
}

#[cfg(feature = "python")]
#[pyclass(name = "VwmacdStream")]
pub struct VwmacdStreamPy {
    stream: VwmacdStream,
}

#[cfg(feature = "python")]
#[pymethods]
impl VwmacdStreamPy {
    #[new]
    #[pyo3(signature = (fast_period=None, slow_period=None, signal_period=None, fast_ma_type=None, slow_ma_type=None, signal_ma_type=None))]
    fn new(
        fast_period: Option<usize>,
        slow_period: Option<usize>,
        signal_period: Option<usize>,
        fast_ma_type: Option<&str>,
        slow_ma_type: Option<&str>,
        signal_ma_type: Option<&str>,
    ) -> PyResult<Self> {
        let params = VwmacdParams {
            fast_period,
            slow_period,
            signal_period,
            fast_ma_type: fast_ma_type.map(|s| s.to_string()),
            slow_ma_type: slow_ma_type.map(|s| s.to_string()),
            signal_ma_type: signal_ma_type.map(|s| s.to_string()),
        };

        let stream =
            VwmacdStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;

        Ok(VwmacdStreamPy { stream })
    }

    fn update(&mut self, close: f64, volume: f64) -> (Option<f64>, Option<f64>, Option<f64>) {
        match self.stream.update(close, volume) {
            Some((macd, signal, hist)) => (Some(macd), Some(signal), Some(hist)),
            None => (None, None, None),
        }
    }
}

#[cfg(feature = "python")]
#[pyfunction(name = "vwmacd_batch")]
#[pyo3(signature=(close, volume, fast_range, slow_range, signal_range, fast_ma_type="sma", slow_ma_type="sma", signal_ma_type="ema", kernel=None))]
pub fn vwmacd_batch_py<'py>(
    py: Python<'py>,
    close: PyReadonlyArray1<'py, f64>,
    volume: PyReadonlyArray1<'py, f64>,
    fast_range: (usize, usize, usize),
    slow_range: (usize, usize, usize),
    signal_range: (usize, usize, usize),
    fast_ma_type: &str,
    slow_ma_type: &str,
    signal_ma_type: &str,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    let close = close.as_slice()?;
    let volume = volume.as_slice()?;

    let sweep = VwmacdBatchRange {
        fast: fast_range,
        slow: slow_range,
        signal: signal_range,
        fast_ma_type: fast_ma_type.to_string(),
        slow_ma_type: slow_ma_type.to_string(),
        signal_ma_type: signal_ma_type.to_string(),
    };
    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
    let rows = combos.len();
    let cols = close.len();
    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("vwmacd_batch: rows*cols overflow".to_string()))?;

    let macd_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let signal_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let hist_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };

    let macd_slice = unsafe { macd_arr.as_slice_mut()? };
    let signal_slice = unsafe { signal_arr.as_slice_mut()? };
    let hist_slice = unsafe { hist_arr.as_slice_mut()? };

    let kern = validate_kernel(kernel, true)?;
    py.allow_threads(|| {
        let simd = match kern {
            Kernel::Auto => detect_best_batch_kernel(),
            k => k,
        };
        vwmacd_batch_inner_into(
            close,
            volume,
            &sweep,
            simd,
            true,
            macd_slice,
            signal_slice,
            hist_slice,
        )
    })
    .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let d = PyDict::new(py);
    d.set_item("macd", macd_arr.reshape((rows, cols))?)?;
    d.set_item("signal", signal_arr.reshape((rows, cols))?)?;
    d.set_item("hist", hist_arr.reshape((rows, cols))?)?;
    d.set_item(
        "fast_periods",
        combos
            .iter()
            .map(|p| p.fast_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "slow_periods",
        combos
            .iter()
            .map(|p| p.slow_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "signal_periods",
        combos
            .iter()
            .map(|p| p.signal_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "fast_ma_types",
        combos
            .iter()
            .map(|p| p.fast_ma_type.as_deref().unwrap_or("sma"))
            .collect::<Vec<_>>(),
    )?;
    d.set_item(
        "slow_ma_types",
        combos
            .iter()
            .map(|p| p.slow_ma_type.as_deref().unwrap_or("sma"))
            .collect::<Vec<_>>(),
    )?;
    d.set_item(
        "signal_ma_types",
        combos
            .iter()
            .map(|p| p.signal_ma_type.as_deref().unwrap_or("ema"))
            .collect::<Vec<_>>(),
    )?;
    Ok(d)
}

#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::{cuda_available, CudaVwmacd};
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::utilities::dlpack_cuda::make_device_array_py;

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "vwmacd_cuda_batch_dev")]
#[pyo3(signature = (close_f32, volume_f32, fast_range, slow_range, signal_range, device_id=0))]
pub fn vwmacd_cuda_batch_dev_py<'py>(
    py: Python<'py>,
    close_f32: numpy::PyReadonlyArray1<'py, f32>,
    volume_f32: numpy::PyReadonlyArray1<'py, f32>,
    fast_range: (usize, usize, usize),
    slow_range: (usize, usize, usize),
    signal_range: (usize, usize, usize),
    device_id: usize,
) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
    use numpy::IntoPyArray;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let prices = close_f32.as_slice()?;
    let volumes = volume_f32.as_slice()?;
    let sweep = VwmacdBatchRange {
        fast: fast_range,
        slow: slow_range,
        signal: signal_range,
        fast_ma_type: "sma".to_string(),
        slow_ma_type: "sma".to_string(),
        signal_ma_type: "ema".to_string(),
    };

    let ((macd_buf, signal_buf, hist_buf), combos) = py.allow_threads(|| {
        let cuda = CudaVwmacd::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        cuda.vwmacd_batch_dev(prices, volumes, &sweep)
            .map(|(triplet, combos)| ((triplet.macd, triplet.signal, triplet.hist), combos))
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    let dict = pyo3::types::PyDict::new(py);
    let macd_dev = make_device_array_py(device_id, macd_buf)?;
    dict.set_item("macd", Py::new(py, macd_dev)?)?;
    let signal_dev = make_device_array_py(device_id, signal_buf)?;
    dict.set_item("signal", Py::new(py, signal_dev)?)?;
    let hist_dev = make_device_array_py(device_id, hist_buf)?;
    dict.set_item("hist", Py::new(py, hist_dev)?)?;
    dict.set_item("rows", combos.len())?;
    dict.set_item("cols", prices.len())?;
    dict.set_item(
        "fasts",
        combos
            .iter()
            .map(|c| c.fast_period.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "slows",
        combos
            .iter()
            .map(|c| c.slow_period.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "signals",
        combos
            .iter()
            .map(|c| c.signal_period.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict)
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "vwmacd_cuda_many_series_one_param_dev")]
#[pyo3(signature = (prices_tm_f32, volumes_tm_f32, fast, slow, signal, device_id=0))]
pub fn vwmacd_cuda_many_series_one_param_dev_py<'py>(
    py: Python<'py>,
    prices_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
    volumes_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
    fast: usize,
    slow: usize,
    signal: usize,
    device_id: usize,
) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
    use numpy::PyUntypedArrayMethods;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let ps = prices_tm_f32.shape();
    let vs = volumes_tm_f32.shape();
    if ps.len() != 2 || vs.len() != 2 || ps != vs {
        return Err(PyValueError::new_err(
            "expected two 2D arrays with same shape",
        ));
    }
    let rows = ps[0];
    let cols = ps[1];
    let p = prices_tm_f32.as_slice()?;
    let v = volumes_tm_f32.as_slice()?;
    let params = VwmacdParams {
        fast_period: Some(fast),
        slow_period: Some(slow),
        signal_period: Some(signal),
        fast_ma_type: Some("sma".into()),
        slow_ma_type: Some("sma".into()),
        signal_ma_type: Some("ema".into()),
    };

    let (macd_buf, signal_buf, hist_buf) = py.allow_threads(|| {
        let cuda = CudaVwmacd::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        cuda.vwmacd_many_series_one_param_time_major_dev(p, v, cols, rows, &params)
            .map(|triplet| (triplet.macd, triplet.signal, triplet.hist))
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    let dict = pyo3::types::PyDict::new(py);
    let macd_dev = make_device_array_py(device_id, macd_buf)?;
    dict.set_item("macd", Py::new(py, macd_dev)?)?;
    let signal_dev = make_device_array_py(device_id, signal_buf)?;
    dict.set_item("signal", Py::new(py, signal_dev)?)?;
    let hist_dev = make_device_array_py(device_id, hist_buf)?;
    dict.set_item("hist", Py::new(py, hist_dev)?)?;
    dict.set_item("rows", rows)?;
    dict.set_item("cols", cols)?;
    dict.set_item("fast", fast)?;
    dict.set_item("slow", slow)?;
    dict.set_item("signal_len", signal)?;
    Ok(dict)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::skip_if_unsupported;
    use crate::utilities::data_loader::read_candles_from_csv;

    fn check_vwmacd_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let default_params = VwmacdParams {
            fast_period: None,
            slow_period: None,
            signal_period: None,
            fast_ma_type: None,
            slow_ma_type: None,
            signal_ma_type: None,
        };
        let input = VwmacdInput::from_candles(&candles, "close", "volume", default_params);
        let output = vwmacd_with_kernel(&input, kernel)?;
        assert_eq!(output.macd.len(), candles.close.len());
        Ok(())
    }

    fn check_vwmacd_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let input = VwmacdInput::with_default_candles(&candles);
        let result = vwmacd_with_kernel(&input, kernel)?;

        let expected_macd = [
            -394.95161155,
            -508.29106210,
            -490.70190723,
            -388.94996199,
            -341.13720646,
        ];

        let expected_signal = [
            -539.48861567,
            -533.24910496,
            -524.73966541,
            -497.58172247,
            -466.29282108,
        ];

        let expected_histogram = [
            144.53700412,
            24.95804286,
            34.03775818,
            108.63176274,
            125.15561462,
        ];

        let last_five_macd = &result.macd[result.macd.len().saturating_sub(5)..];
        for (i, &val) in last_five_macd.iter().enumerate() {
            assert!(
                (val - expected_macd[i]).abs() < 2e-4,
                "[{}] MACD mismatch at idx {}: got {}, expected {}",
                test_name,
                i,
                val,
                expected_macd[i]
            );
        }

        let last_five_signal = &result.signal[result.signal.len().saturating_sub(5)..];
        for (i, &val) in last_five_signal.iter().enumerate() {
            assert!(
                (val - expected_signal[i]).abs() < 2e-4,
                "[{}] Signal mismatch at idx {}: got {}, expected {}",
                test_name,
                i,
                val,
                expected_signal[i]
            );
        }

        let last_five_hist = &result.hist[result.hist.len().saturating_sub(5)..];
        for (i, &val) in last_five_hist.iter().enumerate() {
            assert!(
                (val - expected_histogram[i]).abs() < 2e-4,
                "[{}] Histogram mismatch at idx {}: got {}, expected {}",
                test_name,
                i,
                val,
                expected_histogram[i]
            );
        }

        Ok(())
    }
    fn check_vwmacd_with_custom_ma_types(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let params = VwmacdParams {
            fast_period: Some(12),
            slow_period: Some(26),
            signal_period: Some(9),
            fast_ma_type: Some("ema".to_string()),
            slow_ma_type: Some("wma".to_string()),
            signal_ma_type: Some("sma".to_string()),
        };
        let input = VwmacdInput::from_candles(&candles, "close", "volume", params);
        let output = vwmacd_with_kernel(&input, kernel)?;
        assert_eq!(output.macd.len(), candles.close.len());

        let default_input = VwmacdInput::with_default_candles(&candles);
        let default_output = vwmacd_with_kernel(&default_input, kernel)?;

        let different_count = output
            .macd
            .iter()
            .zip(&default_output.macd)
            .skip(50)
            .filter(|(&a, &b)| !a.is_nan() && !b.is_nan() && (a - b).abs() > 1e-10)
            .count();

        assert!(
            different_count > 0,
            "Custom MA types should produce different results"
        );
        Ok(())
    }

    fn check_vwmacd_nan_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let close = [f64::NAN, f64::NAN];
        let volume = [f64::NAN, f64::NAN];
        let params = VwmacdParams::default();
        let input = VwmacdInput::from_slices(&close, &volume, params);
        let result = vwmacd_with_kernel(&input, kernel);
        assert!(result.is_err());
        Ok(())
    }

    fn check_vwmacd_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let close = [10.0, 20.0, 30.0];
        let volume = [1.0, 1.0, 1.0];
        let params = VwmacdParams {
            fast_period: Some(0),
            slow_period: Some(26),
            signal_period: Some(9),
            fast_ma_type: None,
            slow_ma_type: None,
            signal_ma_type: None,
        };
        let input = VwmacdInput::from_slices(&close, &volume, params);
        let result = vwmacd_with_kernel(&input, kernel);
        assert!(result.is_err());
        Ok(())
    }

    fn check_vwmacd_period_exceeds(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let close = [10.0, 20.0, 30.0];
        let volume = [100.0, 200.0, 300.0];
        let params = VwmacdParams {
            fast_period: Some(12),
            slow_period: Some(26),
            signal_period: Some(9),
            fast_ma_type: None,
            slow_ma_type: None,
            signal_ma_type: None,
        };
        let input = VwmacdInput::from_slices(&close, &volume, params);
        let result = vwmacd_with_kernel(&input, kernel);
        assert!(result.is_err());
        Ok(())
    }

    macro_rules! generate_all_vwmacd_tests {
        ($($test_fn:ident),*) => {
            paste::paste! {
                $(
                    #[test]
                    fn [<$test_fn _scalar_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
                    }
                )*
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                $(
                    #[test]
                    fn [<$test_fn _avx2_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
                    }
                    #[test]
                    fn [<$test_fn _avx512_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
                    }
                )*
                #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
                $(
                    #[test]
                    fn [<$test_fn _simd128_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _simd128_f64>]), Kernel::Scalar);
                    }
                )*
            }
        }
    }
    #[cfg(debug_assertions)]
    fn check_vwmacd_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let test_params = vec![
            VwmacdParams::default(),
            VwmacdParams {
                fast_period: Some(2),
                slow_period: Some(3),
                signal_period: Some(2),
                fast_ma_type: Some("sma".to_string()),
                slow_ma_type: Some("sma".to_string()),
                signal_ma_type: Some("ema".to_string()),
            },
            VwmacdParams {
                fast_period: Some(5),
                slow_period: Some(10),
                signal_period: Some(3),
                fast_ma_type: Some("ema".to_string()),
                slow_ma_type: Some("ema".to_string()),
                signal_ma_type: Some("sma".to_string()),
            },
            VwmacdParams {
                fast_period: Some(10),
                slow_period: Some(20),
                signal_period: Some(5),
                fast_ma_type: Some("wma".to_string()),
                slow_ma_type: Some("sma".to_string()),
                signal_ma_type: Some("ema".to_string()),
            },
            VwmacdParams {
                fast_period: Some(12),
                slow_period: Some(26),
                signal_period: Some(9),
                fast_ma_type: Some("sma".to_string()),
                slow_ma_type: Some("sma".to_string()),
                signal_ma_type: Some("ema".to_string()),
            },
            VwmacdParams {
                fast_period: Some(20),
                slow_period: Some(40),
                signal_period: Some(10),
                fast_ma_type: Some("ema".to_string()),
                slow_ma_type: Some("wma".to_string()),
                signal_ma_type: Some("sma".to_string()),
            },
            VwmacdParams {
                fast_period: Some(50),
                slow_period: Some(100),
                signal_period: Some(20),
                fast_ma_type: Some("sma".to_string()),
                slow_ma_type: Some("ema".to_string()),
                signal_ma_type: Some("wma".to_string()),
            },
            VwmacdParams {
                fast_period: Some(25),
                slow_period: Some(26),
                signal_period: Some(9),
                fast_ma_type: Some("ema".to_string()),
                slow_ma_type: Some("ema".to_string()),
                signal_ma_type: Some("ema".to_string()),
            },
            VwmacdParams {
                fast_period: Some(8),
                slow_period: Some(21),
                signal_period: Some(5),
                fast_ma_type: Some("wma".to_string()),
                slow_ma_type: Some("wma".to_string()),
                signal_ma_type: Some("wma".to_string()),
            },
            VwmacdParams {
                fast_period: Some(15),
                slow_period: Some(30),
                signal_period: Some(15),
                fast_ma_type: Some("sma".to_string()),
                slow_ma_type: Some("wma".to_string()),
                signal_ma_type: Some("ema".to_string()),
            },
        ];

        for (param_idx, params) in test_params.iter().enumerate() {
            let input = VwmacdInput::from_candles(&candles, "close", "volume", params.clone());
            let output = vwmacd_with_kernel(&input, kernel)?;

            for (i, &val) in output.macd.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();

                if bits == 0x11111111_11111111 {
                    panic!(
						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) in MACD at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }

                if bits == 0x22222222_22222222 {
                    panic!(
						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) in MACD at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }

                if bits == 0x33333333_33333333 {
                    panic!(
						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) in MACD at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }
            }

            for (i, &val) in output.signal.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();

                if bits == 0x11111111_11111111 {
                    panic!(
						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) in Signal at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }

                if bits == 0x22222222_22222222 {
                    panic!(
						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) in Signal at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }

                if bits == 0x33333333_33333333 {
                    panic!(
						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) in Signal at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }
            }

            for (i, &val) in output.hist.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();

                if bits == 0x11111111_11111111 {
                    panic!(
						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) in Histogram at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }

                if bits == 0x22222222_22222222 {
                    panic!(
						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) in Histogram at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }

                if bits == 0x33333333_33333333 {
                    panic!(
						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) in Histogram at index {} \
						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
						test_name, val, bits, i,
						params.fast_period.unwrap_or(12),
						params.slow_period.unwrap_or(26),
						params.signal_period.unwrap_or(9),
						params.fast_ma_type.as_deref().unwrap_or("sma"),
						params.slow_ma_type.as_deref().unwrap_or("sma"),
						params.signal_ma_type.as_deref().unwrap_or("ema"),
						param_idx
					);
                }
            }
        }

        Ok(())
    }

    #[cfg(not(debug_assertions))]
    fn check_vwmacd_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    #[cfg(feature = "proptest")]
    #[allow(clippy::float_cmp)]
    fn check_vwmacd_property(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        use proptest::prelude::*;
        skip_if_unsupported!(kernel, test_name);

        let strat = (2usize..=20, 5usize..=50, 2usize..=20, 0..3usize).prop_flat_map(
            |(fast, slow, signal, ma_variant)| {
                let slow = slow.max(fast + 1);
                let data_len = slow * 2 + signal;
                (
                    prop::collection::vec(
                        (100.0f64..10000.0f64).prop_filter("finite", |x| x.is_finite()),
                        data_len..400,
                    ),
                    prop::collection::vec(
                        (0.001f64..1000000.0f64)
                            .prop_filter("finite positive", |x| x.is_finite() && *x > 0.0),
                        data_len..400,
                    ),
                    Just(fast),
                    Just(slow),
                    Just(signal),
                    Just(ma_variant),
                )
            },
        );

        proptest::test_runner::TestRunner::default()
            .run(&strat, |(close, volume, fast, slow, signal, ma_variant)| {
                let len = close.len().min(volume.len());
                let close = &close[..len];
                let volume = &volume[..len];

                let (fast_ma, slow_ma, signal_ma) = match ma_variant {
                    0 => ("sma", "sma", "ema"),
                    1 => ("ema", "ema", "sma"),
                    _ => ("wma", "sma", "ema"),
                };

                let params = VwmacdParams {
                    fast_period: Some(fast),
                    slow_period: Some(slow),
                    signal_period: Some(signal),
                    fast_ma_type: Some(fast_ma.to_string()),
                    slow_ma_type: Some(slow_ma.to_string()),
                    signal_ma_type: Some(signal_ma.to_string()),
                };
                let input = VwmacdInput::from_slices(close, volume, params);

                let VwmacdOutput {
                    macd,
                    signal: sig,
                    hist,
                } = vwmacd_with_kernel(&input, kernel).unwrap();
                let VwmacdOutput {
                    macd: ref_macd,
                    signal: ref_sig,
                    hist: ref_hist,
                } = vwmacd_with_kernel(&input, Kernel::Scalar).unwrap();

                let params_fast = VwmacdParams {
                    fast_period: Some(fast),
                    slow_period: Some(fast),
                    signal_period: Some(2),
                    fast_ma_type: Some(fast_ma.to_string()),
                    slow_ma_type: Some(fast_ma.to_string()),
                    signal_ma_type: Some("sma".to_string()),
                };
                let input_fast = VwmacdInput::from_slices(close, volume, params_fast);
                let fast_vwma_result = vwmacd_with_kernel(&input_fast, Kernel::Scalar).unwrap();

                let macd_warmup = slow - 1;
                let signal_warmup = macd_warmup + signal - 1;
                let hist_warmup = signal_warmup;

                for i in 0..len {
                    let y_macd = macd[i];
                    let y_sig = sig[i];
                    let y_hist = hist[i];
                    let r_macd = ref_macd[i];
                    let r_sig = ref_sig[i];
                    let r_hist = ref_hist[i];

                    if y_macd.is_nan() != r_macd.is_nan() {
                        prop_assert!(
                            false,
                            "MACD NaN mismatch at index {}: test={} ref={}",
                            i,
                            y_macd.is_nan(),
                            r_macd.is_nan()
                        );
                    }
                    if y_sig.is_nan() != r_sig.is_nan() {
                        prop_assert!(
                            false,
                            "Signal NaN mismatch at index {}: test={} ref={}",
                            i,
                            y_sig.is_nan(),
                            r_sig.is_nan()
                        );
                    }
                    if y_hist.is_nan() != r_hist.is_nan() {
                        prop_assert!(
                            false,
                            "Histogram NaN mismatch at index {}: test={} ref={}",
                            i,
                            y_hist.is_nan(),
                            r_hist.is_nan()
                        );
                    }

                    if i >= hist_warmup {
                        prop_assert!(
                            y_macd.is_finite(),
                            "MACD not finite at index {}: {}",
                            i,
                            y_macd
                        );
                        prop_assert!(
                            y_sig.is_finite(),
                            "Signal not finite at index {}: {}",
                            i,
                            y_sig
                        );
                        prop_assert!(
                            y_hist.is_finite(),
                            "Histogram not finite at index {}: {}",
                            i,
                            y_hist
                        );
                    }

                    if y_macd.is_finite() && y_sig.is_finite() {
                        let expected_hist = y_macd - y_sig;
                        prop_assert!(
                            (y_hist - expected_hist).abs() <= 1e-9,
                            "Histogram mismatch at {}: {} vs {} (macd={}, signal={})",
                            i,
                            y_hist,
                            expected_hist,
                            y_macd,
                            y_sig
                        );
                    }

                    if !y_macd.is_finite() || !r_macd.is_finite() {
                        prop_assert!(
                            y_macd.to_bits() == r_macd.to_bits(),
                            "MACD finite/NaN mismatch at {}: {} vs {}",
                            i,
                            y_macd,
                            r_macd
                        );
                    } else {
                        let ulp_diff = y_macd.to_bits().abs_diff(r_macd.to_bits());
                        prop_assert!(
                            (y_macd - r_macd).abs() <= 1e-9 || ulp_diff <= 4,
                            "MACD mismatch at {}: {} vs {} (ULP={})",
                            i,
                            y_macd,
                            r_macd,
                            ulp_diff
                        );
                    }

                    if !y_sig.is_finite() || !r_sig.is_finite() {
                        prop_assert!(
                            y_sig.to_bits() == r_sig.to_bits(),
                            "Signal finite/NaN mismatch at {}: {} vs {}",
                            i,
                            y_sig,
                            r_sig
                        );
                    } else {
                        let ulp_diff = y_sig.to_bits().abs_diff(r_sig.to_bits());
                        prop_assert!(
                            (y_sig - r_sig).abs() <= 1e-9 || ulp_diff <= 4,
                            "Signal mismatch at {}: {} vs {} (ULP={})",
                            i,
                            y_sig,
                            r_sig,
                            ulp_diff
                        );
                    }

                    if !y_hist.is_finite() || !r_hist.is_finite() {
                        prop_assert!(
                            y_hist.to_bits() == r_hist.to_bits(),
                            "Histogram finite/NaN mismatch at {}: {} vs {}",
                            i,
                            y_hist,
                            r_hist
                        );
                    } else {
                        let ulp_diff = y_hist.to_bits().abs_diff(r_hist.to_bits());
                        prop_assert!(
                            (y_hist - r_hist).abs() <= 1e-9 || ulp_diff <= 4,
                            "Histogram mismatch at {}: {} vs {} (ULP={})",
                            i,
                            y_hist,
                            r_hist,
                            ulp_diff
                        );
                    }

                    if close.windows(2).all(|w| (w[0] - w[1]).abs() < f64::EPSILON)
                        && volume
                            .windows(2)
                            .all(|w| (w[0] - w[1]).abs() < f64::EPSILON)
                        && y_macd.is_finite()
                    {
                        prop_assert!(
							y_macd.abs() <= 1e-9,
							"MACD should be ~0 with constant prices and volumes, got {} at index {}", y_macd, i
						);
                    }

                    if volume[i] < 1.0 && y_macd.is_finite() {
                        prop_assert!(
                            y_macd.is_finite(),
                            "MACD should be finite even with small volume {} at index {}",
                            volume[i],
                            i
                        );
                    }

                    if y_macd.is_finite() && i >= slow - 1 {
                        let all_prices_min = close.iter().cloned().fold(f64::INFINITY, f64::min);
                        let all_prices_max =
                            close.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
                        let total_range = all_prices_max - all_prices_min;

                        prop_assert!(
                            y_macd.abs() <= total_range + 1e-6,
                            "MACD {} exceeds total price range {} at index {}",
                            y_macd.abs(),
                            total_range,
                            i
                        );
                    }
                }

                if len > slow * 2 {
                    let mut extreme_volume = volume.to_vec();

                    for i in (0..len).step_by(5) {
                        extreme_volume[i] *= 1000.0;
                    }

                    let params_extreme = VwmacdParams {
                        fast_period: Some(fast),
                        slow_period: Some(slow),
                        signal_period: Some(signal),
                        fast_ma_type: Some(fast_ma.to_string()),
                        slow_ma_type: Some(slow_ma.to_string()),
                        signal_ma_type: Some(signal_ma.to_string()),
                    };
                    let input_extreme =
                        VwmacdInput::from_slices(close, &extreme_volume, params_extreme);

                    let result = vwmacd_with_kernel(&input_extreme, kernel);
                    prop_assert!(result.is_ok(), "Should handle extreme volume ratios");

                    if let Ok(extreme_output) = result {
                        for i in hist_warmup..len {
                            if extreme_output.macd[i].is_finite() {
                                prop_assert!(
                                    extreme_output.macd[i].is_finite(),
                                    "MACD should be finite with extreme volumes at index {}",
                                    i
                                );
                            }
                        }
                    }
                }

                Ok(())
            })
            .unwrap();

        Ok(())
    }

    generate_all_vwmacd_tests!(
        check_vwmacd_partial_params,
        check_vwmacd_accuracy,
        check_vwmacd_with_custom_ma_types,
        check_vwmacd_nan_data,
        check_vwmacd_zero_period,
        check_vwmacd_period_exceeds,
        check_vwmacd_streaming,
        check_vwmacd_no_poison
    );

    #[cfg(feature = "proptest")]
    generate_all_vwmacd_tests!(check_vwmacd_property);

    fn check_vwmacd_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let fast_period = 12;
        let slow_period = 26;
        let signal_period = 9;
        let fast_ma_type = "sma";
        let slow_ma_type = "sma";
        let signal_ma_type = "ema";

        let params = VwmacdParams {
            fast_period: Some(fast_period),
            slow_period: Some(slow_period),
            signal_period: Some(signal_period),
            fast_ma_type: Some(fast_ma_type.to_string()),
            slow_ma_type: Some(slow_ma_type.to_string()),
            signal_ma_type: Some(signal_ma_type.to_string()),
        };
        let input = VwmacdInput::from_slices(&candles.close, &candles.volume, params.clone());
        let batch_output = vwmacd_with_kernel(&input, kernel)?;

        let mut stream = VwmacdStream::try_new(params)?;

        let mut stream_macd = Vec::with_capacity(candles.close.len());
        let mut stream_signal = Vec::with_capacity(candles.close.len());
        let mut stream_hist = Vec::with_capacity(candles.close.len());

        for i in 0..candles.close.len() {
            match stream.update(candles.close[i], candles.volume[i]) {
                Some((m, s, h)) => {
                    stream_macd.push(m);
                    stream_signal.push(s);
                    stream_hist.push(h);
                }
                None => {
                    stream_macd.push(f64::NAN);
                    stream_signal.push(f64::NAN);
                    stream_hist.push(f64::NAN);
                }
            }
        }

        assert_eq!(batch_output.macd.len(), stream_macd.len());
        assert_eq!(batch_output.signal.len(), stream_signal.len());
        assert_eq!(batch_output.hist.len(), stream_hist.len());

        let warmup = slow_period + 10;
        for i in warmup..stream_macd.len().min(warmup + 50) {
            let b = batch_output.macd[i];
            let s = stream_macd[i];

            if !b.is_nan() && !s.is_nan() {
                let diff = (b - s).abs();
                let avg = (b.abs() + s.abs()) / 2.0;
                let relative_diff = if avg > 1e-10 { diff / avg } else { diff };

                if relative_diff > 0.5 && diff > 10.0 {
                    eprintln!(
						"[{}] Warning: Large VWMACD streaming difference at idx {}: batch={}, stream={}, diff={}",
						test_name, i, b, s, diff
					);
                }
            }
        }

        for i in warmup..stream_signal.len().min(warmup + 50) {
            let b = batch_output.signal[i];
            let s = stream_signal[i];

            if !b.is_nan() && !s.is_nan() {
                let diff = (b - s).abs();
                let avg = (b.abs() + s.abs()) / 2.0;
                let relative_diff = if avg > 1e-10 { diff / avg } else { diff };

                if relative_diff > 0.5 && diff > 10.0 {
                    eprintln!(
						"[{}] Warning: Large signal streaming difference at idx {}: batch={}, stream={}, diff={}",
						test_name, i, b, s, diff
					);
                }
            }
        }

        let valid_macd_count = stream_macd
            .iter()
            .skip(warmup)
            .filter(|v| !v.is_nan())
            .count();
        let valid_signal_count = stream_signal
            .iter()
            .skip(warmup)
            .filter(|v| !v.is_nan())
            .count();

        assert!(
            valid_macd_count > 0,
            "[{}] VWMACD streaming produced no valid MACD values after warmup",
            test_name
        );
        assert!(
            valid_signal_count > 0,
            "[{}] VWMACD streaming produced no valid signal values after warmup",
            test_name
        );

        Ok(())
    }

    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);

        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;

        let close = &c.close;
        let volume = &c.volume;

        let output = VwmacdBatchBuilder::new()
            .kernel(kernel)
            .apply_slices(close, volume)?;

        let def = VwmacdParams::default();
        let (macd_row, signal_row, hist_row) =
            output.values_for(&def).expect("default row missing");
        assert_eq!(macd_row.len(), close.len());

        let expected_macd = [
            -394.95161155,
            -508.29106210,
            -490.70190723,
            -388.94996199,
            -341.13720646,
        ];
        let start = macd_row.len() - 5;
        for (i, &v) in macd_row[start..].iter().enumerate() {
            assert!(
                (v - expected_macd[i]).abs() < 1e-3,
                "[{test}] default-row MACD mismatch at idx {i}: got {v}, expected {}",
                expected_macd[i]
            );
        }

        let input = VwmacdInput::from_candles(&c, "close", "volume", def.clone());
        let result = vwmacd_with_kernel(&input, kernel)?;

        let expected_signal = [
            -539.48861567,
            -533.24910496,
            -524.73966541,
            -497.58172247,
            -466.29282108,
        ];
        let signal_slice = &result.signal[result.signal.len() - 5..];
        for (i, &v) in signal_slice.iter().enumerate() {
            assert!(
                (v - expected_signal[i]).abs() < 1e-3,
                "[{test}] default-row Signal mismatch at idx {i}: got {v}, expected {}",
                expected_signal[i]
            );
        }

        let expected_histogram = [
            144.53700412,
            24.95804286,
            34.03775818,
            108.63176274,
            125.15561462,
        ];
        let hist_slice = &result.hist[result.hist.len() - 5..];
        for (i, &v) in hist_slice.iter().enumerate() {
            assert!(
                (v - expected_histogram[i]).abs() < 1e-3,
                "[{test}] default-row Histogram mismatch at idx {i}: got {v}, expected {}",
                expected_histogram[i]
            );
        }

        Ok(())
    }

    fn check_batch_grid(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);

        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;

        let close = &c.close;
        let volume = &c.volume;

        let output = VwmacdBatchBuilder::new()
            .kernel(kernel)
            .fast_range(10, 14, 2)
            .slow_range(20, 26, 3)
            .signal_range(5, 9, 2)
            .apply_slices(close, volume)?;

        assert_eq!(output.cols, close.len());
        assert_eq!(output.rows, 3 * 3 * 3);

        let params = VwmacdParams {
            fast_period: Some(12),
            slow_period: Some(23),
            signal_period: Some(7),
            fast_ma_type: Some("sma".to_string()),
            slow_ma_type: Some("sma".to_string()),
            signal_ma_type: Some("ema".to_string()),
        };
        let (macd_row, signal_row, hist_row) =
            output.values_for(&params).expect("row for params missing");
        assert_eq!(macd_row.len(), close.len());
        Ok(())
    }

    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
    #[test]
    fn test_vwmacd_into_matches_api() -> Result<(), Box<dyn Error>> {
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let input = VwmacdInput::with_default_candles(&candles);

        let base = vwmacd(&input)?;

        let n = candles.close.len();
        let mut macd_out = vec![0.0; n];
        let mut signal_out = vec![0.0; n];
        let mut hist_out = vec![0.0; n];
        vwmacd_into(&input, &mut macd_out, &mut signal_out, &mut hist_out)?;

        assert_eq!(base.macd.len(), n);
        assert_eq!(base.signal.len(), n);
        assert_eq!(base.hist.len(), n);

        fn eq_or_both_nan(a: f64, b: f64) -> bool {
            (a.is_nan() && b.is_nan()) || (a == b)
        }

        for i in 0..n {
            assert!(
                eq_or_both_nan(base.macd[i], macd_out[i]),
                "MACD mismatch at {}: base={}, into={}",
                i,
                base.macd[i],
                macd_out[i]
            );
            assert!(
                eq_or_both_nan(base.signal[i], signal_out[i]),
                "Signal mismatch at {}: base={}, into={}",
                i,
                base.signal[i],
                signal_out[i]
            );
            assert!(
                eq_or_both_nan(base.hist[i], hist_out[i]),
                "Hist mismatch at {}: base={}, into={}",
                i,
                base.hist[i],
                hist_out[i]
            );
        }

        Ok(())
    }

    fn check_batch_param_map(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);

        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;

        let close = &c.close;
        let volume = &c.volume;

        let batch = VwmacdBatchBuilder::new()
            .kernel(kernel)
            .fast_range(12, 14, 1)
            .slow_range(26, 28, 1)
            .signal_range(9, 11, 1)
            .apply_slices(close, volume)?;

        for (ix, param) in batch.params.iter().enumerate() {
            let by_index = &batch.macd[ix * batch.cols..(ix + 1) * batch.cols];
            let (by_api_macd, by_api_signal, by_api_hist) = batch.values_for(param).unwrap();

            assert_eq!(by_index.len(), by_api_macd.len());
            for (i, (&x, &y)) in by_index.iter().zip(by_api_macd.iter()).enumerate() {
                if x.is_nan() && y.is_nan() {
                    continue;
                }
                assert!(
                    (x == y),
                    "[{}] param {:?}, mismatch at idx {}: got {}, expected {}",
                    test,
                    param,
                    i,
                    x,
                    y
                );
            }
        }
        Ok(())
    }

    fn check_batch_custom_ma_types(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);

        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;

        let close = &c.close;
        let volume = &c.volume;

        let output = VwmacdBatchBuilder::new()
            .kernel(kernel)
            .fast_ma_type("ema".to_string())
            .slow_ma_type("wma".to_string())
            .signal_ma_type("sma".to_string())
            .apply_slices(close, volume)?;

        let params = VwmacdParams {
            fast_period: Some(12),
            slow_period: Some(26),
            signal_period: Some(9),
            fast_ma_type: Some("ema".to_string()),
            slow_ma_type: Some("wma".to_string()),
            signal_ma_type: Some("sma".to_string()),
        };
        let (macd_row, signal_row, hist_row) = output
            .values_for(&params)
            .expect("custom MA types row missing");
        assert_eq!(macd_row.len(), close.len());
        Ok(())
    }

    macro_rules! gen_batch_tests {
        ($fn_name:ident) => {
            paste::paste! {
                #[test] fn [<$fn_name _scalar>]()      {
                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
                }
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                #[test] fn [<$fn_name _avx2>]()        {
                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
                }
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                #[test] fn [<$fn_name _avx512>]()      {
                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
                }
                #[test] fn [<$fn_name _auto_detect>]() {
                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
                }
            }
        };
    }

    #[cfg(debug_assertions)]
    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);

        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;

        let close = &c.close;
        let volume = &c.volume;

        let test_configs = vec![
            (2, 10, 2, 11, 20, 3, 2, 5, 1),
            (5, 15, 5, 16, 30, 5, 3, 9, 3),
            (10, 30, 10, 31, 60, 10, 5, 15, 5),
            (2, 5, 1, 6, 10, 1, 2, 4, 1),
            (12, 12, 0, 26, 26, 0, 9, 9, 0),
            (8, 16, 4, 20, 40, 10, 5, 10, 5),
        ];

        for (
            cfg_idx,
            &(
                fast_start,
                fast_end,
                fast_step,
                slow_start,
                slow_end,
                slow_step,
                signal_start,
                signal_end,
                signal_step,
            ),
        ) in test_configs.iter().enumerate()
        {
            let mut builder = VwmacdBatchBuilder::new().kernel(kernel);

            if fast_step > 0 {
                builder = builder.fast_range(fast_start, fast_end, fast_step);
            } else {
                builder = builder.fast_range(fast_start, fast_start, 1);
            }

            if slow_step > 0 {
                builder = builder.slow_range(slow_start, slow_end, slow_step);
            } else {
                builder = builder.slow_range(slow_start, slow_start, 1);
            }

            if signal_step > 0 {
                builder = builder.signal_range(signal_start, signal_end, signal_step);
            } else {
                builder = builder.signal_range(signal_start, signal_start, 1);
            }

            let output = builder.apply_slices(close, volume)?;

            for (idx, &val) in output.macd.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();
                let row = idx / output.cols;
                let col = idx % output.cols;
                let combo = &output.params[row];

                if bits == 0x11111111_11111111 {
                    panic!(
                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
						 fast_ma={}, slow_ma={}, signal_ma={}",
                        test,
                        cfg_idx,
                        val,
                        bits,
                        row,
                        col,
                        idx,
                        combo.fast_period.unwrap_or(12),
                        combo.slow_period.unwrap_or(26),
                        combo.signal_period.unwrap_or(9),
                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
                        combo.signal_ma_type.as_deref().unwrap_or("ema")
                    );
                }

                if bits == 0x22222222_22222222 {
                    panic!(
                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
						 fast_ma={}, slow_ma={}, signal_ma={}",
                        test,
                        cfg_idx,
                        val,
                        bits,
                        row,
                        col,
                        idx,
                        combo.fast_period.unwrap_or(12),
                        combo.slow_period.unwrap_or(26),
                        combo.signal_period.unwrap_or(9),
                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
                        combo.signal_ma_type.as_deref().unwrap_or("ema")
                    );
                }

                if bits == 0x33333333_33333333 {
                    panic!(
                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
						 fast_ma={}, slow_ma={}, signal_ma={}",
                        test,
                        cfg_idx,
                        val,
                        bits,
                        row,
                        col,
                        idx,
                        combo.fast_period.unwrap_or(12),
                        combo.slow_period.unwrap_or(26),
                        combo.signal_period.unwrap_or(9),
                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
                        combo.signal_ma_type.as_deref().unwrap_or("ema")
                    );
                }
            }
        }

        let ma_type_configs = vec![
            ("ema", "ema", "ema"),
            ("sma", "wma", "ema"),
            ("wma", "wma", "sma"),
        ];

        for (cfg_idx, &(fast_ma, slow_ma, signal_ma)) in ma_type_configs.iter().enumerate() {
            let output = VwmacdBatchBuilder::new()
                .kernel(kernel)
                .fast_range(10, 15, 5)
                .slow_range(20, 30, 10)
                .signal_range(5, 10, 5)
                .fast_ma_type(fast_ma.to_string())
                .slow_ma_type(slow_ma.to_string())
                .signal_ma_type(signal_ma.to_string())
                .apply_slices(close, volume)?;

            for (idx, &val) in output.macd.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();
                let row = idx / output.cols;
                let col = idx % output.cols;
                let combo = &output.params[row];

                if bits == 0x11111111_11111111
                    || bits == 0x22222222_22222222
                    || bits == 0x33333333_33333333
                {
                    let poison_type = if bits == 0x11111111_11111111 {
                        "alloc_with_nan_prefix"
                    } else if bits == 0x22222222_22222222 {
                        "init_matrix_prefixes"
                    } else {
                        "make_uninit_matrix"
                    };

                    panic!(
                        "[{}] MA Type Config {}: Found {} poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
						 fast_ma={}, slow_ma={}, signal_ma={}",
                        test,
                        cfg_idx,
                        poison_type,
                        val,
                        bits,
                        row,
                        col,
                        idx,
                        combo.fast_period.unwrap_or(12),
                        combo.slow_period.unwrap_or(26),
                        combo.signal_period.unwrap_or(9),
                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
                        combo.signal_ma_type.as_deref().unwrap_or("ema")
                    );
                }
            }
        }

        Ok(())
    }

    #[cfg(not(debug_assertions))]
    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    gen_batch_tests!(check_batch_default_row);
    gen_batch_tests!(check_batch_grid);
    gen_batch_tests!(check_batch_param_map);
    gen_batch_tests!(check_batch_custom_ma_types);
    gen_batch_tests!(check_batch_no_poison);
}
