Skip to main content

vector_ta/indicators/
yang_zhang_volatility.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18    alloc_uninit_f64, alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel,
19    init_matrix_prefixes, make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
24use core::arch::x86_64::*;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
28use std::arch::is_x86_feature_detected;
29use std::mem::ManuallyDrop;
30use thiserror::Error;
31
32#[derive(Debug, Clone)]
33pub enum YangZhangVolatilityData<'a> {
34    Candles {
35        candles: &'a Candles,
36    },
37    Slices {
38        open: &'a [f64],
39        high: &'a [f64],
40        low: &'a [f64],
41        close: &'a [f64],
42    },
43}
44
45#[derive(Debug, Clone)]
46pub struct YangZhangVolatilityOutput {
47    pub yz: Vec<f64>,
48    pub rs: Vec<f64>,
49}
50
51#[derive(Debug, Clone)]
52#[cfg_attr(
53    all(target_arch = "wasm32", feature = "wasm"),
54    derive(Serialize, Deserialize)
55)]
56pub struct YangZhangVolatilityParams {
57    pub lookback: Option<usize>,
58    pub k_override: Option<bool>,
59    pub k: Option<f64>,
60}
61
62impl Default for YangZhangVolatilityParams {
63    fn default() -> Self {
64        Self {
65            lookback: Some(14),
66            k_override: Some(false),
67            k: Some(0.34),
68        }
69    }
70}
71
72#[derive(Debug, Clone)]
73pub struct YangZhangVolatilityInput<'a> {
74    pub data: YangZhangVolatilityData<'a>,
75    pub params: YangZhangVolatilityParams,
76}
77
78impl<'a> YangZhangVolatilityInput<'a> {
79    #[inline]
80    pub fn from_candles(candles: &'a Candles, params: YangZhangVolatilityParams) -> Self {
81        Self {
82            data: YangZhangVolatilityData::Candles { candles },
83            params,
84        }
85    }
86
87    #[inline]
88    pub fn from_slices(
89        open: &'a [f64],
90        high: &'a [f64],
91        low: &'a [f64],
92        close: &'a [f64],
93        params: YangZhangVolatilityParams,
94    ) -> Self {
95        Self {
96            data: YangZhangVolatilityData::Slices {
97                open,
98                high,
99                low,
100                close,
101            },
102            params,
103        }
104    }
105
106    #[inline]
107    pub fn with_default_candles(candles: &'a Candles) -> Self {
108        Self::from_candles(candles, YangZhangVolatilityParams::default())
109    }
110
111    #[inline]
112    pub fn get_lookback(&self) -> usize {
113        self.params.lookback.unwrap_or(14)
114    }
115
116    #[inline]
117    pub fn get_k_override(&self) -> bool {
118        self.params.k_override.unwrap_or(false)
119    }
120
121    #[inline]
122    pub fn get_k(&self) -> f64 {
123        self.params.k.unwrap_or(0.34)
124    }
125}
126
127#[derive(Copy, Clone, Debug)]
128pub struct YangZhangVolatilityBuilder {
129    lookback: Option<usize>,
130    k_override: Option<bool>,
131    k: Option<f64>,
132    kernel: Kernel,
133}
134
135impl Default for YangZhangVolatilityBuilder {
136    fn default() -> Self {
137        Self {
138            lookback: None,
139            k_override: None,
140            k: None,
141            kernel: Kernel::Auto,
142        }
143    }
144}
145
146impl YangZhangVolatilityBuilder {
147    #[inline(always)]
148    pub fn new() -> Self {
149        Self::default()
150    }
151
152    #[inline(always)]
153    pub fn lookback(mut self, n: usize) -> Self {
154        self.lookback = Some(n);
155        self
156    }
157
158    #[inline(always)]
159    pub fn k_override(mut self, v: bool) -> Self {
160        self.k_override = Some(v);
161        self
162    }
163
164    #[inline(always)]
165    pub fn k(mut self, v: f64) -> Self {
166        self.k = Some(v);
167        self
168    }
169
170    #[inline(always)]
171    pub fn kernel(mut self, k: Kernel) -> Self {
172        self.kernel = k;
173        self
174    }
175
176    #[inline(always)]
177    pub fn apply(self, c: &Candles) -> Result<YangZhangVolatilityOutput, YangZhangVolatilityError> {
178        let p = YangZhangVolatilityParams {
179            lookback: self.lookback,
180            k_override: self.k_override,
181            k: self.k,
182        };
183        let i = YangZhangVolatilityInput::from_candles(c, p);
184        yang_zhang_volatility_with_kernel(&i, self.kernel)
185    }
186
187    #[inline(always)]
188    pub fn apply_slices(
189        self,
190        open: &[f64],
191        high: &[f64],
192        low: &[f64],
193        close: &[f64],
194    ) -> Result<YangZhangVolatilityOutput, YangZhangVolatilityError> {
195        let p = YangZhangVolatilityParams {
196            lookback: self.lookback,
197            k_override: self.k_override,
198            k: self.k,
199        };
200        let i = YangZhangVolatilityInput::from_slices(open, high, low, close, p);
201        yang_zhang_volatility_with_kernel(&i, self.kernel)
202    }
203
204    #[inline(always)]
205    pub fn into_stream(self) -> Result<YangZhangVolatilityStream, YangZhangVolatilityError> {
206        let p = YangZhangVolatilityParams {
207            lookback: self.lookback,
208            k_override: self.k_override,
209            k: self.k,
210        };
211        YangZhangVolatilityStream::try_new(p)
212    }
213}
214
215#[derive(Debug, Error)]
216pub enum YangZhangVolatilityError {
217    #[error("yang_zhang_volatility: Input data slice is empty.")]
218    EmptyInputData,
219    #[error("yang_zhang_volatility: All values are NaN.")]
220    AllValuesNaN,
221    #[error(
222        "yang_zhang_volatility: Invalid lookback: lookback = {lookback}, data length = {data_len}"
223    )]
224    InvalidLookback { lookback: usize, data_len: usize },
225    #[error("yang_zhang_volatility: Not enough valid data: needed = {needed}, valid = {valid}")]
226    NotEnoughValidData { needed: usize, valid: usize },
227    #[error("yang_zhang_volatility: Inconsistent slice lengths: open={open_len}, high={high_len}, low={low_len}, close={close_len}")]
228    InconsistentSliceLengths {
229        open_len: usize,
230        high_len: usize,
231        low_len: usize,
232        close_len: usize,
233    },
234    #[error(
235        "yang_zhang_volatility: Invalid k override value: {k}. Must be finite and within [0, 1]."
236    )]
237    InvalidK { k: f64 },
238    #[error("yang_zhang_volatility: Output length mismatch: expected = {expected}, got = {got}")]
239    OutputLengthMismatch { expected: usize, got: usize },
240    #[error("yang_zhang_volatility: Invalid range: start={start}, end={end}, step={step}")]
241    InvalidRange {
242        start: String,
243        end: String,
244        step: String,
245    },
246    #[error("yang_zhang_volatility: Invalid kernel for batch: {0:?}")]
247    InvalidKernelForBatch(Kernel),
248}
249
250#[derive(Debug, Clone)]
251pub struct YangZhangVolatilityStream {
252    lookback: usize,
253    k: f64,
254    prev_close: f64,
255    o: Vec<f64>,
256    c: Vec<f64>,
257    rs: Vec<f64>,
258    sum_o: f64,
259    sumsq_o: f64,
260    sum_c: f64,
261    sumsq_c: f64,
262    sum_rs: f64,
263    idx: usize,
264    cnt: usize,
265}
266
267impl YangZhangVolatilityStream {
268    #[inline(always)]
269    pub fn try_new(params: YangZhangVolatilityParams) -> Result<Self, YangZhangVolatilityError> {
270        let lookback = params.lookback.unwrap_or(14);
271        if lookback == 0 {
272            return Err(YangZhangVolatilityError::InvalidLookback {
273                lookback,
274                data_len: 0,
275            });
276        }
277
278        let k = if params.k_override.unwrap_or(false) {
279            let k = params.k.unwrap_or(0.34);
280            if !k.is_finite() || !(0.0..=1.0).contains(&k) {
281                return Err(YangZhangVolatilityError::InvalidK { k });
282            }
283            k
284        } else {
285            k_default(lookback)
286        };
287
288        Ok(Self {
289            lookback,
290            k,
291            prev_close: f64::NAN,
292            o: vec![0.0; lookback],
293            c: vec![0.0; lookback],
294            rs: vec![0.0; lookback],
295            sum_o: 0.0,
296            sumsq_o: 0.0,
297            sum_c: 0.0,
298            sumsq_c: 0.0,
299            sum_rs: 0.0,
300            idx: 0,
301            cnt: 0,
302        })
303    }
304
305    #[inline(always)]
306    pub fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
307        if !open.is_finite() || !high.is_finite() || !low.is_finite() || !close.is_finite() {
308            self.prev_close = close;
309            return None;
310        }
311        if open <= 0.0 || high <= 0.0 || low <= 0.0 || close <= 0.0 {
312            self.prev_close = close;
313            return None;
314        }
315
316        if self.prev_close.is_nan() {
317            self.prev_close = close;
318            return None;
319        }
320
321        let oret = (open / self.prev_close).ln();
322        let cret = (close / open).ln();
323        let rsc = rs_component(high, low, open, close);
324
325        self.prev_close = close;
326
327        let i = self.idx;
328        if self.cnt < self.lookback {
329            self.o[i] = oret;
330            self.c[i] = cret;
331            self.rs[i] = rsc;
332
333            self.sum_o += oret;
334            self.sumsq_o += oret * oret;
335            self.sum_c += cret;
336            self.sumsq_c += cret * cret;
337            self.sum_rs += rsc;
338
339            self.cnt += 1;
340        } else {
341            let old_o = self.o[i];
342            let old_c = self.c[i];
343            let old_rs = self.rs[i];
344
345            self.sum_o -= old_o;
346            self.sumsq_o -= old_o * old_o;
347            self.sum_c -= old_c;
348            self.sumsq_c -= old_c * old_c;
349            self.sum_rs -= old_rs;
350
351            self.o[i] = oret;
352            self.c[i] = cret;
353            self.rs[i] = rsc;
354
355            self.sum_o += oret;
356            self.sumsq_o += oret * oret;
357            self.sum_c += cret;
358            self.sumsq_c += cret * cret;
359            self.sum_rs += rsc;
360        }
361
362        self.idx += 1;
363        if self.idx == self.lookback {
364            self.idx = 0;
365        }
366
367        if self.cnt < self.lookback {
368            return None;
369        }
370
371        let lb_f = self.lookback as f64;
372        let mut rs_var = self.sum_rs / lb_f;
373        if rs_var < 0.0 {
374            rs_var = 0.0;
375        }
376        let rs_out = rs_var.sqrt();
377
378        let o_var = sample_var(self.sum_o, self.sumsq_o, self.lookback);
379        let c_var = sample_var(self.sum_c, self.sumsq_c, self.lookback);
380        let mut yz_var = o_var + self.k * c_var + (1.0 - self.k) * rs_var;
381        if yz_var < 0.0 {
382            yz_var = 0.0;
383        }
384        let yz_out = yz_var.sqrt();
385
386        Some((yz_out, rs_out))
387    }
388
389    #[inline(always)]
390    pub fn get_warmup_period(&self) -> usize {
391        self.lookback
392    }
393}
394
395#[inline]
396pub fn yang_zhang_volatility(
397    input: &YangZhangVolatilityInput,
398) -> Result<YangZhangVolatilityOutput, YangZhangVolatilityError> {
399    yang_zhang_volatility_with_kernel(input, Kernel::Auto)
400}
401
402#[inline(always)]
403fn k_default(lookback: usize) -> f64 {
404    if lookback <= 1 {
405        0.0
406    } else {
407        0.34 / (1.34 + ((lookback + 1) as f64) / ((lookback - 1) as f64))
408    }
409}
410
411#[inline(always)]
412fn first_valid_ohlc(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> usize {
413    let len = close.len();
414    let mut i = 0;
415    while i < len {
416        if !open[i].is_nan() && !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan() {
417            break;
418        }
419        i += 1;
420    }
421    i.min(len)
422}
423
424#[inline(always)]
425fn yang_zhang_prepare<'a>(
426    input: &'a YangZhangVolatilityInput,
427    kernel: Kernel,
428) -> Result<
429    (
430        &'a [f64],
431        &'a [f64],
432        &'a [f64],
433        &'a [f64],
434        usize,
435        usize,
436        Kernel,
437    ),
438    YangZhangVolatilityError,
439> {
440    let (open, high, low, close): (&[f64], &[f64], &[f64], &[f64]) = match &input.data {
441        YangZhangVolatilityData::Candles { candles } => {
442            (&candles.open, &candles.high, &candles.low, &candles.close)
443        }
444        YangZhangVolatilityData::Slices {
445            open,
446            high,
447            low,
448            close,
449        } => (open, high, low, close),
450    };
451
452    let len = close.len();
453    if len == 0 {
454        return Err(YangZhangVolatilityError::EmptyInputData);
455    }
456    if open.len() != len || high.len() != len || low.len() != len {
457        return Err(YangZhangVolatilityError::InconsistentSliceLengths {
458            open_len: open.len(),
459            high_len: high.len(),
460            low_len: low.len(),
461            close_len: close.len(),
462        });
463    }
464
465    let first = first_valid_ohlc(open, high, low, close);
466    if first >= len {
467        return Err(YangZhangVolatilityError::AllValuesNaN);
468    }
469
470    let lookback = input.get_lookback();
471    if lookback == 0 || lookback > len {
472        return Err(YangZhangVolatilityError::InvalidLookback {
473            lookback,
474            data_len: len,
475        });
476    }
477    if len - first < lookback + 1 {
478        return Err(YangZhangVolatilityError::NotEnoughValidData {
479            needed: lookback + 1,
480            valid: len - first,
481        });
482    }
483
484    let chosen = match kernel {
485        Kernel::Auto => detect_best_kernel(),
486        k => k.to_non_batch(),
487    };
488
489    Ok((open, high, low, close, lookback, first, chosen))
490}
491
492#[inline(always)]
493fn rs_component(high: f64, low: f64, open: f64, close: f64) -> f64 {
494    (high / close).ln() * (high / open).ln() + (low / close).ln() * (low / open).ln()
495}
496
497#[inline(always)]
498fn sample_var(sum: f64, sumsq: f64, n: usize) -> f64 {
499    if n <= 1 {
500        return 0.0;
501    }
502    let nf = n as f64;
503    let denom = (n - 1) as f64;
504    let mut v = (sumsq - (sum * sum) / nf) / denom;
505    if v < 0.0 {
506        v = 0.0;
507    }
508    v
509}
510
511#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
512#[inline]
513#[target_feature(enable = "avx2")]
514unsafe fn _mm256_abs_pd(a: __m256d) -> __m256d {
515    let sign_mask = _mm256_set1_pd(-0.0);
516    _mm256_andnot_pd(sign_mask, a)
517}
518
519#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
520#[inline]
521#[target_feature(enable = "avx512f")]
522unsafe fn _mm512_abs_pd(a: __m512d) -> __m512d {
523    let sign_mask = _mm512_set1_pd(-0.0);
524    _mm512_andnot_pd(sign_mask, a)
525}
526
527#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
528#[inline]
529#[target_feature(enable = "avx2,fma")]
530unsafe fn ln1p_taylor_14_avx2(y: __m256d) -> __m256d {
531    let c0 = _mm256_set1_pd(1.0);
532    let c1 = _mm256_set1_pd(-1.0 / 2.0);
533    let c2 = _mm256_set1_pd(1.0 / 3.0);
534    let c3 = _mm256_set1_pd(-1.0 / 4.0);
535    let c4 = _mm256_set1_pd(1.0 / 5.0);
536    let c5 = _mm256_set1_pd(-1.0 / 6.0);
537    let c6 = _mm256_set1_pd(1.0 / 7.0);
538    let c7 = _mm256_set1_pd(-1.0 / 8.0);
539    let c8 = _mm256_set1_pd(1.0 / 9.0);
540    let c9 = _mm256_set1_pd(-1.0 / 10.0);
541    let c10 = _mm256_set1_pd(1.0 / 11.0);
542    let c11 = _mm256_set1_pd(-1.0 / 12.0);
543    let c12 = _mm256_set1_pd(1.0 / 13.0);
544    let c13 = _mm256_set1_pd(-1.0 / 14.0);
545
546    let mut p = c13;
547    p = _mm256_fmadd_pd(y, p, c12);
548    p = _mm256_fmadd_pd(y, p, c11);
549    p = _mm256_fmadd_pd(y, p, c10);
550    p = _mm256_fmadd_pd(y, p, c9);
551    p = _mm256_fmadd_pd(y, p, c8);
552    p = _mm256_fmadd_pd(y, p, c7);
553    p = _mm256_fmadd_pd(y, p, c6);
554    p = _mm256_fmadd_pd(y, p, c5);
555    p = _mm256_fmadd_pd(y, p, c4);
556    p = _mm256_fmadd_pd(y, p, c3);
557    p = _mm256_fmadd_pd(y, p, c2);
558    p = _mm256_fmadd_pd(y, p, c1);
559    p = _mm256_fmadd_pd(y, p, c0);
560
561    _mm256_mul_pd(y, p)
562}
563
564#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
565#[inline]
566#[target_feature(enable = "avx512f,fma")]
567unsafe fn ln1p_taylor_14_avx512(y: __m512d) -> __m512d {
568    let c0 = _mm512_set1_pd(1.0);
569    let c1 = _mm512_set1_pd(-1.0 / 2.0);
570    let c2 = _mm512_set1_pd(1.0 / 3.0);
571    let c3 = _mm512_set1_pd(-1.0 / 4.0);
572    let c4 = _mm512_set1_pd(1.0 / 5.0);
573    let c5 = _mm512_set1_pd(-1.0 / 6.0);
574    let c6 = _mm512_set1_pd(1.0 / 7.0);
575    let c7 = _mm512_set1_pd(-1.0 / 8.0);
576    let c8 = _mm512_set1_pd(1.0 / 9.0);
577    let c9 = _mm512_set1_pd(-1.0 / 10.0);
578    let c10 = _mm512_set1_pd(1.0 / 11.0);
579    let c11 = _mm512_set1_pd(-1.0 / 12.0);
580    let c12 = _mm512_set1_pd(1.0 / 13.0);
581    let c13 = _mm512_set1_pd(-1.0 / 14.0);
582
583    let mut p = c13;
584    p = _mm512_fmadd_pd(y, p, c12);
585    p = _mm512_fmadd_pd(y, p, c11);
586    p = _mm512_fmadd_pd(y, p, c10);
587    p = _mm512_fmadd_pd(y, p, c9);
588    p = _mm512_fmadd_pd(y, p, c8);
589    p = _mm512_fmadd_pd(y, p, c7);
590    p = _mm512_fmadd_pd(y, p, c6);
591    p = _mm512_fmadd_pd(y, p, c5);
592    p = _mm512_fmadd_pd(y, p, c4);
593    p = _mm512_fmadd_pd(y, p, c3);
594    p = _mm512_fmadd_pd(y, p, c2);
595    p = _mm512_fmadd_pd(y, p, c1);
596    p = _mm512_fmadd_pd(y, p, c0);
597
598    _mm512_mul_pd(y, p)
599}
600
601#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
602#[inline]
603#[target_feature(enable = "avx2,fma")]
604unsafe fn yang_zhang_precompute_ln_diff_avx2(
605    open: &[f64],
606    high: &[f64],
607    low: &[f64],
608    close: &[f64],
609    oret: &mut [f64],
610    cret: &mut [f64],
611    rs_val: &mut [f64],
612) {
613    let len = close.len();
614    if len == 0 {
615        return;
616    }
617
618    let ln_open0 = open[0].ln();
619    let ln_high0 = high[0].ln();
620    let ln_low0 = low[0].ln();
621    let ln_close0 = close[0].ln();
622
623    oret[0] = 0.0;
624    cret[0] = ln_close0 - ln_open0;
625    let h_c0 = ln_high0 - ln_close0;
626    let h_o0 = ln_high0 - ln_open0;
627    let l_c0 = ln_low0 - ln_close0;
628    let l_o0 = ln_low0 - ln_open0;
629    rs_val[0] = h_c0 * h_o0 + l_c0 * l_o0;
630
631    let one = _mm256_set1_pd(1.0);
632    let threshold = _mm256_set1_pd(0.2);
633
634    let mut i = 1usize;
635    while i + 4 <= len {
636        let o = _mm256_loadu_pd(open.as_ptr().add(i));
637        let h = _mm256_loadu_pd(high.as_ptr().add(i));
638        let l = _mm256_loadu_pd(low.as_ptr().add(i));
639        let c = _mm256_loadu_pd(close.as_ptr().add(i));
640        let c_prev = _mm256_loadu_pd(close.as_ptr().add(i - 1));
641
642        let r_oret = _mm256_div_pd(o, c_prev);
643        let r_cret = _mm256_div_pd(c, o);
644        let r_hc = _mm256_div_pd(h, c);
645        let r_ho = _mm256_div_pd(h, o);
646        let r_lc = _mm256_div_pd(l, c);
647        let r_lo = _mm256_div_pd(l, o);
648
649        let y_oret = _mm256_sub_pd(r_oret, one);
650        let y_cret = _mm256_sub_pd(r_cret, one);
651        let y_hc = _mm256_sub_pd(r_hc, one);
652        let y_ho = _mm256_sub_pd(r_ho, one);
653        let y_lc = _mm256_sub_pd(r_lc, one);
654        let y_lo = _mm256_sub_pd(r_lo, one);
655
656        let m_oret =
657            _mm256_movemask_pd(_mm256_cmp_pd(_mm256_abs_pd(y_oret), threshold, _CMP_LT_OQ)) as u8;
658        let m_cret =
659            _mm256_movemask_pd(_mm256_cmp_pd(_mm256_abs_pd(y_cret), threshold, _CMP_LT_OQ)) as u8;
660        let m_hc =
661            _mm256_movemask_pd(_mm256_cmp_pd(_mm256_abs_pd(y_hc), threshold, _CMP_LT_OQ)) as u8;
662        let m_ho =
663            _mm256_movemask_pd(_mm256_cmp_pd(_mm256_abs_pd(y_ho), threshold, _CMP_LT_OQ)) as u8;
664        let m_lc =
665            _mm256_movemask_pd(_mm256_cmp_pd(_mm256_abs_pd(y_lc), threshold, _CMP_LT_OQ)) as u8;
666        let m_lo =
667            _mm256_movemask_pd(_mm256_cmp_pd(_mm256_abs_pd(y_lo), threshold, _CMP_LT_OQ)) as u8;
668
669        let ln_oret = ln1p_taylor_14_avx2(y_oret);
670        let ln_cret = ln1p_taylor_14_avx2(y_cret);
671        let ln_hc = ln1p_taylor_14_avx2(y_hc);
672        let ln_ho = ln1p_taylor_14_avx2(y_ho);
673        let ln_lc = ln1p_taylor_14_avx2(y_lc);
674        let ln_lo = ln1p_taylor_14_avx2(y_lo);
675
676        _mm256_storeu_pd(oret.as_mut_ptr().add(i), ln_oret);
677        _mm256_storeu_pd(cret.as_mut_ptr().add(i), ln_cret);
678        let rs_v = _mm256_add_pd(_mm256_mul_pd(ln_hc, ln_ho), _mm256_mul_pd(ln_lc, ln_lo));
679        _mm256_storeu_pd(rs_val.as_mut_ptr().add(i), rs_v);
680
681        let m_rs = m_hc & m_ho & m_lc & m_lo;
682        if m_oret == 0b1111 && m_cret == 0b1111 && m_rs == 0b1111 {
683            i += 4;
684            continue;
685        }
686
687        let mut oret_lanes = [0.0f64; 4];
688        let mut cret_lanes = [0.0f64; 4];
689        let mut hc_lanes = [0.0f64; 4];
690        let mut ho_lanes = [0.0f64; 4];
691        let mut lc_lanes = [0.0f64; 4];
692        let mut lo_lanes = [0.0f64; 4];
693        _mm256_storeu_pd(oret_lanes.as_mut_ptr(), ln_oret);
694        _mm256_storeu_pd(cret_lanes.as_mut_ptr(), ln_cret);
695        _mm256_storeu_pd(hc_lanes.as_mut_ptr(), ln_hc);
696        _mm256_storeu_pd(ho_lanes.as_mut_ptr(), ln_ho);
697        _mm256_storeu_pd(lc_lanes.as_mut_ptr(), ln_lc);
698        _mm256_storeu_pd(lo_lanes.as_mut_ptr(), ln_lo);
699
700        for lane in 0..4 {
701            let idx = i + lane;
702            let bit = 1u8 << lane;
703            if (m_oret & bit) == 0 {
704                oret_lanes[lane] = (open[idx] / close[idx - 1]).ln();
705            }
706            if (m_cret & bit) == 0 {
707                cret_lanes[lane] = (close[idx] / open[idx]).ln();
708            }
709            if (m_hc & bit) == 0 {
710                hc_lanes[lane] = (high[idx] / close[idx]).ln();
711            }
712            if (m_ho & bit) == 0 {
713                ho_lanes[lane] = (high[idx] / open[idx]).ln();
714            }
715            if (m_lc & bit) == 0 {
716                lc_lanes[lane] = (low[idx] / close[idx]).ln();
717            }
718            if (m_lo & bit) == 0 {
719                lo_lanes[lane] = (low[idx] / open[idx]).ln();
720            }
721
722            oret[idx] = oret_lanes[lane];
723            cret[idx] = cret_lanes[lane];
724            rs_val[idx] = hc_lanes[lane] * ho_lanes[lane] + lc_lanes[lane] * lo_lanes[lane];
725        }
726
727        i += 4;
728    }
729
730    while i < len {
731        let ln_open = open[i].ln();
732        let ln_high = high[i].ln();
733        let ln_low = low[i].ln();
734        let ln_close = close[i].ln();
735
736        oret[i] = (open[i] / close[i - 1]).ln();
737        cret[i] = ln_close - ln_open;
738        let h_c = ln_high - ln_close;
739        let h_o = ln_high - ln_open;
740        let l_c = ln_low - ln_close;
741        let l_o = ln_low - ln_open;
742        rs_val[i] = h_c * h_o + l_c * l_o;
743
744        i += 1;
745    }
746}
747
748#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
749#[inline]
750#[target_feature(enable = "avx512f,fma")]
751unsafe fn yang_zhang_precompute_ln_diff_avx512_fast(
752    open: &[f64],
753    high: &[f64],
754    low: &[f64],
755    close: &[f64],
756    oret: &mut [f64],
757    cret: &mut [f64],
758    rs_val: &mut [f64],
759) {
760    let len = close.len();
761    if len == 0 {
762        return;
763    }
764
765    oret[0] = 0.0;
766    cret[0] = (close[0] / open[0]).ln();
767    rs_val[0] = rs_component(high[0], low[0], open[0], close[0]);
768
769    let one = _mm512_set1_pd(1.0);
770    let threshold = _mm512_set1_pd(0.25);
771
772    let mut i = 1usize;
773    while i + 8 <= len {
774        let o = _mm512_loadu_pd(open.as_ptr().add(i));
775        let h = _mm512_loadu_pd(high.as_ptr().add(i));
776        let l = _mm512_loadu_pd(low.as_ptr().add(i));
777        let c = _mm512_loadu_pd(close.as_ptr().add(i));
778        let c_prev = _mm512_loadu_pd(close.as_ptr().add(i - 1));
779
780        let r_oret = _mm512_div_pd(o, c_prev);
781        let r_cret = _mm512_div_pd(c, o);
782
783        let r_hc = _mm512_div_pd(h, c);
784        let r_ho = _mm512_div_pd(h, o);
785        let r_lc = _mm512_div_pd(l, c);
786        let r_lo = _mm512_div_pd(l, o);
787
788        let y_oret = _mm512_sub_pd(r_oret, one);
789        let y_cret = _mm512_sub_pd(r_cret, one);
790        let y_hc = _mm512_sub_pd(r_hc, one);
791        let y_ho = _mm512_sub_pd(r_ho, one);
792        let y_lc = _mm512_sub_pd(r_lc, one);
793        let y_lo = _mm512_sub_pd(r_lo, one);
794
795        let m_oret = _mm512_cmp_pd_mask(_mm512_abs_pd(y_oret), threshold, _CMP_LT_OQ);
796        let m_cret = _mm512_cmp_pd_mask(_mm512_abs_pd(y_cret), threshold, _CMP_LT_OQ);
797        let m_hc = _mm512_cmp_pd_mask(_mm512_abs_pd(y_hc), threshold, _CMP_LT_OQ);
798        let m_ho = _mm512_cmp_pd_mask(_mm512_abs_pd(y_ho), threshold, _CMP_LT_OQ);
799        let m_lc = _mm512_cmp_pd_mask(_mm512_abs_pd(y_lc), threshold, _CMP_LT_OQ);
800        let m_lo = _mm512_cmp_pd_mask(_mm512_abs_pd(y_lo), threshold, _CMP_LT_OQ);
801
802        let ln_oret = ln1p_taylor_14_avx512(y_oret);
803        let ln_cret = ln1p_taylor_14_avx512(y_cret);
804
805        let ln_hc = ln1p_taylor_14_avx512(y_hc);
806        let ln_ho = ln1p_taylor_14_avx512(y_ho);
807        let ln_lc = ln1p_taylor_14_avx512(y_lc);
808        let ln_lo = ln1p_taylor_14_avx512(y_lo);
809
810        let rs_v = _mm512_fmadd_pd(ln_hc, ln_ho, _mm512_mul_pd(ln_lc, ln_lo));
811        _mm512_storeu_pd(oret.as_mut_ptr().add(i), ln_oret);
812        _mm512_storeu_pd(cret.as_mut_ptr().add(i), ln_cret);
813        _mm512_storeu_pd(rs_val.as_mut_ptr().add(i), rs_v);
814        let m_rs = m_hc & m_ho & m_lc & m_lo;
815        if m_oret == 0xFF && m_cret == 0xFF && m_rs == 0xFF {
816            i += 8;
817            continue;
818        }
819
820        if m_oret != 0xFF {
821            let mut missing = !m_oret;
822            while missing != 0 {
823                let lane = missing.trailing_zeros() as usize;
824                let bit = 1u8 << lane;
825                let idx = i + lane;
826                oret[idx] = (open[idx] / close[idx - 1]).ln();
827                missing &= !bit;
828            }
829        }
830
831        if m_cret != 0xFF {
832            let mut missing = !m_cret;
833            while missing != 0 {
834                let lane = missing.trailing_zeros() as usize;
835                let bit = 1u8 << lane;
836                let idx = i + lane;
837                cret[idx] = (close[idx] / open[idx]).ln();
838                missing &= !bit;
839            }
840        }
841
842        if m_rs != 0xFF {
843            let mut hc_lanes = [0.0f64; 8];
844            let mut ho_lanes = [0.0f64; 8];
845            let mut lc_lanes = [0.0f64; 8];
846            let mut lo_lanes = [0.0f64; 8];
847            _mm512_storeu_pd(hc_lanes.as_mut_ptr(), ln_hc);
848            _mm512_storeu_pd(ho_lanes.as_mut_ptr(), ln_ho);
849            _mm512_storeu_pd(lc_lanes.as_mut_ptr(), ln_lc);
850            _mm512_storeu_pd(lo_lanes.as_mut_ptr(), ln_lo);
851
852            let mut missing = !m_rs;
853            while missing != 0 {
854                let lane = missing.trailing_zeros() as usize;
855                let bit = 1u8 << lane;
856                let idx = i + lane;
857
858                if (m_hc & bit) == 0 {
859                    hc_lanes[lane] = (high[idx] / close[idx]).ln();
860                }
861                if (m_ho & bit) == 0 {
862                    ho_lanes[lane] = (high[idx] / open[idx]).ln();
863                }
864                if (m_lc & bit) == 0 {
865                    lc_lanes[lane] = (low[idx] / close[idx]).ln();
866                }
867                if (m_lo & bit) == 0 {
868                    lo_lanes[lane] = (low[idx] / open[idx]).ln();
869                }
870                rs_val[idx] = hc_lanes[lane] * ho_lanes[lane] + lc_lanes[lane] * lo_lanes[lane];
871                missing &= !bit;
872            }
873        }
874
875        i += 8;
876    }
877
878    while i < len {
879        oret[i] = (open[i] / close[i - 1]).ln();
880        cret[i] = (close[i] / open[i]).ln();
881        rs_val[i] = rs_component(high[i], low[i], open[i], close[i]);
882        i += 1;
883    }
884}
885
886#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
887#[inline]
888#[target_feature(enable = "avx512f")]
889unsafe fn yang_zhang_precompute_ln_diff_avx512(
890    open: &[f64],
891    high: &[f64],
892    low: &[f64],
893    close: &[f64],
894    oret: &mut [f64],
895    cret: &mut [f64],
896    rs_val: &mut [f64],
897) {
898    let len = close.len();
899    if len == 0 {
900        return;
901    }
902
903    if is_x86_feature_detected!("fma") {
904        yang_zhang_precompute_ln_diff_avx512_fast(open, high, low, close, oret, cret, rs_val);
905        return;
906    }
907
908    let ln_open0 = open[0].ln();
909    let ln_high0 = high[0].ln();
910    let ln_low0 = low[0].ln();
911    let ln_close0 = close[0].ln();
912
913    oret[0] = 0.0;
914    cret[0] = ln_close0 - ln_open0;
915    let h_c0 = ln_high0 - ln_close0;
916    let h_o0 = ln_high0 - ln_open0;
917    let l_c0 = ln_low0 - ln_close0;
918    let l_o0 = ln_low0 - ln_open0;
919    rs_val[0] = h_c0 * h_o0 + l_c0 * l_o0;
920
921    let mut prev_ln_close = ln_close0;
922
923    let mut i = 1usize;
924    while i + 8 <= len {
925        let ln_open_lanes = [
926            open[i].ln(),
927            open[i + 1].ln(),
928            open[i + 2].ln(),
929            open[i + 3].ln(),
930            open[i + 4].ln(),
931            open[i + 5].ln(),
932            open[i + 6].ln(),
933            open[i + 7].ln(),
934        ];
935        let ln_high_lanes = [
936            high[i].ln(),
937            high[i + 1].ln(),
938            high[i + 2].ln(),
939            high[i + 3].ln(),
940            high[i + 4].ln(),
941            high[i + 5].ln(),
942            high[i + 6].ln(),
943            high[i + 7].ln(),
944        ];
945        let ln_low_lanes = [
946            low[i].ln(),
947            low[i + 1].ln(),
948            low[i + 2].ln(),
949            low[i + 3].ln(),
950            low[i + 4].ln(),
951            low[i + 5].ln(),
952            low[i + 6].ln(),
953            low[i + 7].ln(),
954        ];
955        let ln_close_lanes = [
956            close[i].ln(),
957            close[i + 1].ln(),
958            close[i + 2].ln(),
959            close[i + 3].ln(),
960            close[i + 4].ln(),
961            close[i + 5].ln(),
962            close[i + 6].ln(),
963            close[i + 7].ln(),
964        ];
965
966        let lo = _mm512_loadu_pd(ln_open_lanes.as_ptr());
967        let lh = _mm512_loadu_pd(ln_high_lanes.as_ptr());
968        let ll = _mm512_loadu_pd(ln_low_lanes.as_ptr());
969        let lc = _mm512_loadu_pd(ln_close_lanes.as_ptr());
970
971        let h_c = _mm512_sub_pd(lh, lc);
972        let h_o = _mm512_sub_pd(lh, lo);
973        let l_c = _mm512_sub_pd(ll, lc);
974        let l_o = _mm512_sub_pd(ll, lo);
975
976        let rs_v = _mm512_add_pd(_mm512_mul_pd(h_c, h_o), _mm512_mul_pd(l_c, l_o));
977        _mm512_storeu_pd(rs_val.as_mut_ptr().add(i), rs_v);
978
979        let cr = _mm512_sub_pd(lc, lo);
980        _mm512_storeu_pd(cret.as_mut_ptr().add(i), cr);
981
982        for lane in 0..8 {
983            let idx = i + lane;
984            oret[idx] = ln_open_lanes[lane] - prev_ln_close;
985            prev_ln_close = ln_close_lanes[lane];
986        }
987
988        i += 8;
989    }
990
991    while i < len {
992        let ln_open = open[i].ln();
993        let ln_high = high[i].ln();
994        let ln_low = low[i].ln();
995        let ln_close = close[i].ln();
996
997        oret[i] = ln_open - prev_ln_close;
998        prev_ln_close = ln_close;
999
1000        cret[i] = ln_close - ln_open;
1001        let h_c = ln_high - ln_close;
1002        let h_o = ln_high - ln_open;
1003        let l_c = ln_low - ln_close;
1004        let l_o = ln_low - ln_open;
1005        rs_val[i] = h_c * h_o + l_c * l_o;
1006
1007        i += 1;
1008    }
1009}
1010
1011#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1012#[inline]
1013#[target_feature(enable = "avx2")]
1014unsafe fn yang_zhang_compute_into_avx2(
1015    open: &[f64],
1016    high: &[f64],
1017    low: &[f64],
1018    close: &[f64],
1019    lookback: usize,
1020    first: usize,
1021    k: f64,
1022    out_yz: &mut [f64],
1023    out_rs: &mut [f64],
1024) {
1025    let len = close.len();
1026    let mut rs_val = alloc_uninit_f64(len);
1027    let mut oret = alloc_uninit_f64(len);
1028    let mut cret = alloc_uninit_f64(len);
1029    yang_zhang_precompute_ln_diff_avx2(open, high, low, close, &mut oret, &mut cret, &mut rs_val);
1030    yang_zhang_row_precomputed_into(&oret, &cret, &rs_val, lookback, first, k, out_yz, out_rs);
1031}
1032
1033#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1034#[inline]
1035#[target_feature(enable = "avx512f")]
1036unsafe fn yang_zhang_compute_into_avx512(
1037    open: &[f64],
1038    high: &[f64],
1039    low: &[f64],
1040    close: &[f64],
1041    lookback: usize,
1042    first: usize,
1043    k: f64,
1044    out_yz: &mut [f64],
1045    out_rs: &mut [f64],
1046) {
1047    let len = close.len();
1048    let mut rs_val = alloc_uninit_f64(len);
1049    let mut oret = alloc_uninit_f64(len);
1050    let mut cret = alloc_uninit_f64(len);
1051    yang_zhang_precompute_ln_diff_avx512(open, high, low, close, &mut oret, &mut cret, &mut rs_val);
1052    yang_zhang_row_precomputed_into(&oret, &cret, &rs_val, lookback, first, k, out_yz, out_rs);
1053}
1054
1055#[inline(always)]
1056fn yang_zhang_compute_into(
1057    open: &[f64],
1058    high: &[f64],
1059    low: &[f64],
1060    close: &[f64],
1061    lookback: usize,
1062    first: usize,
1063    k: f64,
1064    out_yz: &mut [f64],
1065    out_rs: &mut [f64],
1066) {
1067    let len = close.len();
1068    let warmup = first + lookback;
1069    if warmup >= len {
1070        return;
1071    }
1072
1073    let lb_f = lookback as f64;
1074
1075    let start = warmup;
1076    let win_start = start + 1 - lookback;
1077
1078    let mut sum_rs = 0.0;
1079    for j in win_start..=start {
1080        sum_rs += rs_component(high[j], low[j], open[j], close[j]);
1081    }
1082
1083    let mut sum_o = 0.0;
1084    let mut sumsq_o = 0.0;
1085    let mut sum_c = 0.0;
1086    let mut sumsq_c = 0.0;
1087
1088    for j in win_start..=start {
1089        let oret = (open[j] / close[j - 1]).ln();
1090        sum_o += oret;
1091        sumsq_o += oret * oret;
1092
1093        let cret = (close[j] / open[j]).ln();
1094        sum_c += cret;
1095        sumsq_c += cret * cret;
1096    }
1097
1098    for t in start..len {
1099        let mut rs_var = sum_rs / lb_f;
1100        if rs_var < 0.0 {
1101            rs_var = 0.0;
1102        }
1103        out_rs[t] = rs_var.sqrt();
1104
1105        let o_var = sample_var(sum_o, sumsq_o, lookback);
1106        let c_var = sample_var(sum_c, sumsq_c, lookback);
1107
1108        let mut yz_var = o_var + k * c_var + (1.0 - k) * rs_var;
1109        if yz_var < 0.0 {
1110            yz_var = 0.0;
1111        }
1112        out_yz[t] = yz_var.sqrt();
1113
1114        if t + 1 < len {
1115            let add_rs = rs_component(high[t + 1], low[t + 1], open[t + 1], close[t + 1]);
1116            let sub_idx = t + 1 - lookback;
1117            let sub_rs = rs_component(high[sub_idx], low[sub_idx], open[sub_idx], close[sub_idx]);
1118            sum_rs += add_rs - sub_rs;
1119
1120            let add_oret = (open[t + 1] / close[t]).ln();
1121            let sub_oret = (open[sub_idx] / close[sub_idx - 1]).ln();
1122            sum_o += add_oret - sub_oret;
1123            sumsq_o += add_oret * add_oret - sub_oret * sub_oret;
1124
1125            let add_cret = (close[t + 1] / open[t + 1]).ln();
1126            let sub_cret = (close[sub_idx] / open[sub_idx]).ln();
1127            sum_c += add_cret - sub_cret;
1128            sumsq_c += add_cret * add_cret - sub_cret * sub_cret;
1129        }
1130    }
1131}
1132
1133#[inline]
1134pub fn yang_zhang_volatility_with_kernel(
1135    input: &YangZhangVolatilityInput,
1136    kernel: Kernel,
1137) -> Result<YangZhangVolatilityOutput, YangZhangVolatilityError> {
1138    let (open, high, low, close, lookback, first, chosen) = yang_zhang_prepare(input, kernel)?;
1139
1140    let k = if input.get_k_override() {
1141        let k = input.get_k();
1142        if !k.is_finite() || !(0.0..=1.0).contains(&k) {
1143            return Err(YangZhangVolatilityError::InvalidK { k });
1144        }
1145        k
1146    } else {
1147        k_default(lookback)
1148    };
1149
1150    let len = close.len();
1151    let warmup = first + lookback;
1152    let mut yz = alloc_with_nan_prefix(len, warmup);
1153    let mut rs = alloc_with_nan_prefix(len, warmup);
1154
1155    unsafe {
1156        match chosen {
1157            Kernel::Scalar => yang_zhang_compute_into(
1158                open, high, low, close, lookback, first, k, &mut yz, &mut rs,
1159            ),
1160            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1161            Kernel::Avx2 => yang_zhang_compute_into_avx2(
1162                open, high, low, close, lookback, first, k, &mut yz, &mut rs,
1163            ),
1164            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1165            Kernel::Avx512 => yang_zhang_compute_into_avx512(
1166                open, high, low, close, lookback, first, k, &mut yz, &mut rs,
1167            ),
1168            #[allow(unreachable_patterns)]
1169            _ => yang_zhang_compute_into(
1170                open, high, low, close, lookback, first, k, &mut yz, &mut rs,
1171            ),
1172        }
1173    }
1174
1175    Ok(YangZhangVolatilityOutput { yz, rs })
1176}
1177
1178#[inline]
1179pub fn yang_zhang_volatility_into_slice(
1180    dst_yz: &mut [f64],
1181    dst_rs: &mut [f64],
1182    input: &YangZhangVolatilityInput,
1183    kern: Kernel,
1184) -> Result<(), YangZhangVolatilityError> {
1185    let (open, high, low, close, lookback, first, chosen) = yang_zhang_prepare(input, kern)?;
1186    let expected = close.len();
1187    if dst_yz.len() != expected || dst_rs.len() != expected {
1188        let got = dst_yz.len().max(dst_rs.len());
1189        return Err(YangZhangVolatilityError::OutputLengthMismatch { expected, got });
1190    }
1191
1192    let k = if input.get_k_override() {
1193        let k = input.get_k();
1194        if !k.is_finite() || !(0.0..=1.0).contains(&k) {
1195            return Err(YangZhangVolatilityError::InvalidK { k });
1196        }
1197        k
1198    } else {
1199        k_default(lookback)
1200    };
1201
1202    let warmup = first + lookback;
1203    let warm = warmup.min(expected);
1204    for v in &mut dst_yz[..warm] {
1205        *v = f64::NAN;
1206    }
1207    for v in &mut dst_rs[..warm] {
1208        *v = f64::NAN;
1209    }
1210
1211    unsafe {
1212        match chosen {
1213            Kernel::Scalar => {
1214                yang_zhang_compute_into(open, high, low, close, lookback, first, k, dst_yz, dst_rs)
1215            }
1216            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1217            Kernel::Avx2 => yang_zhang_compute_into_avx2(
1218                open, high, low, close, lookback, first, k, dst_yz, dst_rs,
1219            ),
1220            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1221            Kernel::Avx512 => yang_zhang_compute_into_avx512(
1222                open, high, low, close, lookback, first, k, dst_yz, dst_rs,
1223            ),
1224            #[allow(unreachable_patterns)]
1225            _ => {
1226                yang_zhang_compute_into(open, high, low, close, lookback, first, k, dst_yz, dst_rs)
1227            }
1228        }
1229    }
1230    Ok(())
1231}
1232
1233#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1234#[inline]
1235pub fn yang_zhang_volatility_into(
1236    input: &YangZhangVolatilityInput,
1237    out_yz: &mut [f64],
1238    out_rs: &mut [f64],
1239) -> Result<(), YangZhangVolatilityError> {
1240    yang_zhang_volatility_into_slice(out_yz, out_rs, input, Kernel::Auto)
1241}
1242
1243#[derive(Clone, Debug)]
1244pub struct YangZhangVolatilityBatchRange {
1245    pub lookback: (usize, usize, usize),
1246    pub k_override: bool,
1247    pub k: (f64, f64, f64),
1248}
1249
1250impl Default for YangZhangVolatilityBatchRange {
1251    fn default() -> Self {
1252        Self {
1253            lookback: (14, 252, 1),
1254            k_override: false,
1255            k: (0.34, 0.34, 0.0),
1256        }
1257    }
1258}
1259
1260#[derive(Clone, Debug, Default)]
1261pub struct YangZhangVolatilityBatchBuilder {
1262    range: YangZhangVolatilityBatchRange,
1263    kernel: Kernel,
1264}
1265
1266impl YangZhangVolatilityBatchBuilder {
1267    pub fn new() -> Self {
1268        Self::default()
1269    }
1270
1271    pub fn kernel(mut self, k: Kernel) -> Self {
1272        self.kernel = k;
1273        self
1274    }
1275
1276    #[inline]
1277    pub fn lookback_range(mut self, start: usize, end: usize, step: usize) -> Self {
1278        self.range.lookback = (start, end, step);
1279        self
1280    }
1281
1282    #[inline]
1283    pub fn lookback_static(mut self, n: usize) -> Self {
1284        self.range.lookback = (n, n, 0);
1285        self
1286    }
1287
1288    #[inline]
1289    pub fn k_override(mut self, v: bool) -> Self {
1290        self.range.k_override = v;
1291        self
1292    }
1293
1294    #[inline]
1295    pub fn k_range(mut self, start: f64, end: f64, step: f64) -> Self {
1296        self.range.k = (start, end, step);
1297        self
1298    }
1299
1300    #[inline]
1301    pub fn k_static(mut self, k: f64) -> Self {
1302        self.range.k = (k, k, 0.0);
1303        self
1304    }
1305
1306    pub fn apply_slices(
1307        self,
1308        open: &[f64],
1309        high: &[f64],
1310        low: &[f64],
1311        close: &[f64],
1312    ) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1313        yang_zhang_volatility_batch_with_kernel(open, high, low, close, &self.range, self.kernel)
1314    }
1315
1316    pub fn with_default_slices(
1317        open: &[f64],
1318        high: &[f64],
1319        low: &[f64],
1320        close: &[f64],
1321        k: Kernel,
1322    ) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1323        YangZhangVolatilityBatchBuilder::new()
1324            .kernel(k)
1325            .apply_slices(open, high, low, close)
1326    }
1327
1328    pub fn apply_candles(
1329        self,
1330        c: &Candles,
1331    ) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1332        self.apply_slices(&c.open, &c.high, &c.low, &c.close)
1333    }
1334
1335    pub fn with_default_candles(
1336        c: &Candles,
1337    ) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1338        YangZhangVolatilityBatchBuilder::new()
1339            .kernel(Kernel::Auto)
1340            .apply_candles(c)
1341    }
1342}
1343
1344#[derive(Clone, Debug)]
1345pub struct YangZhangVolatilityBatchOutput {
1346    pub yz: Vec<f64>,
1347    pub rs: Vec<f64>,
1348    pub combos: Vec<YangZhangVolatilityParams>,
1349    pub rows: usize,
1350    pub cols: usize,
1351}
1352
1353impl YangZhangVolatilityBatchOutput {
1354    pub fn row_for_params(&self, p: &YangZhangVolatilityParams) -> Option<usize> {
1355        let lb = p.lookback.unwrap_or(14);
1356        let ko = p.k_override.unwrap_or(false);
1357        let k = p.k.unwrap_or(0.34);
1358        self.combos.iter().position(|c| {
1359            let clb = c.lookback.unwrap_or(14);
1360            let cko = c.k_override.unwrap_or(false);
1361            if clb != lb || cko != ko {
1362                return false;
1363            }
1364            if ko {
1365                (c.k.unwrap_or(0.34) - k).abs() < 1e-12
1366            } else {
1367                true
1368            }
1369        })
1370    }
1371
1372    pub fn yz_for(&self, p: &YangZhangVolatilityParams) -> Option<&[f64]> {
1373        self.row_for_params(p).and_then(|row| {
1374            row.checked_mul(self.cols)
1375                .and_then(|start| self.yz.get(start..start + self.cols))
1376        })
1377    }
1378
1379    pub fn rs_for(&self, p: &YangZhangVolatilityParams) -> Option<&[f64]> {
1380        self.row_for_params(p).and_then(|row| {
1381            row.checked_mul(self.cols)
1382                .and_then(|start| self.rs.get(start..start + self.cols))
1383        })
1384    }
1385}
1386
1387#[inline(always)]
1388fn expand_grid_yang_zhang(
1389    r: &YangZhangVolatilityBatchRange,
1390) -> Result<Vec<YangZhangVolatilityParams>, YangZhangVolatilityError> {
1391    fn axis_usize(
1392        (start, end, step): (usize, usize, usize),
1393    ) -> Result<Vec<usize>, YangZhangVolatilityError> {
1394        if step == 0 || start == end {
1395            return Ok(vec![start]);
1396        }
1397        let st = step.max(1);
1398        if start < end {
1399            let mut v = Vec::new();
1400            let mut x = start;
1401            while x <= end {
1402                v.push(x);
1403                match x.checked_add(st) {
1404                    Some(next) => {
1405                        if next == x {
1406                            break;
1407                        }
1408                        x = next;
1409                    }
1410                    None => break,
1411                }
1412            }
1413            if v.is_empty() {
1414                return Err(YangZhangVolatilityError::InvalidRange {
1415                    start: start.to_string(),
1416                    end: end.to_string(),
1417                    step: step.to_string(),
1418                });
1419            }
1420            Ok(v)
1421        } else {
1422            let mut v = Vec::new();
1423            let mut x = start;
1424            loop {
1425                v.push(x);
1426                if x == end {
1427                    break;
1428                }
1429                let next = x.saturating_sub(st);
1430                if next == x {
1431                    break;
1432                }
1433                x = next;
1434                if x < end {
1435                    break;
1436                }
1437            }
1438            if v.is_empty() {
1439                return Err(YangZhangVolatilityError::InvalidRange {
1440                    start: start.to_string(),
1441                    end: end.to_string(),
1442                    step: step.to_string(),
1443                });
1444            }
1445            Ok(v)
1446        }
1447    }
1448
1449    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, YangZhangVolatilityError> {
1450        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
1451            return Ok(vec![start]);
1452        }
1453        let st = step.abs();
1454        if start < end {
1455            let mut v = Vec::new();
1456            let mut x = start;
1457            while x <= end + 1e-12 {
1458                v.push(x);
1459                x += st;
1460            }
1461            if v.is_empty() {
1462                return Err(YangZhangVolatilityError::InvalidRange {
1463                    start: start.to_string(),
1464                    end: end.to_string(),
1465                    step: step.to_string(),
1466                });
1467            }
1468            Ok(v)
1469        } else {
1470            let mut v = Vec::new();
1471            let mut x = start;
1472            while x >= end - 1e-12 {
1473                v.push(x);
1474                x -= st;
1475            }
1476            if v.is_empty() {
1477                return Err(YangZhangVolatilityError::InvalidRange {
1478                    start: start.to_string(),
1479                    end: end.to_string(),
1480                    step: step.to_string(),
1481                });
1482            }
1483            Ok(v)
1484        }
1485    }
1486
1487    let lookbacks = axis_usize(r.lookback)?;
1488    let ks = if r.k_override {
1489        axis_f64(r.k)?
1490    } else {
1491        vec![r.k.0]
1492    };
1493
1494    let mut out = Vec::with_capacity(lookbacks.len().saturating_mul(ks.len()));
1495    for &lb in &lookbacks {
1496        for &k in &ks {
1497            out.push(YangZhangVolatilityParams {
1498                lookback: Some(lb),
1499                k_override: Some(r.k_override),
1500                k: Some(k),
1501            });
1502        }
1503    }
1504    Ok(out)
1505}
1506
1507pub fn yang_zhang_volatility_batch_with_kernel(
1508    open: &[f64],
1509    high: &[f64],
1510    low: &[f64],
1511    close: &[f64],
1512    sweep: &YangZhangVolatilityBatchRange,
1513    k: Kernel,
1514) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1515    let kernel = match k {
1516        Kernel::Auto => detect_best_batch_kernel(),
1517        other if other.is_batch() => other,
1518        _ => return Err(YangZhangVolatilityError::InvalidKernelForBatch(k)),
1519    };
1520    yang_zhang_volatility_batch_par_slice(open, high, low, close, sweep, kernel.to_non_batch())
1521}
1522
1523#[inline(always)]
1524pub fn yang_zhang_volatility_batch_slice(
1525    open: &[f64],
1526    high: &[f64],
1527    low: &[f64],
1528    close: &[f64],
1529    sweep: &YangZhangVolatilityBatchRange,
1530    kern: Kernel,
1531) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1532    yang_zhang_volatility_batch_inner(open, high, low, close, sweep, kern, false)
1533}
1534
1535#[inline(always)]
1536pub fn yang_zhang_volatility_batch_par_slice(
1537    open: &[f64],
1538    high: &[f64],
1539    low: &[f64],
1540    close: &[f64],
1541    sweep: &YangZhangVolatilityBatchRange,
1542    kern: Kernel,
1543) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1544    yang_zhang_volatility_batch_inner(open, high, low, close, sweep, kern, true)
1545}
1546
1547#[inline(always)]
1548fn yang_zhang_volatility_batch_inner(
1549    open: &[f64],
1550    high: &[f64],
1551    low: &[f64],
1552    close: &[f64],
1553    sweep: &YangZhangVolatilityBatchRange,
1554    kern: Kernel,
1555    parallel: bool,
1556) -> Result<YangZhangVolatilityBatchOutput, YangZhangVolatilityError> {
1557    let combos = expand_grid_yang_zhang(sweep)?;
1558    let len = close.len();
1559    if len == 0 {
1560        return Err(YangZhangVolatilityError::EmptyInputData);
1561    }
1562    if open.len() != len || high.len() != len || low.len() != len {
1563        return Err(YangZhangVolatilityError::InconsistentSliceLengths {
1564            open_len: open.len(),
1565            high_len: high.len(),
1566            low_len: low.len(),
1567            close_len: close.len(),
1568        });
1569    }
1570
1571    let first = first_valid_ohlc(open, high, low, close);
1572    if first >= len {
1573        return Err(YangZhangVolatilityError::AllValuesNaN);
1574    }
1575
1576    let max_lb = combos
1577        .iter()
1578        .map(|c| c.lookback.unwrap_or(14))
1579        .max()
1580        .unwrap_or(0);
1581
1582    if max_lb == 0 || len - first < max_lb + 1 {
1583        return Err(YangZhangVolatilityError::NotEnoughValidData {
1584            needed: max_lb + 1,
1585            valid: len - first,
1586        });
1587    }
1588
1589    let rows = combos.len();
1590    let cols = len;
1591
1592    let mut buf_yz_mu = make_uninit_matrix(rows, cols);
1593    let mut buf_rs_mu = make_uninit_matrix(rows, cols);
1594
1595    let warmup_periods: Vec<usize> = combos
1596        .iter()
1597        .map(|c| first.saturating_add(c.lookback.unwrap_or(14)))
1598        .collect();
1599
1600    init_matrix_prefixes(&mut buf_yz_mu, cols, &warmup_periods);
1601    init_matrix_prefixes(&mut buf_rs_mu, cols, &warmup_periods);
1602
1603    let mut buf_yz_guard = ManuallyDrop::new(buf_yz_mu);
1604    let mut buf_rs_guard = ManuallyDrop::new(buf_rs_mu);
1605
1606    let yz: &mut [f64] = unsafe {
1607        core::slice::from_raw_parts_mut(buf_yz_guard.as_mut_ptr() as *mut f64, buf_yz_guard.len())
1608    };
1609    let rs: &mut [f64] = unsafe {
1610        core::slice::from_raw_parts_mut(buf_rs_guard.as_mut_ptr() as *mut f64, buf_rs_guard.len())
1611    };
1612
1613    yang_zhang_volatility_batch_inner_into(open, high, low, close, sweep, kern, parallel, yz, rs)?;
1614
1615    for (row, &warmup) in warmup_periods.iter().enumerate() {
1616        let row_start = row * cols;
1617        let warm_end = (row_start + warmup).min(row_start + cols);
1618        for i in row_start..warm_end {
1619            yz[i] = f64::NAN;
1620            rs[i] = f64::NAN;
1621        }
1622    }
1623
1624    let yz_values = unsafe {
1625        Vec::from_raw_parts(
1626            buf_yz_guard.as_mut_ptr() as *mut f64,
1627            buf_yz_guard.len(),
1628            buf_yz_guard.capacity(),
1629        )
1630    };
1631    let rs_values = unsafe {
1632        Vec::from_raw_parts(
1633            buf_rs_guard.as_mut_ptr() as *mut f64,
1634            buf_rs_guard.len(),
1635            buf_rs_guard.capacity(),
1636        )
1637    };
1638
1639    Ok(YangZhangVolatilityBatchOutput {
1640        yz: yz_values,
1641        rs: rs_values,
1642        combos,
1643        rows,
1644        cols,
1645    })
1646}
1647
1648#[inline(always)]
1649fn yang_zhang_row_precomputed_into(
1650    oret: &[f64],
1651    cret: &[f64],
1652    rs_val: &[f64],
1653    lookback: usize,
1654    first: usize,
1655    k: f64,
1656    out_yz: &mut [f64],
1657    out_rs: &mut [f64],
1658) {
1659    let len = out_yz.len();
1660    let warmup = first + lookback;
1661    if warmup >= len {
1662        return;
1663    }
1664
1665    let lb_f = lookback as f64;
1666    let start = warmup;
1667    let win_start = start + 1 - lookback;
1668
1669    let mut sum_rs = 0.0;
1670    let mut sum_o = 0.0;
1671    let mut sumsq_o = 0.0;
1672    let mut sum_c = 0.0;
1673    let mut sumsq_c = 0.0;
1674
1675    for j in win_start..=start {
1676        let rsc = rs_val[j];
1677        sum_rs += rsc;
1678
1679        let o = oret[j];
1680        sum_o += o;
1681        sumsq_o += o * o;
1682
1683        let c = cret[j];
1684        sum_c += c;
1685        sumsq_c += c * c;
1686    }
1687
1688    for t in start..len {
1689        let mut rs_var = sum_rs / lb_f;
1690        if rs_var < 0.0 {
1691            rs_var = 0.0;
1692        }
1693        out_rs[t] = rs_var.sqrt();
1694
1695        let o_var = sample_var(sum_o, sumsq_o, lookback);
1696        let c_var = sample_var(sum_c, sumsq_c, lookback);
1697
1698        let mut yz_var = o_var + k * c_var + (1.0 - k) * rs_var;
1699        if yz_var < 0.0 {
1700            yz_var = 0.0;
1701        }
1702        out_yz[t] = yz_var.sqrt();
1703
1704        if t + 1 < len {
1705            let add_idx = t + 1;
1706            let sub_idx = add_idx - lookback;
1707
1708            sum_rs += rs_val[add_idx] - rs_val[sub_idx];
1709
1710            let ao = oret[add_idx];
1711            let so = oret[sub_idx];
1712            sum_o += ao - so;
1713            sumsq_o += ao * ao - so * so;
1714
1715            let ac = cret[add_idx];
1716            let sc = cret[sub_idx];
1717            sum_c += ac - sc;
1718            sumsq_c += ac * ac - sc * sc;
1719        }
1720    }
1721}
1722
1723#[inline(always)]
1724fn yang_zhang_volatility_batch_inner_into(
1725    open: &[f64],
1726    high: &[f64],
1727    low: &[f64],
1728    close: &[f64],
1729    sweep: &YangZhangVolatilityBatchRange,
1730    kern: Kernel,
1731    parallel: bool,
1732    out_yz: &mut [f64],
1733    out_rs: &mut [f64],
1734) -> Result<Vec<YangZhangVolatilityParams>, YangZhangVolatilityError> {
1735    let combos = expand_grid_yang_zhang(sweep)?;
1736
1737    let len = close.len();
1738    if len == 0 {
1739        return Err(YangZhangVolatilityError::EmptyInputData);
1740    }
1741    if open.len() != len || high.len() != len || low.len() != len {
1742        return Err(YangZhangVolatilityError::InconsistentSliceLengths {
1743            open_len: open.len(),
1744            high_len: high.len(),
1745            low_len: low.len(),
1746            close_len: close.len(),
1747        });
1748    }
1749
1750    let first = first_valid_ohlc(open, high, low, close);
1751    if first >= len {
1752        return Err(YangZhangVolatilityError::AllValuesNaN);
1753    }
1754
1755    let max_lb = combos
1756        .iter()
1757        .map(|c| c.lookback.unwrap_or(14))
1758        .max()
1759        .unwrap_or(0);
1760
1761    if max_lb == 0 || len - first < max_lb + 1 {
1762        return Err(YangZhangVolatilityError::NotEnoughValidData {
1763            needed: max_lb + 1,
1764            valid: len - first,
1765        });
1766    }
1767
1768    let rows = combos.len();
1769    let cols = len;
1770    let expected =
1771        rows.checked_mul(cols)
1772            .ok_or_else(|| YangZhangVolatilityError::InvalidRange {
1773                start: rows.to_string(),
1774                end: cols.to_string(),
1775                step: "rows*cols".into(),
1776            })?;
1777
1778    if out_yz.len() != expected || out_rs.len() != expected {
1779        return Err(YangZhangVolatilityError::OutputLengthMismatch {
1780            expected,
1781            got: out_yz.len().max(out_rs.len()),
1782        });
1783    }
1784
1785    let chosen = match kern {
1786        Kernel::Auto => detect_best_kernel(),
1787        k => k,
1788    };
1789
1790    let mut oret = alloc_uninit_f64(len);
1791    let mut cret = alloc_uninit_f64(len);
1792    let mut rs_val = alloc_uninit_f64(len);
1793    match chosen {
1794        Kernel::Scalar => {
1795            if len != 0 {
1796                oret[0] = 0.0;
1797            }
1798
1799            for i in 0..len {
1800                rs_val[i] = rs_component(high[i], low[i], open[i], close[i]);
1801                cret[i] = (close[i] / open[i]).ln();
1802                if i > 0 {
1803                    oret[i] = (open[i] / close[i - 1]).ln();
1804                }
1805            }
1806        }
1807        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1808        Kernel::Avx2 => unsafe {
1809            yang_zhang_precompute_ln_diff_avx2(
1810                open,
1811                high,
1812                low,
1813                close,
1814                &mut oret,
1815                &mut cret,
1816                &mut rs_val,
1817            );
1818        },
1819        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1820        Kernel::Avx512 => unsafe {
1821            yang_zhang_precompute_ln_diff_avx512(
1822                open,
1823                high,
1824                low,
1825                close,
1826                &mut oret,
1827                &mut cret,
1828                &mut rs_val,
1829            );
1830        },
1831        #[allow(unreachable_patterns)]
1832        _ => {
1833            if len != 0 {
1834                oret[0] = 0.0;
1835            }
1836
1837            for i in 0..len {
1838                rs_val[i] = rs_component(high[i], low[i], open[i], close[i]);
1839                cret[i] = (close[i] / open[i]).ln();
1840                if i > 0 {
1841                    oret[i] = (open[i] / close[i - 1]).ln();
1842                }
1843            }
1844        }
1845    }
1846
1847    let warmup_periods: Vec<usize> = combos
1848        .iter()
1849        .map(|c| first.saturating_add(c.lookback.unwrap_or(14)))
1850        .collect();
1851
1852    for (row, &warmup) in warmup_periods.iter().enumerate() {
1853        let row_start = row * cols;
1854        let warm = warmup.min(cols);
1855        out_yz[row_start..row_start + warm].fill(f64::NAN);
1856        out_rs[row_start..row_start + warm].fill(f64::NAN);
1857    }
1858
1859    let do_row = |row: usize,
1860                  dst_yz: &mut [f64],
1861                  dst_rs: &mut [f64]|
1862     -> Result<(), YangZhangVolatilityError> {
1863        let lookback = combos[row].lookback.unwrap_or(14);
1864        if lookback == 0 {
1865            return Err(YangZhangVolatilityError::InvalidLookback {
1866                lookback,
1867                data_len: len,
1868            });
1869        }
1870
1871        let ko = combos[row].k_override.unwrap_or(false);
1872        let k = if ko {
1873            let k = combos[row].k.unwrap_or(0.34);
1874            if !k.is_finite() || !(0.0..=1.0).contains(&k) {
1875                return Err(YangZhangVolatilityError::InvalidK { k });
1876            }
1877            k
1878        } else {
1879            k_default(lookback)
1880        };
1881
1882        yang_zhang_row_precomputed_into(&oret, &cret, &rs_val, lookback, first, k, dst_yz, dst_rs);
1883        Ok(())
1884    };
1885
1886    if parallel {
1887        #[cfg(not(target_arch = "wasm32"))]
1888        {
1889            out_yz
1890                .par_chunks_mut(cols)
1891                .zip(out_rs.par_chunks_mut(cols))
1892                .enumerate()
1893                .try_for_each(|(row, (y, r))| do_row(row, y, r))?;
1894        }
1895
1896        #[cfg(target_arch = "wasm32")]
1897        {
1898            for (row, (y, r)) in out_yz
1899                .chunks_mut(cols)
1900                .zip(out_rs.chunks_mut(cols))
1901                .enumerate()
1902            {
1903                do_row(row, y, r)?;
1904            }
1905        }
1906    } else {
1907        for (row, (y, r)) in out_yz
1908            .chunks_mut(cols)
1909            .zip(out_rs.chunks_mut(cols))
1910            .enumerate()
1911        {
1912            do_row(row, y, r)?;
1913        }
1914    }
1915
1916    for (row, &warmup) in warmup_periods.iter().enumerate() {
1917        let row_start = row * cols;
1918        let warm = warmup.min(cols);
1919        out_yz[row_start..row_start + warm].fill(f64::NAN);
1920        out_rs[row_start..row_start + warm].fill(f64::NAN);
1921    }
1922
1923    Ok(combos)
1924}
1925
1926#[cfg(feature = "python")]
1927#[pyfunction(name = "yang_zhang_volatility")]
1928#[pyo3(signature = (open, high, low, close, lookback, k_override=false, k=0.34, kernel=None))]
1929pub fn yang_zhang_volatility_py<'py>(
1930    py: Python<'py>,
1931    open: PyReadonlyArray1<'py, f64>,
1932    high: PyReadonlyArray1<'py, f64>,
1933    low: PyReadonlyArray1<'py, f64>,
1934    close: PyReadonlyArray1<'py, f64>,
1935    lookback: usize,
1936    k_override: bool,
1937    k: f64,
1938    kernel: Option<&str>,
1939) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
1940    let o = open.as_slice()?;
1941    let h = high.as_slice()?;
1942    let l = low.as_slice()?;
1943    let c = close.as_slice()?;
1944    if o.len() != h.len() || o.len() != l.len() || o.len() != c.len() {
1945        return Err(PyValueError::new_err("OHLC slice length mismatch"));
1946    }
1947
1948    let kern = validate_kernel(kernel, false)?;
1949    let params = YangZhangVolatilityParams {
1950        lookback: Some(lookback),
1951        k_override: Some(k_override),
1952        k: Some(k),
1953    };
1954    let input = YangZhangVolatilityInput::from_slices(o, h, l, c, params);
1955
1956    let out = py
1957        .allow_threads(|| yang_zhang_volatility_with_kernel(&input, kern))
1958        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1959
1960    Ok((out.yz.into_pyarray(py), out.rs.into_pyarray(py)))
1961}
1962
1963#[cfg(feature = "python")]
1964#[pyclass(name = "YangZhangVolatilityStream")]
1965pub struct YangZhangVolatilityStreamPy {
1966    stream: YangZhangVolatilityStream,
1967}
1968
1969#[cfg(feature = "python")]
1970#[pymethods]
1971impl YangZhangVolatilityStreamPy {
1972    #[new]
1973    fn new(lookback: usize, k_override: bool, k: f64) -> PyResult<Self> {
1974        let params = YangZhangVolatilityParams {
1975            lookback: Some(lookback),
1976            k_override: Some(k_override),
1977            k: Some(k),
1978        };
1979        let stream = YangZhangVolatilityStream::try_new(params)
1980            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1981        Ok(Self { stream })
1982    }
1983
1984    fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
1985        self.stream.update(open, high, low, close)
1986    }
1987}
1988
1989#[cfg(feature = "python")]
1990#[pyfunction(name = "yang_zhang_volatility_batch")]
1991#[pyo3(signature = (open, high, low, close, lookback_range, k_override=false, k_range=(0.34,0.34,0.0), kernel=None))]
1992pub fn yang_zhang_volatility_batch_py<'py>(
1993    py: Python<'py>,
1994    open: PyReadonlyArray1<'py, f64>,
1995    high: PyReadonlyArray1<'py, f64>,
1996    low: PyReadonlyArray1<'py, f64>,
1997    close: PyReadonlyArray1<'py, f64>,
1998    lookback_range: (usize, usize, usize),
1999    k_override: bool,
2000    k_range: (f64, f64, f64),
2001    kernel: Option<&str>,
2002) -> PyResult<Bound<'py, PyDict>> {
2003    let o = open.as_slice()?;
2004    let h = high.as_slice()?;
2005    let l = low.as_slice()?;
2006    let c = close.as_slice()?;
2007    if o.len() != h.len() || o.len() != l.len() || o.len() != c.len() {
2008        return Err(PyValueError::new_err("OHLC slice length mismatch"));
2009    }
2010
2011    let sweep = YangZhangVolatilityBatchRange {
2012        lookback: lookback_range,
2013        k_override,
2014        k: k_range,
2015    };
2016
2017    let combos =
2018        expand_grid_yang_zhang(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2019    let rows = combos.len();
2020    let cols = c.len();
2021    let total = rows
2022        .checked_mul(cols)
2023        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
2024
2025    let yz_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2026    let rs_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2027    let yz_out = unsafe { yz_arr.as_slice_mut()? };
2028    let rs_out = unsafe { rs_arr.as_slice_mut()? };
2029
2030    let kern = validate_kernel(kernel, true)?;
2031    py.allow_threads(|| {
2032        let batch = match kern {
2033            Kernel::Auto => detect_best_batch_kernel(),
2034            k => k,
2035        };
2036        yang_zhang_volatility_batch_inner_into(
2037            o,
2038            h,
2039            l,
2040            c,
2041            &sweep,
2042            batch.to_non_batch(),
2043            true,
2044            yz_out,
2045            rs_out,
2046        )
2047    })
2048    .map_err(|e| PyValueError::new_err(e.to_string()))?;
2049
2050    let dict = PyDict::new(py);
2051    dict.set_item("yz", yz_arr.reshape((rows, cols))?)?;
2052    dict.set_item("rs", rs_arr.reshape((rows, cols))?)?;
2053    dict.set_item(
2054        "lookbacks",
2055        combos
2056            .iter()
2057            .map(|p| p.lookback.unwrap_or(14) as u64)
2058            .collect::<Vec<_>>()
2059            .into_pyarray(py),
2060    )?;
2061    dict.set_item(
2062        "k_overrides",
2063        combos
2064            .iter()
2065            .map(|p| p.k_override.unwrap_or(false))
2066            .collect::<Vec<_>>()
2067            .into_pyarray(py),
2068    )?;
2069    dict.set_item(
2070        "ks",
2071        combos
2072            .iter()
2073            .map(|p| p.k.unwrap_or(0.34))
2074            .collect::<Vec<_>>()
2075            .into_pyarray(py),
2076    )?;
2077    dict.set_item("rows", rows)?;
2078    dict.set_item("cols", cols)?;
2079    Ok(dict)
2080}
2081
2082#[cfg(feature = "python")]
2083pub fn register_yang_zhang_volatility_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
2084    m.add_function(wrap_pyfunction!(yang_zhang_volatility_py, m)?)?;
2085    m.add_function(wrap_pyfunction!(yang_zhang_volatility_batch_py, m)?)?;
2086    m.add_class::<YangZhangVolatilityStreamPy>()?;
2087    Ok(())
2088}
2089
2090#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2091#[wasm_bindgen(js_name = "yang_zhang_volatility_js")]
2092pub fn yang_zhang_volatility_js(
2093    open: &[f64],
2094    high: &[f64],
2095    low: &[f64],
2096    close: &[f64],
2097    lookback: usize,
2098    k_override: bool,
2099    k: f64,
2100) -> Result<JsValue, JsValue> {
2101    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
2102        return Err(JsValue::from_str("OHLC slice length mismatch"));
2103    }
2104
2105    let params = YangZhangVolatilityParams {
2106        lookback: Some(lookback),
2107        k_override: Some(k_override),
2108        k: Some(k),
2109    };
2110    let input = YangZhangVolatilityInput::from_slices(open, high, low, close, params);
2111
2112    let mut yz = vec![0.0; close.len()];
2113    let mut rs = vec![0.0; close.len()];
2114    yang_zhang_volatility_into_slice(&mut yz, &mut rs, &input, Kernel::Auto)
2115        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2116
2117    let obj = js_sys::Object::new();
2118    js_sys::Reflect::set(
2119        &obj,
2120        &JsValue::from_str("yz"),
2121        &serde_wasm_bindgen::to_value(&yz).unwrap(),
2122    )?;
2123    js_sys::Reflect::set(
2124        &obj,
2125        &JsValue::from_str("rs"),
2126        &serde_wasm_bindgen::to_value(&rs).unwrap(),
2127    )?;
2128
2129    Ok(obj.into())
2130}
2131
2132#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2133#[derive(Serialize, Deserialize)]
2134pub struct YangZhangVolatilityBatchConfig {
2135    pub lookback_range: Vec<usize>,
2136    pub k_override: bool,
2137    pub k_range: Vec<f64>,
2138}
2139
2140#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2141#[wasm_bindgen(js_name = "yang_zhang_volatility_batch_js")]
2142pub fn yang_zhang_volatility_batch_js(
2143    open: &[f64],
2144    high: &[f64],
2145    low: &[f64],
2146    close: &[f64],
2147    config: JsValue,
2148) -> Result<JsValue, JsValue> {
2149    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
2150        return Err(JsValue::from_str("OHLC slice length mismatch"));
2151    }
2152
2153    let config: YangZhangVolatilityBatchConfig = serde_wasm_bindgen::from_value(config)
2154        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2155
2156    if config.lookback_range.len() != 3 {
2157        return Err(JsValue::from_str(
2158            "Invalid config: lookback_range must have exactly 3 elements [start, end, step]",
2159        ));
2160    }
2161    if config.k_range.len() != 3 {
2162        return Err(JsValue::from_str(
2163            "Invalid config: k_range must have exactly 3 elements [start, end, step]",
2164        ));
2165    }
2166
2167    let sweep = YangZhangVolatilityBatchRange {
2168        lookback: (
2169            config.lookback_range[0],
2170            config.lookback_range[1],
2171            config.lookback_range[2],
2172        ),
2173        k_override: config.k_override,
2174        k: (config.k_range[0], config.k_range[1], config.k_range[2]),
2175    };
2176
2177    let combos = expand_grid_yang_zhang(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2178    let rows = combos.len();
2179    let cols = close.len();
2180    let total = rows
2181        .checked_mul(cols)
2182        .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
2183
2184    let mut yz = vec![0.0; total];
2185    let mut rs = vec![0.0; total];
2186
2187    yang_zhang_volatility_batch_inner_into(
2188        open,
2189        high,
2190        low,
2191        close,
2192        &sweep,
2193        detect_best_kernel(),
2194        false,
2195        &mut yz,
2196        &mut rs,
2197    )
2198    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2199
2200    let obj = js_sys::Object::new();
2201    js_sys::Reflect::set(
2202        &obj,
2203        &JsValue::from_str("yz"),
2204        &serde_wasm_bindgen::to_value(&yz).unwrap(),
2205    )?;
2206    js_sys::Reflect::set(
2207        &obj,
2208        &JsValue::from_str("rs"),
2209        &serde_wasm_bindgen::to_value(&rs).unwrap(),
2210    )?;
2211    js_sys::Reflect::set(
2212        &obj,
2213        &JsValue::from_str("rows"),
2214        &JsValue::from_f64(rows as f64),
2215    )?;
2216    js_sys::Reflect::set(
2217        &obj,
2218        &JsValue::from_str("cols"),
2219        &JsValue::from_f64(cols as f64),
2220    )?;
2221    js_sys::Reflect::set(
2222        &obj,
2223        &JsValue::from_str("combos"),
2224        &serde_wasm_bindgen::to_value(&combos).unwrap(),
2225    )?;
2226    Ok(obj.into())
2227}
2228
2229#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2230#[wasm_bindgen]
2231pub fn yang_zhang_volatility_alloc(len: usize) -> *mut f64 {
2232    let mut v = Vec::<f64>::with_capacity(2 * len);
2233    let p = v.as_mut_ptr();
2234    std::mem::forget(v);
2235    p
2236}
2237
2238#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2239#[wasm_bindgen]
2240pub fn yang_zhang_volatility_free(ptr: *mut f64, len: usize) {
2241    unsafe {
2242        let _ = Vec::from_raw_parts(ptr, 2 * len, 2 * len);
2243    }
2244}
2245
2246#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2247#[wasm_bindgen]
2248pub fn yang_zhang_volatility_into(
2249    open_ptr: *const f64,
2250    high_ptr: *const f64,
2251    low_ptr: *const f64,
2252    close_ptr: *const f64,
2253    out_ptr: *mut f64,
2254    len: usize,
2255    lookback: usize,
2256    k_override: bool,
2257    k: f64,
2258) -> Result<(), JsValue> {
2259    if open_ptr.is_null()
2260        || high_ptr.is_null()
2261        || low_ptr.is_null()
2262        || close_ptr.is_null()
2263        || out_ptr.is_null()
2264    {
2265        return Err(JsValue::from_str(
2266            "null pointer passed to yang_zhang_volatility_into",
2267        ));
2268    }
2269    unsafe {
2270        let open = std::slice::from_raw_parts(open_ptr, len);
2271        let high = std::slice::from_raw_parts(high_ptr, len);
2272        let low = std::slice::from_raw_parts(low_ptr, len);
2273        let close = std::slice::from_raw_parts(close_ptr, len);
2274        let out = std::slice::from_raw_parts_mut(out_ptr, 2 * len);
2275
2276        let params = YangZhangVolatilityParams {
2277            lookback: Some(lookback),
2278            k_override: Some(k_override),
2279            k: Some(k),
2280        };
2281        let input = YangZhangVolatilityInput::from_slices(open, high, low, close, params);
2282
2283        let (yz, rs) = out.split_at_mut(len);
2284        yang_zhang_volatility_into_slice(yz, rs, &input, Kernel::Auto)
2285            .map_err(|e| JsValue::from_str(&e.to_string()))
2286    }
2287}
2288
2289#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2290#[wasm_bindgen]
2291pub fn yang_zhang_volatility_batch_into(
2292    open_ptr: *const f64,
2293    high_ptr: *const f64,
2294    low_ptr: *const f64,
2295    close_ptr: *const f64,
2296    yz_ptr: *mut f64,
2297    rs_ptr: *mut f64,
2298    len: usize,
2299    lookback_start: usize,
2300    lookback_end: usize,
2301    lookback_step: usize,
2302    k_override: bool,
2303    k_start: f64,
2304    k_end: f64,
2305    k_step: f64,
2306) -> Result<usize, JsValue> {
2307    if open_ptr.is_null()
2308        || high_ptr.is_null()
2309        || low_ptr.is_null()
2310        || close_ptr.is_null()
2311        || yz_ptr.is_null()
2312        || rs_ptr.is_null()
2313    {
2314        return Err(JsValue::from_str("null pointer"));
2315    }
2316
2317    unsafe {
2318        let open = std::slice::from_raw_parts(open_ptr, len);
2319        let high = std::slice::from_raw_parts(high_ptr, len);
2320        let low = std::slice::from_raw_parts(low_ptr, len);
2321        let close = std::slice::from_raw_parts(close_ptr, len);
2322
2323        let sweep = YangZhangVolatilityBatchRange {
2324            lookback: (lookback_start, lookback_end, lookback_step),
2325            k_override,
2326            k: (k_start, k_end, k_step),
2327        };
2328
2329        let combos =
2330            expand_grid_yang_zhang(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2331        let rows = combos.len();
2332        let cols = len;
2333
2334        let yz_out = std::slice::from_raw_parts_mut(yz_ptr, rows * cols);
2335        let rs_out = std::slice::from_raw_parts_mut(rs_ptr, rows * cols);
2336
2337        yang_zhang_volatility_batch_inner_into(
2338            open,
2339            high,
2340            low,
2341            close,
2342            &sweep,
2343            detect_best_kernel(),
2344            false,
2345            yz_out,
2346            rs_out,
2347        )
2348        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2349
2350        Ok(rows)
2351    }
2352}
2353
2354#[cfg(test)]
2355mod tests {
2356    use super::*;
2357    use crate::skip_if_unsupported;
2358    use crate::utilities::data_loader::read_candles_from_csv;
2359    use std::error::Error;
2360
2361    const TEST_FILE: &str = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2362
2363    #[inline]
2364    fn eq_or_both_nan_eps(a: f64, b: f64, eps: f64) -> bool {
2365        (a.is_nan() && b.is_nan()) || (a - b).abs() <= eps
2366    }
2367
2368    fn assert_series_close(test: &str, lhs: &[f64], rhs: &[f64], eps: f64, label: &str) {
2369        assert_eq!(
2370            lhs.len(),
2371            rhs.len(),
2372            "[{test}] {label} length mismatch: {} vs {}",
2373            lhs.len(),
2374            rhs.len()
2375        );
2376        for i in 0..lhs.len() {
2377            assert!(
2378                eq_or_both_nan_eps(lhs[i], rhs[i], eps),
2379                "[{test}] {label} mismatch at index {i}: {} vs {}",
2380                lhs[i],
2381                rhs[i]
2382            );
2383        }
2384    }
2385
2386    fn check_yang_zhang_partial_params(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2387        skip_if_unsupported!(kernel, test);
2388        let candles = read_candles_from_csv(TEST_FILE)?;
2389        let params = YangZhangVolatilityParams {
2390            lookback: None,
2391            k_override: None,
2392            k: None,
2393        };
2394        let input = YangZhangVolatilityInput::from_candles(&candles, params);
2395        let out = yang_zhang_volatility_with_kernel(&input, kernel)?;
2396        assert_eq!(out.yz.len(), candles.close.len());
2397        assert_eq!(out.rs.len(), candles.close.len());
2398        Ok(())
2399    }
2400
2401    fn check_yang_zhang_default_candles(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2402        skip_if_unsupported!(kernel, test);
2403        let candles = read_candles_from_csv(TEST_FILE)?;
2404        let input = YangZhangVolatilityInput::with_default_candles(&candles);
2405        match input.data {
2406            YangZhangVolatilityData::Candles { .. } => {}
2407            _ => panic!("Expected YangZhangVolatilityData::Candles"),
2408        }
2409        let out = yang_zhang_volatility_with_kernel(&input, kernel)?;
2410        assert_eq!(out.yz.len(), candles.close.len());
2411        assert_eq!(out.rs.len(), candles.close.len());
2412        Ok(())
2413    }
2414
2415    fn check_yang_zhang_empty_input(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2416        skip_if_unsupported!(kernel, test);
2417        let empty: [f64; 0] = [];
2418        let input = YangZhangVolatilityInput::from_slices(
2419            &empty,
2420            &empty,
2421            &empty,
2422            &empty,
2423            YangZhangVolatilityParams::default(),
2424        );
2425        let res = yang_zhang_volatility_with_kernel(&input, kernel);
2426        assert!(
2427            matches!(res, Err(YangZhangVolatilityError::EmptyInputData)),
2428            "[{test}] expected EmptyInputData"
2429        );
2430        Ok(())
2431    }
2432
2433    fn check_yang_zhang_inconsistent_slices(
2434        test: &str,
2435        kernel: Kernel,
2436    ) -> Result<(), Box<dyn Error>> {
2437        skip_if_unsupported!(kernel, test);
2438        let open = [10.0, 11.0, 12.0, 13.0];
2439        let high = [10.5, 11.5, 12.5];
2440        let low = [9.5, 10.5, 11.5, 12.5];
2441        let close = [10.2, 11.1, 12.3, 13.4];
2442        let input = YangZhangVolatilityInput::from_slices(
2443            &open,
2444            &high,
2445            &low,
2446            &close,
2447            YangZhangVolatilityParams::default(),
2448        );
2449        let res = yang_zhang_volatility_with_kernel(&input, kernel);
2450        assert!(
2451            matches!(
2452                res,
2453                Err(YangZhangVolatilityError::InconsistentSliceLengths { .. })
2454            ),
2455            "[{test}] expected InconsistentSliceLengths"
2456        );
2457        Ok(())
2458    }
2459
2460    fn check_yang_zhang_invalid_lookback_zero(
2461        test: &str,
2462        kernel: Kernel,
2463    ) -> Result<(), Box<dyn Error>> {
2464        skip_if_unsupported!(kernel, test);
2465        let open = [10.0, 11.0, 12.0, 13.0];
2466        let high = [10.5, 11.5, 12.5, 13.5];
2467        let low = [9.5, 10.5, 11.5, 12.5];
2468        let close = [10.2, 11.1, 12.3, 13.4];
2469        let input = YangZhangVolatilityInput::from_slices(
2470            &open,
2471            &high,
2472            &low,
2473            &close,
2474            YangZhangVolatilityParams {
2475                lookback: Some(0),
2476                k_override: Some(false),
2477                k: Some(0.34),
2478            },
2479        );
2480        let res = yang_zhang_volatility_with_kernel(&input, kernel);
2481        assert!(
2482            matches!(
2483                res,
2484                Err(YangZhangVolatilityError::InvalidLookback { lookback: 0, .. })
2485            ),
2486            "[{test}] expected InvalidLookback(0)"
2487        );
2488        Ok(())
2489    }
2490
2491    fn check_yang_zhang_invalid_lookback_exceeds_len(
2492        test: &str,
2493        kernel: Kernel,
2494    ) -> Result<(), Box<dyn Error>> {
2495        skip_if_unsupported!(kernel, test);
2496        let open = [10.0, 11.0, 12.0, 13.0];
2497        let high = [10.5, 11.5, 12.5, 13.5];
2498        let low = [9.5, 10.5, 11.5, 12.5];
2499        let close = [10.2, 11.1, 12.3, 13.4];
2500        let input = YangZhangVolatilityInput::from_slices(
2501            &open,
2502            &high,
2503            &low,
2504            &close,
2505            YangZhangVolatilityParams {
2506                lookback: Some(16),
2507                k_override: Some(false),
2508                k: Some(0.34),
2509            },
2510        );
2511        let res = yang_zhang_volatility_with_kernel(&input, kernel);
2512        assert!(
2513            matches!(
2514                res,
2515                Err(YangZhangVolatilityError::InvalidLookback { lookback: 16, .. })
2516            ),
2517            "[{test}] expected InvalidLookback(16)"
2518        );
2519        Ok(())
2520    }
2521
2522    fn check_yang_zhang_invalid_k_override(
2523        test: &str,
2524        kernel: Kernel,
2525    ) -> Result<(), Box<dyn Error>> {
2526        skip_if_unsupported!(kernel, test);
2527        let open = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
2528        let high = [10.5, 11.5, 12.5, 13.5, 14.5, 15.5];
2529        let low = [9.5, 10.5, 11.5, 12.5, 13.5, 14.5];
2530        let close = [10.2, 11.1, 12.3, 13.4, 14.2, 15.1];
2531        let input = YangZhangVolatilityInput::from_slices(
2532            &open,
2533            &high,
2534            &low,
2535            &close,
2536            YangZhangVolatilityParams {
2537                lookback: Some(3),
2538                k_override: Some(true),
2539                k: Some(1.25),
2540            },
2541        );
2542        let res = yang_zhang_volatility_with_kernel(&input, kernel);
2543        assert!(
2544            matches!(res, Err(YangZhangVolatilityError::InvalidK { .. })),
2545            "[{test}] expected InvalidK"
2546        );
2547        Ok(())
2548    }
2549
2550    fn check_yang_zhang_nan_handling(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2551        skip_if_unsupported!(kernel, test);
2552        let candles = read_candles_from_csv(TEST_FILE)?;
2553        let input = YangZhangVolatilityInput::with_default_candles(&candles);
2554        let out = yang_zhang_volatility_with_kernel(&input, kernel)?;
2555        let first = first_valid_ohlc(&candles.open, &candles.high, &candles.low, &candles.close);
2556        let warmup = first + input.get_lookback();
2557        for i in warmup..out.yz.len() {
2558            assert!(!out.yz[i].is_nan(), "[{test}] yz NaN at {i}");
2559            assert!(!out.rs[i].is_nan(), "[{test}] rs NaN at {i}");
2560        }
2561        Ok(())
2562    }
2563
2564    fn check_yang_zhang_into_slice_matches_api(
2565        test: &str,
2566        kernel: Kernel,
2567    ) -> Result<(), Box<dyn Error>> {
2568        skip_if_unsupported!(kernel, test);
2569        let candles = read_candles_from_csv(TEST_FILE)?;
2570        let params = YangZhangVolatilityParams {
2571            lookback: Some(20),
2572            k_override: Some(true),
2573            k: Some(0.42),
2574        };
2575        let input = YangZhangVolatilityInput::from_candles(&candles, params);
2576
2577        let baseline = yang_zhang_volatility_with_kernel(&input, kernel)?;
2578
2579        let mut yz = vec![0.0; candles.close.len()];
2580        let mut rs = vec![0.0; candles.close.len()];
2581        yang_zhang_volatility_into_slice(&mut yz, &mut rs, &input, kernel)?;
2582
2583        assert_series_close(test, &baseline.yz, &yz, 1e-12, "into_slice yz");
2584        assert_series_close(test, &baseline.rs, &rs, 1e-12, "into_slice rs");
2585
2586        let mut yz_short = vec![0.0; candles.close.len().saturating_sub(1)];
2587        let mut rs_ok = vec![0.0; candles.close.len()];
2588        let err = yang_zhang_volatility_into_slice(&mut yz_short, &mut rs_ok, &input, kernel)
2589            .expect_err("expected OutputLengthMismatch");
2590        assert!(matches!(
2591            err,
2592            YangZhangVolatilityError::OutputLengthMismatch { .. }
2593        ));
2594        Ok(())
2595    }
2596
2597    fn check_yang_zhang_streaming(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2598        skip_if_unsupported!(kernel, test);
2599        let candles = read_candles_from_csv(TEST_FILE)?;
2600        let params = YangZhangVolatilityParams {
2601            lookback: Some(20),
2602            k_override: Some(true),
2603            k: Some(0.42),
2604        };
2605        let input = YangZhangVolatilityInput::from_candles(&candles, params.clone());
2606        let batch = yang_zhang_volatility_with_kernel(&input, kernel)?;
2607
2608        let mut stream = YangZhangVolatilityStream::try_new(params)?;
2609        let mut yz_stream = Vec::with_capacity(candles.close.len());
2610        let mut rs_stream = Vec::with_capacity(candles.close.len());
2611        for i in 0..candles.close.len() {
2612            match stream.update(
2613                candles.open[i],
2614                candles.high[i],
2615                candles.low[i],
2616                candles.close[i],
2617            ) {
2618                Some((yz, rs)) => {
2619                    yz_stream.push(yz);
2620                    rs_stream.push(rs);
2621                }
2622                None => {
2623                    yz_stream.push(f64::NAN);
2624                    rs_stream.push(f64::NAN);
2625                }
2626            }
2627        }
2628
2629        let eps = match kernel {
2630            Kernel::Avx512 => 1e-10,
2631            _ => 1e-10,
2632        };
2633        assert_series_close(test, &batch.yz, &yz_stream, eps, "stream yz");
2634        assert_series_close(test, &batch.rs, &rs_stream, eps, "stream rs");
2635        Ok(())
2636    }
2637
2638    fn check_yang_zhang_matches_naive(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2639        skip_if_unsupported!(kernel, test);
2640        let candles = read_candles_from_csv(TEST_FILE)?;
2641        let input = YangZhangVolatilityInput::with_default_candles(&candles);
2642        let out = yang_zhang_volatility_with_kernel(&input, kernel)?;
2643
2644        let len = candles.close.len();
2645        assert_eq!(out.yz.len(), len);
2646        assert_eq!(out.rs.len(), len);
2647
2648        let lookback = input.get_lookback();
2649        let k = k_default(lookback);
2650
2651        let open = &candles.open;
2652        let high = &candles.high;
2653        let low = &candles.low;
2654        let close = &candles.close;
2655
2656        let first = first_valid_ohlc(open, high, low, close);
2657        let warmup = first + lookback;
2658
2659        let mut expected_yz = vec![f64::NAN; len];
2660        let mut expected_rs = vec![f64::NAN; len];
2661
2662        for t in warmup..len {
2663            let start = t + 1 - lookback;
2664
2665            let mut sum_rs = 0.0;
2666            let mut sum_o = 0.0;
2667            let mut sumsq_o = 0.0;
2668            let mut sum_c = 0.0;
2669            let mut sumsq_c = 0.0;
2670
2671            for j in start..=t {
2672                let rsc = rs_component(high[j], low[j], open[j], close[j]);
2673                sum_rs += rsc;
2674
2675                let oret = (open[j] / close[j - 1]).ln();
2676                sum_o += oret;
2677                sumsq_o += oret * oret;
2678
2679                let cret = (close[j] / open[j]).ln();
2680                sum_c += cret;
2681                sumsq_c += cret * cret;
2682            }
2683
2684            let mut rs_var = sum_rs / (lookback as f64);
2685            if rs_var < 0.0 {
2686                rs_var = 0.0;
2687            }
2688            expected_rs[t] = rs_var.sqrt();
2689
2690            let o_var = sample_var(sum_o, sumsq_o, lookback);
2691            let c_var = sample_var(sum_c, sumsq_c, lookback);
2692
2693            let mut yz_var = o_var + k * c_var + (1.0 - k) * rs_var;
2694            if yz_var < 0.0 {
2695                yz_var = 0.0;
2696            }
2697            expected_yz[t] = yz_var.sqrt();
2698        }
2699
2700        for i in 0..len {
2701            let a = out.yz[i];
2702            let e = expected_yz[i];
2703            if a.is_nan() && e.is_nan() {
2704                continue;
2705            }
2706            let eps = match kernel {
2707                Kernel::Avx512 => 1e-10,
2708                _ => 1e-12,
2709            };
2710            assert!(
2711                (a - e).abs() <= eps,
2712                "[{test}] YZ mismatch at index {}: expected {}, got {}",
2713                i,
2714                e,
2715                a
2716            );
2717
2718            let a = out.rs[i];
2719            let e = expected_rs[i];
2720            if a.is_nan() && e.is_nan() {
2721                continue;
2722            }
2723            assert!(
2724                (a - e).abs() <= eps,
2725                "[{test}] RS mismatch at index {}: expected {}, got {}",
2726                i,
2727                e,
2728                a
2729            );
2730        }
2731        Ok(())
2732    }
2733
2734    fn check_yang_zhang_near_one_accuracy(
2735        test: &str,
2736        kernel: Kernel,
2737    ) -> Result<(), Box<dyn Error>> {
2738        skip_if_unsupported!(kernel, test);
2739
2740        let len = 256usize;
2741        let gap_cases = [
2742            1e-12, -1e-12, 1e-9, -1e-9, 1e-6, -1e-6, 1e-4, -1e-4, 1e-2, -1e-2, 5e-2, -5e-2, 0.19,
2743            -0.19, 0.24, -0.24,
2744        ];
2745        let body_cases = [
2746            -1e-12, 1e-12, -1e-8, 1e-8, -1e-5, 1e-5, -1e-3, 1e-3, -2e-2, 2e-2, -0.08, 0.08, -0.19,
2747            0.19, -0.24, 0.24,
2748        ];
2749        let wick_cases = [1e-12, 1e-9, 1e-6, 1e-4, 1e-2, 3e-2, 0.15];
2750
2751        let mut open: Vec<f64> = vec![0.0; len];
2752        let mut high: Vec<f64> = vec![0.0; len];
2753        let mut low: Vec<f64> = vec![0.0; len];
2754        let mut close: Vec<f64> = vec![0.0; len];
2755
2756        open[0] = 100.0;
2757        close[0] = 100.05;
2758        high[0] = 100.10;
2759        low[0] = 99.95;
2760
2761        for i in 1..len {
2762            let gap = gap_cases[i % gap_cases.len()];
2763            let body = body_cases[(i * 3) % body_cases.len()];
2764            let upper_wick = wick_cases[(i * 5) % wick_cases.len()];
2765            let lower_wick = wick_cases[(i * 7) % wick_cases.len()];
2766
2767            open[i] = close[i - 1] * (1.0 + gap);
2768            close[i] = open[i] * (1.0 + body);
2769
2770            let top = open[i].max(close[i]);
2771            let bottom = open[i].min(close[i]);
2772            high[i] = top * (1.0 + upper_wick);
2773            low[i] = bottom * (1.0 - lower_wick);
2774        }
2775
2776        let params = YangZhangVolatilityParams {
2777            lookback: Some(14),
2778            k_override: Some(true),
2779            k: Some(0.42),
2780        };
2781        let input = YangZhangVolatilityInput::from_slices(&open, &high, &low, &close, params);
2782        let scalar = yang_zhang_volatility_with_kernel(&input, Kernel::Scalar)?;
2783        let simd = yang_zhang_volatility_with_kernel(&input, kernel)?;
2784
2785        assert_series_close(test, &scalar.yz, &simd.yz, 1e-8, "near-one yz");
2786        assert_series_close(test, &scalar.rs, &simd.rs, 1e-8, "near-one rs");
2787        Ok(())
2788    }
2789
2790    #[cfg(debug_assertions)]
2791    fn check_yang_zhang_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2792        skip_if_unsupported!(kernel, test);
2793        let candles = read_candles_from_csv(TEST_FILE)?;
2794        let configs = [
2795            YangZhangVolatilityParams::default(),
2796            YangZhangVolatilityParams {
2797                lookback: Some(2),
2798                k_override: Some(true),
2799                k: Some(0.0),
2800            },
2801            YangZhangVolatilityParams {
2802                lookback: Some(5),
2803                k_override: Some(true),
2804                k: Some(1.0),
2805            },
2806            YangZhangVolatilityParams {
2807                lookback: Some(30),
2808                k_override: Some(false),
2809                k: Some(0.34),
2810            },
2811            YangZhangVolatilityParams {
2812                lookback: Some(80),
2813                k_override: Some(true),
2814                k: Some(0.5),
2815            },
2816        ];
2817
2818        for params in configs {
2819            let input = YangZhangVolatilityInput::from_candles(&candles, params);
2820            let out = yang_zhang_volatility_with_kernel(&input, kernel)?;
2821            for &v in out.yz.iter().chain(out.rs.iter()) {
2822                if v.is_nan() {
2823                    continue;
2824                }
2825                let bits = v.to_bits();
2826                assert_ne!(
2827                    bits, 0x11111111_11111111,
2828                    "[{test}] poison 0x11 encountered"
2829                );
2830                assert_ne!(
2831                    bits, 0x22222222_22222222,
2832                    "[{test}] poison 0x22 encountered"
2833                );
2834                assert_ne!(
2835                    bits, 0x33333333_33333333,
2836                    "[{test}] poison 0x33 encountered"
2837                );
2838            }
2839        }
2840        Ok(())
2841    }
2842
2843    #[cfg(not(debug_assertions))]
2844    fn check_yang_zhang_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2845        Ok(())
2846    }
2847
2848    macro_rules! generate_all_yang_zhang_tests {
2849        ($($test_fn:ident),*) => {
2850            paste::paste! {
2851                $(
2852                    #[test]
2853                    fn [<$test_fn _scalar_f64>]() {
2854                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2855                    }
2856                )*
2857                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2858                $(
2859                    #[test]
2860                    fn [<$test_fn _avx2_f64>]() {
2861                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2862                    }
2863                    #[test]
2864                    fn [<$test_fn _avx512_f64>]() {
2865                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2866                    }
2867                )*
2868            }
2869        }
2870    }
2871
2872    generate_all_yang_zhang_tests!(
2873        check_yang_zhang_partial_params,
2874        check_yang_zhang_default_candles,
2875        check_yang_zhang_empty_input,
2876        check_yang_zhang_inconsistent_slices,
2877        check_yang_zhang_invalid_lookback_zero,
2878        check_yang_zhang_invalid_lookback_exceeds_len,
2879        check_yang_zhang_invalid_k_override,
2880        check_yang_zhang_nan_handling,
2881        check_yang_zhang_into_slice_matches_api,
2882        check_yang_zhang_streaming,
2883        check_yang_zhang_matches_naive,
2884        check_yang_zhang_near_one_accuracy,
2885        check_yang_zhang_no_poison
2886    );
2887
2888    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2889        skip_if_unsupported!(kernel, test);
2890        let candles = read_candles_from_csv(TEST_FILE)?;
2891        let batch = YangZhangVolatilityBatchBuilder::new()
2892            .kernel(kernel)
2893            .lookback_range(14, 16, 1)
2894            .k_override(false)
2895            .k_static(0.34)
2896            .apply_candles(&candles)?;
2897
2898        let def = YangZhangVolatilityParams::default();
2899        let yz = batch.yz_for(&def).expect("default yz row missing");
2900        let rs = batch.rs_for(&def).expect("default rs row missing");
2901
2902        let input = YangZhangVolatilityInput::from_candles(&candles, def);
2903        let single = yang_zhang_volatility_with_kernel(&input, Kernel::Scalar)?;
2904
2905        assert_series_close(test, yz, &single.yz, 1e-12, "batch default yz");
2906        assert_series_close(test, rs, &single.rs, 1e-12, "batch default rs");
2907        Ok(())
2908    }
2909
2910    fn check_batch_sweep_vs_single(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2911        skip_if_unsupported!(kernel, test);
2912        let candles = read_candles_from_csv(TEST_FILE)?;
2913        let batch = YangZhangVolatilityBatchBuilder::new()
2914            .kernel(kernel)
2915            .lookback_range(8, 12, 2)
2916            .k_override(true)
2917            .k_range(0.2, 0.6, 0.2)
2918            .apply_slices(&candles.open, &candles.high, &candles.low, &candles.close)?;
2919
2920        assert_eq!(batch.rows, batch.combos.len());
2921        assert_eq!(batch.cols, candles.close.len());
2922
2923        for params in &batch.combos {
2924            let row = batch.row_for_params(params).expect("row missing");
2925            let start = row * batch.cols;
2926            let end = start + batch.cols;
2927
2928            let input = YangZhangVolatilityInput::from_candles(&candles, params.clone());
2929            let single = yang_zhang_volatility_with_kernel(&input, Kernel::Scalar)?;
2930
2931            assert_series_close(
2932                test,
2933                &batch.yz[start..end],
2934                &single.yz,
2935                1e-10,
2936                "batch sweep yz",
2937            );
2938            assert_series_close(
2939                test,
2940                &batch.rs[start..end],
2941                &single.rs,
2942                1e-10,
2943                "batch sweep rs",
2944            );
2945        }
2946        Ok(())
2947    }
2948
2949    #[cfg(debug_assertions)]
2950    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2951        skip_if_unsupported!(kernel, test);
2952        let candles = read_candles_from_csv(TEST_FILE)?;
2953        let configs = [
2954            (2, 10, 2, true, 0.0, 1.0, 0.2),
2955            (5, 25, 5, false, 0.34, 0.34, 0.0),
2956            (10, 40, 10, true, 0.2, 0.8, 0.3),
2957            (8, 12, 1, true, 0.4, 0.4, 0.0),
2958        ];
2959
2960        for (lb_s, lb_e, lb_st, ko, k_s, k_e, k_st) in configs {
2961            let out = YangZhangVolatilityBatchBuilder::new()
2962                .kernel(kernel)
2963                .lookback_range(lb_s, lb_e, lb_st)
2964                .k_override(ko)
2965                .k_range(k_s, k_e, k_st)
2966                .apply_candles(&candles)?;
2967            for &v in out.yz.iter().chain(out.rs.iter()) {
2968                if v.is_nan() {
2969                    continue;
2970                }
2971                let bits = v.to_bits();
2972                assert_ne!(
2973                    bits, 0x11111111_11111111,
2974                    "[{test}] poison 0x11 encountered"
2975                );
2976                assert_ne!(
2977                    bits, 0x22222222_22222222,
2978                    "[{test}] poison 0x22 encountered"
2979                );
2980                assert_ne!(
2981                    bits, 0x33333333_33333333,
2982                    "[{test}] poison 0x33 encountered"
2983                );
2984            }
2985        }
2986        Ok(())
2987    }
2988
2989    #[cfg(not(debug_assertions))]
2990    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2991        Ok(())
2992    }
2993
2994    macro_rules! gen_batch_tests {
2995        ($fn_name:ident) => {
2996            paste::paste! {
2997                #[test]
2998                fn [<$fn_name _scalar>]() {
2999                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3000                }
3001                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3002                #[test]
3003                fn [<$fn_name _avx2>]() {
3004                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3005                }
3006                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3007                #[test]
3008                fn [<$fn_name _avx512>]() {
3009                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3010                }
3011                #[test]
3012                fn [<$fn_name _auto_detect>]() {
3013                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3014                }
3015            }
3016        };
3017    }
3018
3019    gen_batch_tests!(check_batch_default_row);
3020    gen_batch_tests!(check_batch_sweep_vs_single);
3021    gen_batch_tests!(check_batch_no_poison);
3022
3023    #[test]
3024    fn test_batch_invalid_kernel_for_batch() -> Result<(), Box<dyn Error>> {
3025        let candles = read_candles_from_csv(TEST_FILE)?;
3026        let sweep = YangZhangVolatilityBatchRange {
3027            lookback: (8, 12, 2),
3028            k_override: true,
3029            k: (0.2, 0.6, 0.2),
3030        };
3031        let err = yang_zhang_volatility_batch_with_kernel(
3032            &candles.open,
3033            &candles.high,
3034            &candles.low,
3035            &candles.close,
3036            &sweep,
3037            Kernel::Scalar,
3038        )
3039        .expect_err("expected InvalidKernelForBatch");
3040        assert!(matches!(
3041            err,
3042            YangZhangVolatilityError::InvalidKernelForBatch(Kernel::Scalar)
3043        ));
3044        Ok(())
3045    }
3046
3047    #[test]
3048    fn test_batch_inner_into_output_length_mismatch() -> Result<(), Box<dyn Error>> {
3049        let candles = read_candles_from_csv(TEST_FILE)?;
3050        let sweep = YangZhangVolatilityBatchRange {
3051            lookback: (8, 12, 2),
3052            k_override: true,
3053            k: (0.2, 0.6, 0.2),
3054        };
3055        let combos = expand_grid_yang_zhang(&sweep)?;
3056        let rows = combos.len();
3057        let cols = candles.close.len();
3058        let expected = rows * cols;
3059        let mut yz = vec![0.0; expected.saturating_sub(1)];
3060        let mut rs = vec![0.0; expected];
3061        let err = yang_zhang_volatility_batch_inner_into(
3062            &candles.open,
3063            &candles.high,
3064            &candles.low,
3065            &candles.close,
3066            &sweep,
3067            Kernel::Scalar,
3068            false,
3069            &mut yz,
3070            &mut rs,
3071        )
3072        .expect_err("expected OutputLengthMismatch");
3073        assert!(matches!(
3074            err,
3075            YangZhangVolatilityError::OutputLengthMismatch { .. }
3076        ));
3077        Ok(())
3078    }
3079
3080    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
3081    #[test]
3082    fn test_yang_zhang_into_matches_api() -> Result<(), Box<dyn Error>> {
3083        let candles = read_candles_from_csv(TEST_FILE)?;
3084        let params = YangZhangVolatilityParams {
3085            lookback: Some(14),
3086            k_override: Some(true),
3087            k: Some(0.34),
3088        };
3089        let input = YangZhangVolatilityInput::from_candles(&candles, params);
3090        let baseline = yang_zhang_volatility(&input)?;
3091
3092        let mut yz = vec![0.0; candles.close.len()];
3093        let mut rs = vec![0.0; candles.close.len()];
3094        yang_zhang_volatility_into(&input, &mut yz, &mut rs)?;
3095
3096        assert_series_close(
3097            "test_yang_zhang_into_matches_api",
3098            &baseline.yz,
3099            &yz,
3100            1e-12,
3101            "into yz",
3102        );
3103        assert_series_close(
3104            "test_yang_zhang_into_matches_api",
3105            &baseline.rs,
3106            &rs,
3107            1e-12,
3108            "into rs",
3109        );
3110        Ok(())
3111    }
3112
3113    #[test]
3114    fn test_yang_zhang_builder_apply_matches_slices() -> Result<(), Box<dyn Error>> {
3115        let candles = read_candles_from_csv(TEST_FILE)?;
3116        let b = YangZhangVolatilityBuilder::new()
3117            .lookback(20)
3118            .k_override(true)
3119            .k(0.42)
3120            .kernel(Kernel::Scalar);
3121        let by_candles = b.apply(&candles)?;
3122        let by_slices =
3123            b.apply_slices(&candles.open, &candles.high, &candles.low, &candles.close)?;
3124        assert_series_close(
3125            "test_yang_zhang_builder_apply_matches_slices",
3126            &by_candles.yz,
3127            &by_slices.yz,
3128            1e-12,
3129            "builder yz",
3130        );
3131        assert_series_close(
3132            "test_yang_zhang_builder_apply_matches_slices",
3133            &by_candles.rs,
3134            &by_slices.rs,
3135            1e-12,
3136            "builder rs",
3137        );
3138        Ok(())
3139    }
3140}