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}