Skip to main content

vector_ta/indicators/
damiani_volatmeter.rs

1use crate::utilities::data_loader::{source_type, Candles};
2#[cfg(all(feature = "python", feature = "cuda"))]
3use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
4use crate::utilities::enums::Kernel;
5use crate::utilities::helpers::{
6    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
7    make_uninit_matrix,
8};
9#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
10use core::arch::x86_64::*;
11#[cfg(not(target_arch = "wasm32"))]
12use rayon::prelude::*;
13use std::convert::AsRef;
14use thiserror::Error;
15
16#[cfg(feature = "python")]
17use crate::utilities::kernel_validation::validate_kernel;
18#[cfg(feature = "python")]
19use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
20#[cfg(feature = "python")]
21use pyo3::exceptions::PyValueError;
22#[cfg(feature = "python")]
23use pyo3::prelude::*;
24#[cfg(feature = "python")]
25use pyo3::types::PyDict;
26
27#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
28use serde::{Deserialize, Serialize};
29#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
30use wasm_bindgen::prelude::*;
31
32impl<'a> AsRef<[f64]> for DamianiVolatmeterInput<'a> {
33    #[inline(always)]
34    fn as_ref(&self) -> &[f64] {
35        match &self.data {
36            DamianiVolatmeterData::Slice(slice) => slice,
37            DamianiVolatmeterData::Candles { candles, source } => source_type(candles, source),
38        }
39    }
40}
41
42#[derive(Debug, Clone)]
43pub enum DamianiVolatmeterData<'a> {
44    Candles {
45        candles: &'a Candles,
46        source: &'a str,
47    },
48    Slice(&'a [f64]),
49}
50
51#[derive(Debug, Clone)]
52pub struct DamianiVolatmeterOutput {
53    pub vol: Vec<f64>,
54    pub anti: Vec<f64>,
55}
56
57#[derive(Debug, Clone)]
58#[cfg_attr(
59    all(target_arch = "wasm32", feature = "wasm"),
60    derive(Serialize, Deserialize)
61)]
62pub struct DamianiVolatmeterParams {
63    pub vis_atr: Option<usize>,
64    pub vis_std: Option<usize>,
65    pub sed_atr: Option<usize>,
66    pub sed_std: Option<usize>,
67    pub threshold: Option<f64>,
68}
69
70impl Default for DamianiVolatmeterParams {
71    fn default() -> Self {
72        Self {
73            vis_atr: Some(13),
74            vis_std: Some(20),
75            sed_atr: Some(40),
76            sed_std: Some(100),
77            threshold: Some(1.4),
78        }
79    }
80}
81
82#[derive(Debug, Clone)]
83pub struct DamianiVolatmeterInput<'a> {
84    pub data: DamianiVolatmeterData<'a>,
85    pub params: DamianiVolatmeterParams,
86}
87
88impl<'a> DamianiVolatmeterInput<'a> {
89    #[inline]
90    pub fn from_candles(c: &'a Candles, s: &'a str, p: DamianiVolatmeterParams) -> Self {
91        Self {
92            data: DamianiVolatmeterData::Candles {
93                candles: c,
94                source: s,
95            },
96            params: p,
97        }
98    }
99    #[inline]
100    pub fn from_slice(sl: &'a [f64], p: DamianiVolatmeterParams) -> Self {
101        Self {
102            data: DamianiVolatmeterData::Slice(sl),
103            params: p,
104        }
105    }
106    #[inline]
107    pub fn with_default_candles(c: &'a Candles) -> Self {
108        Self::from_candles(c, "close", DamianiVolatmeterParams::default())
109    }
110    #[inline]
111    pub fn get_vis_atr(&self) -> usize {
112        self.params.vis_atr.unwrap_or(13)
113    }
114    #[inline]
115    pub fn get_vis_std(&self) -> usize {
116        self.params.vis_std.unwrap_or(20)
117    }
118    #[inline]
119    pub fn get_sed_atr(&self) -> usize {
120        self.params.sed_atr.unwrap_or(40)
121    }
122    #[inline]
123    pub fn get_sed_std(&self) -> usize {
124        self.params.sed_std.unwrap_or(100)
125    }
126    #[inline]
127    pub fn get_threshold(&self) -> f64 {
128        self.params.threshold.unwrap_or(1.4)
129    }
130}
131
132#[derive(Copy, Clone, Debug)]
133pub struct DamianiVolatmeterBuilder {
134    vis_atr: Option<usize>,
135    vis_std: Option<usize>,
136    sed_atr: Option<usize>,
137    sed_std: Option<usize>,
138    threshold: Option<f64>,
139    kernel: Kernel,
140}
141
142impl Default for DamianiVolatmeterBuilder {
143    fn default() -> Self {
144        Self {
145            vis_atr: None,
146            vis_std: None,
147            sed_atr: None,
148            sed_std: None,
149            threshold: None,
150            kernel: Kernel::Auto,
151        }
152    }
153}
154
155impl DamianiVolatmeterBuilder {
156    #[inline(always)]
157    pub fn new() -> Self {
158        Self::default()
159    }
160    #[inline(always)]
161    pub fn vis_atr(mut self, n: usize) -> Self {
162        self.vis_atr = Some(n);
163        self
164    }
165    #[inline(always)]
166    pub fn vis_std(mut self, n: usize) -> Self {
167        self.vis_std = Some(n);
168        self
169    }
170    #[inline(always)]
171    pub fn sed_atr(mut self, n: usize) -> Self {
172        self.sed_atr = Some(n);
173        self
174    }
175    #[inline(always)]
176    pub fn sed_std(mut self, n: usize) -> Self {
177        self.sed_std = Some(n);
178        self
179    }
180    #[inline(always)]
181    pub fn threshold(mut self, x: f64) -> Self {
182        self.threshold = Some(x);
183        self
184    }
185    #[inline(always)]
186    pub fn kernel(mut self, k: Kernel) -> Self {
187        self.kernel = k;
188        self
189    }
190
191    #[inline(always)]
192    pub fn apply(self, c: &Candles) -> Result<DamianiVolatmeterOutput, DamianiVolatmeterError> {
193        self.apply_src(c, "close")
194    }
195
196    #[inline(always)]
197    pub fn apply_src(
198        self,
199        c: &Candles,
200        src: &str,
201    ) -> Result<DamianiVolatmeterOutput, DamianiVolatmeterError> {
202        let p = DamianiVolatmeterParams {
203            vis_atr: self.vis_atr,
204            vis_std: self.vis_std,
205            sed_atr: self.sed_atr,
206            sed_std: self.sed_std,
207            threshold: self.threshold,
208        };
209        let i = DamianiVolatmeterInput::from_candles(c, src, p);
210        damiani_volatmeter_with_kernel(&i, self.kernel)
211    }
212
213    #[inline(always)]
214    pub fn apply_slice(self, d: &[f64]) -> Result<DamianiVolatmeterOutput, DamianiVolatmeterError> {
215        let p = DamianiVolatmeterParams {
216            vis_atr: self.vis_atr,
217            vis_std: self.vis_std,
218            sed_atr: self.sed_atr,
219            sed_std: self.sed_std,
220            threshold: self.threshold,
221        };
222        let i = DamianiVolatmeterInput::from_slice(d, p);
223        damiani_volatmeter_with_kernel(&i, self.kernel)
224    }
225
226    #[inline(always)]
227    pub fn into_stream<'a>(
228        self,
229        candles: &'a Candles,
230        src: &'a str,
231    ) -> Result<DamianiVolatmeterStream<'a>, DamianiVolatmeterError> {
232        let p = DamianiVolatmeterParams {
233            vis_atr: self.vis_atr,
234            vis_std: self.vis_std,
235            sed_atr: self.sed_atr,
236            sed_std: self.sed_std,
237            threshold: self.threshold,
238        };
239        DamianiVolatmeterStream::new_from_candles(candles, src, p)
240    }
241}
242
243#[derive(Debug, Error)]
244#[non_exhaustive]
245pub enum DamianiVolatmeterError {
246    #[error("damiani_volatmeter: empty input data")]
247    EmptyInputData,
248    #[error("damiani_volatmeter: All values are NaN.")]
249    AllValuesNaN,
250    #[error("damiani_volatmeter: Invalid period: data length = {data_len}, vis_atr = {vis_atr}, vis_std = {vis_std}, sed_atr = {sed_atr}, sed_std = {sed_std}")]
251    InvalidPeriod {
252        data_len: usize,
253        vis_atr: usize,
254        vis_std: usize,
255        sed_atr: usize,
256        sed_std: usize,
257    },
258    #[error("damiani_volatmeter: Not enough valid data after first non-NaN index. needed = {needed}, valid = {valid}")]
259    NotEnoughValidData { needed: usize, valid: usize },
260    #[error("damiani_volatmeter: output length mismatch. expected={expected}, got={got}")]
261    OutputLengthMismatch { expected: usize, got: usize },
262    #[error("damiani_volatmeter: invalid range: start={start} end={end} step={step}")]
263    InvalidRange { start: i64, end: i64, step: i64 },
264    #[error("damiani_volatmeter: Empty data provided.")]
265    EmptyData,
266    #[error("damiani_volatmeter: Non-batch kernel '{kernel:?}' cannot be used with batch API. Use one of: Auto, Scalar, Avx2Batch, Avx512Batch.")]
267    NonBatchKernel { kernel: Kernel },
268    #[error("damiani_volatmeter: invalid kernel for batch: {0:?}")]
269    InvalidKernelForBatch(Kernel),
270}
271
272#[inline]
273pub fn damiani_volatmeter(
274    input: &DamianiVolatmeterInput,
275) -> Result<DamianiVolatmeterOutput, DamianiVolatmeterError> {
276    damiani_volatmeter_with_kernel(input, Kernel::Auto)
277}
278
279fn damiani_volatmeter_prepare<'a>(
280    input: &'a DamianiVolatmeterInput,
281    kernel: Kernel,
282) -> Result<
283    (
284        &'a [f64],
285        &'a [f64],
286        &'a [f64],
287        usize,
288        usize,
289        usize,
290        usize,
291        f64,
292        usize,
293        usize,
294        Kernel,
295    ),
296    DamianiVolatmeterError,
297> {
298    let (high, low, close): (&[f64], &[f64], &[f64]) = match &input.data {
299        DamianiVolatmeterData::Candles { candles, source } => {
300            let h = source_type(candles, "high");
301            let l = source_type(candles, "low");
302            let c = source_type(candles, source);
303            (h, l, c)
304        }
305        DamianiVolatmeterData::Slice(slice) => (slice, slice, slice),
306    };
307
308    let len = close.len();
309    if len == 0 {
310        return Err(DamianiVolatmeterError::EmptyData);
311    }
312
313    let vis_atr = input.get_vis_atr();
314    let vis_std = input.get_vis_std();
315    let sed_atr = input.get_sed_atr();
316    let sed_std = input.get_sed_std();
317    let threshold = input.get_threshold();
318
319    if vis_atr == 0
320        || vis_std == 0
321        || sed_atr == 0
322        || sed_std == 0
323        || vis_atr > len
324        || vis_std > len
325        || sed_atr > len
326        || sed_std > len
327    {
328        return Err(DamianiVolatmeterError::InvalidPeriod {
329            data_len: len,
330            vis_atr,
331            vis_std,
332            sed_atr,
333            sed_std,
334        });
335    }
336
337    let first = close
338        .iter()
339        .position(|&x| !x.is_nan())
340        .ok_or(DamianiVolatmeterError::AllValuesNaN)?;
341    let needed = *[vis_atr, vis_std, sed_atr, sed_std, 3]
342        .iter()
343        .max()
344        .unwrap();
345    if (len - first) < needed {
346        return Err(DamianiVolatmeterError::NotEnoughValidData {
347            needed,
348            valid: len - first,
349        });
350    }
351
352    let chosen = match kernel {
353        Kernel::Auto => Kernel::Scalar,
354        other => other,
355    };
356
357    Ok((
358        high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, needed, chosen,
359    ))
360}
361
362pub fn damiani_volatmeter_with_kernel(
363    input: &DamianiVolatmeterInput,
364    kernel: Kernel,
365) -> Result<DamianiVolatmeterOutput, DamianiVolatmeterError> {
366    let (high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, needed, chosen) =
367        damiani_volatmeter_prepare(input, kernel)?;
368
369    let len = close.len();
370    let warm_end = first + needed - 1;
371    let mut vol = alloc_with_nan_prefix(len, warm_end.min(len));
372    let mut anti = alloc_with_nan_prefix(len, warm_end.min(len));
373
374    unsafe {
375        match chosen {
376            Kernel::Scalar | Kernel::ScalarBatch => damiani_volatmeter_scalar(
377                high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, &mut vol,
378                &mut anti,
379            ),
380            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
381            Kernel::Avx2 | Kernel::Avx2Batch => damiani_volatmeter_avx2(
382                high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, &mut vol,
383                &mut anti,
384            ),
385            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
386            Kernel::Avx512 | Kernel::Avx512Batch => damiani_volatmeter_avx512(
387                high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, &mut vol,
388                &mut anti,
389            ),
390            _ => unreachable!(),
391        }
392    }
393
394    let cut = (warm_end + 1).min(len);
395    for x in &mut vol[..cut] {
396        *x = f64::NAN;
397    }
398    for x in &mut anti[..cut] {
399        *x = f64::NAN;
400    }
401
402    Ok(DamianiVolatmeterOutput { vol, anti })
403}
404
405#[inline]
406pub fn damiani_volatmeter_into_slice(
407    vol_dst: &mut [f64],
408    anti_dst: &mut [f64],
409    input: &DamianiVolatmeterInput,
410    kernel: Kernel,
411) -> Result<(), DamianiVolatmeterError> {
412    let (high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, needed, chosen) =
413        damiani_volatmeter_prepare(input, kernel)?;
414
415    let len = close.len();
416
417    if vol_dst.len() != len {
418        return Err(DamianiVolatmeterError::OutputLengthMismatch {
419            expected: len,
420            got: vol_dst.len(),
421        });
422    }
423    if anti_dst.len() != len {
424        return Err(DamianiVolatmeterError::OutputLengthMismatch {
425            expected: len,
426            got: anti_dst.len(),
427        });
428    }
429
430    unsafe {
431        match chosen {
432            Kernel::Scalar | Kernel::ScalarBatch => damiani_volatmeter_scalar(
433                high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol_dst,
434                anti_dst,
435            ),
436            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
437            Kernel::Avx2 | Kernel::Avx2Batch => damiani_volatmeter_avx2(
438                high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol_dst,
439                anti_dst,
440            ),
441            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
442            Kernel::Avx512 | Kernel::Avx512Batch => damiani_volatmeter_avx512(
443                high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol_dst,
444                anti_dst,
445            ),
446            _ => unreachable!(),
447        }
448    }
449
450    let warm_end = first + needed - 1;
451    let cut = (warm_end + 1).min(len);
452    for x in &mut vol_dst[..cut] {
453        *x = f64::NAN;
454    }
455    for x in &mut anti_dst[..cut] {
456        *x = f64::NAN;
457    }
458
459    Ok(())
460}
461
462#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
463#[inline]
464pub fn damiani_volatmeter_into(
465    input: &DamianiVolatmeterInput,
466    vol_out: &mut [f64],
467    anti_out: &mut [f64],
468) -> Result<(), DamianiVolatmeterError> {
469    damiani_volatmeter_into_slice(vol_out, anti_out, input, Kernel::Auto)
470}
471
472#[inline]
473pub unsafe fn damiani_volatmeter_scalar(
474    high: &[f64],
475    low: &[f64],
476    close: &[f64],
477    vis_atr: usize,
478    vis_std: usize,
479    sed_atr: usize,
480    sed_std: usize,
481    threshold: f64,
482    first: usize,
483    vol: &mut [f64],
484    anti: &mut [f64],
485) {
486    let len = close.len();
487    let mut atr_vis_val = f64::NAN;
488    let mut atr_sed_val = f64::NAN;
489    let mut sum_vis = 0.0;
490    let mut sum_sed = 0.0;
491
492    let vis_atr_f = vis_atr as f64;
493    let sed_atr_f = sed_atr as f64;
494    let needed_all = *[vis_atr, vis_std, sed_atr, sed_std, 3]
495        .iter()
496        .max()
497        .unwrap();
498
499    let mut prev_close = f64::NAN;
500    let mut have_prev = false;
501
502    let mut ring_vis = vec![0.0; vis_std];
503    let mut ring_sed = vec![0.0; sed_std];
504    let mut sum_vis_std = 0.0;
505    let mut sum_sq_vis_std = 0.0;
506    let mut sum_sed_std = 0.0;
507    let mut sum_sq_sed_std = 0.0;
508    let mut idx_vis = 0;
509    let mut idx_sed = 0;
510    let mut filled_vis = 0;
511    let mut filled_sed = 0;
512
513    let lag_s = 0.5_f64;
514
515    for i in first..len {
516        let tr = if have_prev && close[i].is_finite() {
517            let tr1 = high[i] - low[i];
518            let tr2 = (high[i] - prev_close).abs();
519            let tr3 = (low[i] - prev_close).abs();
520            tr1.max(tr2).max(tr3)
521        } else {
522            0.0
523        };
524
525        if close[i].is_finite() {
526            prev_close = close[i];
527            have_prev = true;
528        }
529
530        if i < vis_atr {
531            sum_vis += tr;
532            if i == vis_atr - 1 {
533                atr_vis_val = sum_vis / vis_atr_f;
534            }
535        } else if atr_vis_val.is_finite() {
536            atr_vis_val = ((vis_atr_f - 1.0) * atr_vis_val + tr) / vis_atr_f;
537        }
538
539        if i < sed_atr {
540            sum_sed += tr;
541            if i == sed_atr - 1 {
542                atr_sed_val = sum_sed / sed_atr_f;
543            }
544        } else if atr_sed_val.is_finite() {
545            atr_sed_val = ((sed_atr_f - 1.0) * atr_sed_val + tr) / sed_atr_f;
546        }
547
548        let val = if close[i].is_nan() { 0.0 } else { close[i] };
549
550        let old_v = ring_vis[idx_vis];
551        ring_vis[idx_vis] = val;
552        idx_vis = (idx_vis + 1) % vis_std;
553        if filled_vis < vis_std {
554            filled_vis += 1;
555            sum_vis_std += val;
556            sum_sq_vis_std += val * val;
557        } else {
558            sum_vis_std = sum_vis_std - old_v + val;
559            sum_sq_vis_std = sum_sq_vis_std - (old_v * old_v) + (val * val);
560        }
561
562        let old_s = ring_sed[idx_sed];
563        ring_sed[idx_sed] = val;
564        idx_sed = (idx_sed + 1) % sed_std;
565        if filled_sed < sed_std {
566            filled_sed += 1;
567            sum_sed_std += val;
568            sum_sq_sed_std += val * val;
569        } else {
570            sum_sed_std = sum_sed_std - old_s + val;
571            sum_sq_sed_std = sum_sq_sed_std - (old_s * old_s) + (val * val);
572        }
573
574        if i >= needed_all {
575            let p1 = if i >= 1 && !vol[i - 1].is_nan() {
576                vol[i - 1]
577            } else {
578                0.0
579            };
580            let p3 = if i >= 3 && !vol[i - 3].is_nan() {
581                vol[i - 3]
582            } else {
583                0.0
584            };
585
586            let sed_safe = if atr_sed_val.is_finite() && atr_sed_val != 0.0 {
587                atr_sed_val
588            } else {
589                atr_sed_val + f64::EPSILON
590            };
591
592            vol[i] = (atr_vis_val / sed_safe) + lag_s * (p1 - p3);
593
594            if filled_vis == vis_std && filled_sed == sed_std {
595                let mean_vis = sum_vis_std / (vis_std as f64);
596                let mean_sq_vis = sum_sq_vis_std / (vis_std as f64);
597                let var_vis = (mean_sq_vis - mean_vis * mean_vis).max(0.0);
598                let std_vis = var_vis.sqrt();
599
600                let mean_sed = sum_sed_std / (sed_std as f64);
601                let mean_sq_sed = sum_sq_sed_std / (sed_std as f64);
602                let var_sed = (mean_sq_sed - mean_sed * mean_sed).max(0.0);
603                let std_sed = var_sed.sqrt();
604
605                let ratio = if std_sed != 0.0 {
606                    std_vis / std_sed
607                } else {
608                    std_vis / (std_sed + f64::EPSILON)
609                };
610                anti[i] = threshold - ratio;
611            }
612        }
613    }
614}
615
616#[inline]
617fn stddev(sum: f64, sum_sq: f64, n: usize) -> f64 {
618    if n == 0 {
619        return 0.0;
620    }
621    let mean = sum / n as f64;
622    let mean_sq = sum_sq / n as f64;
623    let var = mean_sq - mean * mean;
624    if var <= 0.0 {
625        0.0
626    } else {
627        var.sqrt()
628    }
629}
630
631#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
632#[inline]
633pub unsafe fn damiani_volatmeter_avx512(
634    high: &[f64],
635    low: &[f64],
636    close: &[f64],
637    vis_atr: usize,
638    vis_std: usize,
639    sed_atr: usize,
640    sed_std: usize,
641    threshold: f64,
642    first: usize,
643    vol: &mut [f64],
644    anti: &mut [f64],
645) {
646    damiani_volatmeter_scalar(
647        high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol, anti,
648    )
649}
650
651#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
652#[inline]
653pub unsafe fn damiani_volatmeter_avx2(
654    high: &[f64],
655    low: &[f64],
656    close: &[f64],
657    vis_atr: usize,
658    vis_std: usize,
659    sed_atr: usize,
660    sed_std: usize,
661    threshold: f64,
662    first: usize,
663    vol: &mut [f64],
664    anti: &mut [f64],
665) {
666    damiani_volatmeter_scalar(
667        high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol, anti,
668    )
669}
670
671pub fn damiani_volatmeter_batch_with_kernel(
672    data: &[f64],
673    sweep: &DamianiVolatmeterBatchRange,
674    k: Kernel,
675) -> Result<DamianiVolatmeterBatchOutput, DamianiVolatmeterError> {
676    let kernel = match k {
677        Kernel::Auto => Kernel::ScalarBatch,
678        other if other.is_batch() => other,
679        other => return Err(DamianiVolatmeterError::InvalidKernelForBatch(other)),
680    };
681    let simd = match kernel {
682        Kernel::Avx512Batch => Kernel::Avx512,
683        Kernel::Avx2Batch => Kernel::Avx2,
684        Kernel::ScalarBatch => Kernel::Scalar,
685        _ => unreachable!(),
686    };
687    damiani_volatmeter_batch_par_slice(data, sweep, simd)
688}
689
690#[derive(Clone, Debug)]
691#[cfg_attr(
692    all(target_arch = "wasm32", feature = "wasm"),
693    derive(Serialize, Deserialize)
694)]
695pub struct DamianiVolatmeterBatchRange {
696    pub vis_atr: (usize, usize, usize),
697    pub vis_std: (usize, usize, usize),
698    pub sed_atr: (usize, usize, usize),
699    pub sed_std: (usize, usize, usize),
700    pub threshold: (f64, f64, f64),
701}
702impl Default for DamianiVolatmeterBatchRange {
703    fn default() -> Self {
704        Self {
705            vis_atr: (13, 262, 1),
706            vis_std: (20, 20, 0),
707            sed_atr: (40, 40, 0),
708            sed_std: (100, 100, 0),
709            threshold: (1.4, 1.4, 0.0),
710        }
711    }
712}
713#[derive(Clone, Debug, Default)]
714pub struct DamianiVolatmeterBatchBuilder {
715    range: DamianiVolatmeterBatchRange,
716    kernel: Kernel,
717}
718impl DamianiVolatmeterBatchBuilder {
719    pub fn new() -> Self {
720        Self::default()
721    }
722    pub fn kernel(mut self, k: Kernel) -> Self {
723        self.kernel = k;
724        self
725    }
726    pub fn vis_atr_range(mut self, s: usize, e: usize, step: usize) -> Self {
727        self.range.vis_atr = (s, e, step);
728        self
729    }
730    pub fn vis_std_range(mut self, s: usize, e: usize, step: usize) -> Self {
731        self.range.vis_std = (s, e, step);
732        self
733    }
734    pub fn sed_atr_range(mut self, s: usize, e: usize, step: usize) -> Self {
735        self.range.sed_atr = (s, e, step);
736        self
737    }
738    pub fn sed_std_range(mut self, s: usize, e: usize, step: usize) -> Self {
739        self.range.sed_std = (s, e, step);
740        self
741    }
742    pub fn threshold_range(mut self, s: f64, e: f64, step: f64) -> Self {
743        self.range.threshold = (s, e, step);
744        self
745    }
746
747    pub fn apply_slice(
748        self,
749        data: &[f64],
750    ) -> Result<DamianiVolatmeterBatchOutput, DamianiVolatmeterError> {
751        damiani_volatmeter_batch_with_kernel(data, &self.range, self.kernel)
752    }
753    pub fn apply_candles(
754        self,
755        c: &Candles,
756        src: &str,
757    ) -> Result<DamianiVolatmeterBatchOutput, DamianiVolatmeterError> {
758        let slice = source_type(c, src);
759        self.apply_slice(slice)
760    }
761}
762
763#[derive(Clone, Debug)]
764pub struct DamianiVolatmeterBatchOutput {
765    pub vol: Vec<f64>,
766    pub anti: Vec<f64>,
767    pub combos: Vec<DamianiVolatmeterParams>,
768    pub rows: usize,
769    pub cols: usize,
770}
771impl DamianiVolatmeterBatchOutput {
772    pub fn row_for_params(&self, p: &DamianiVolatmeterParams) -> Option<usize> {
773        self.combos.iter().position(|c| {
774            c.vis_atr == p.vis_atr
775                && c.vis_std == p.vis_std
776                && c.sed_atr == p.sed_atr
777                && c.sed_std == p.sed_std
778                && (c.threshold.unwrap_or(1.4) - p.threshold.unwrap_or(1.4)).abs() < 1e-12
779        })
780    }
781    pub fn vol_for(&self, p: &DamianiVolatmeterParams) -> Option<&[f64]> {
782        self.row_for_params(p).map(|row| {
783            let start = row * self.cols;
784            &self.vol[start..start + self.cols]
785        })
786    }
787    pub fn anti_for(&self, p: &DamianiVolatmeterParams) -> Option<&[f64]> {
788        self.row_for_params(p).map(|row| {
789            let start = row * self.cols;
790            &self.anti[start..start + self.cols]
791        })
792    }
793}
794#[inline(always)]
795pub fn damiani_volatmeter_batch_slice(
796    data: &[f64],
797    sweep: &DamianiVolatmeterBatchRange,
798    kern: Kernel,
799) -> Result<DamianiVolatmeterBatchOutput, DamianiVolatmeterError> {
800    damiani_volatmeter_batch_inner(data, sweep, kern, false)
801}
802#[inline(always)]
803pub fn damiani_volatmeter_batch_par_slice(
804    data: &[f64],
805    sweep: &DamianiVolatmeterBatchRange,
806    kern: Kernel,
807) -> Result<DamianiVolatmeterBatchOutput, DamianiVolatmeterError> {
808    damiani_volatmeter_batch_inner(data, sweep, kern, true)
809}
810fn expand_grid(
811    r: &DamianiVolatmeterBatchRange,
812) -> Result<Vec<DamianiVolatmeterParams>, DamianiVolatmeterError> {
813    fn axis_usize(
814        (s, e, step): (usize, usize, usize),
815    ) -> Result<Vec<usize>, DamianiVolatmeterError> {
816        if step == 0 || s == e {
817            return Ok(vec![s]);
818        }
819        let mut out = Vec::new();
820        if s < e {
821            if step == 0 {
822                return Ok(vec![s]);
823            }
824            let mut x = s;
825            while x <= e {
826                out.push(x);
827                match x.checked_add(step) {
828                    Some(nx) => x = nx,
829                    None => break,
830                }
831            }
832        } else {
833            let mut x = s as i64;
834            let step_i = step as i64;
835            while x >= e as i64 {
836                out.push(x as usize);
837                x -= step_i;
838            }
839        }
840        if out.is_empty() {
841            return Err(DamianiVolatmeterError::InvalidRange {
842                start: s as i64,
843                end: e as i64,
844                step: step as i64,
845            });
846        }
847        Ok(out)
848    }
849    fn axis_f64((s, e, step): (f64, f64, f64)) -> Result<Vec<f64>, DamianiVolatmeterError> {
850        if step == 0.0 || (s - e).abs() < 1e-12 {
851            return Ok(vec![s]);
852        }
853        let mut out = Vec::new();
854        let eps = 1e-12;
855        if s < e {
856            if step <= 0.0 {
857                return Err(DamianiVolatmeterError::InvalidRange {
858                    start: s as i64,
859                    end: e as i64,
860                    step: step as i64,
861                });
862            }
863            let mut x = s;
864            while x <= e + eps {
865                out.push(x);
866                x += step;
867            }
868        } else {
869            if step <= 0.0 {
870                return Err(DamianiVolatmeterError::InvalidRange {
871                    start: s as i64,
872                    end: e as i64,
873                    step: step as i64,
874                });
875            }
876            let mut x = s;
877            while x >= e - eps {
878                out.push(x);
879                x -= step;
880            }
881        }
882        if out.is_empty() {
883            return Err(DamianiVolatmeterError::InvalidRange {
884                start: s as i64,
885                end: e as i64,
886                step: step as i64,
887            });
888        }
889        Ok(out)
890    }
891
892    let vis_atrs = axis_usize(r.vis_atr)?;
893    let vis_stds = axis_usize(r.vis_std)?;
894    let sed_atrs = axis_usize(r.sed_atr)?;
895    let sed_stds = axis_usize(r.sed_std)?;
896    let thresholds = axis_f64(r.threshold)?;
897
898    let cap_mul = vis_atrs
899        .len()
900        .checked_mul(vis_stds.len())
901        .and_then(|v| v.checked_mul(sed_atrs.len()))
902        .and_then(|v| v.checked_mul(sed_stds.len()))
903        .and_then(|v| v.checked_mul(thresholds.len()))
904        .ok_or(DamianiVolatmeterError::InvalidRange {
905            start: 0,
906            end: 0,
907            step: 0,
908        })?;
909    let mut out = Vec::with_capacity(cap_mul);
910    for &va in &vis_atrs {
911        for &vs in &vis_stds {
912            for &sa in &sed_atrs {
913                for &ss in &sed_stds {
914                    for &th in &thresholds {
915                        out.push(DamianiVolatmeterParams {
916                            vis_atr: Some(va),
917                            vis_std: Some(vs),
918                            sed_atr: Some(sa),
919                            sed_std: Some(ss),
920                            threshold: Some(th),
921                        });
922                    }
923                }
924            }
925        }
926    }
927    if out.is_empty() {
928        return Err(DamianiVolatmeterError::InvalidRange {
929            start: 0,
930            end: 0,
931            step: 0,
932        });
933    }
934    Ok(out)
935}
936#[inline(always)]
937fn damiani_volatmeter_batch_inner(
938    data: &[f64],
939    sweep: &DamianiVolatmeterBatchRange,
940    kern: Kernel,
941    parallel: bool,
942) -> Result<DamianiVolatmeterBatchOutput, DamianiVolatmeterError> {
943    let combos = expand_grid(sweep)?;
944    if combos.is_empty() {
945        return Err(DamianiVolatmeterError::InvalidRange {
946            start: 0,
947            end: 0,
948            step: 0,
949        });
950    }
951    let first = data
952        .iter()
953        .position(|x| !x.is_nan())
954        .ok_or(DamianiVolatmeterError::AllValuesNaN)?;
955    let max_p = combos
956        .iter()
957        .map(|c| {
958            *[
959                c.vis_atr.unwrap(),
960                c.vis_std.unwrap(),
961                c.sed_atr.unwrap(),
962                c.sed_std.unwrap(),
963            ]
964            .iter()
965            .max()
966            .unwrap()
967        })
968        .max()
969        .unwrap();
970    if data.len() - first < max_p {
971        return Err(DamianiVolatmeterError::NotEnoughValidData {
972            needed: max_p,
973            valid: data.len() - first,
974        });
975    }
976    let rows = combos.len();
977    let cols = data.len();
978    let _total = rows
979        .checked_mul(cols)
980        .ok_or(DamianiVolatmeterError::InvalidRange {
981            start: rows as i64,
982            end: cols as i64,
983            step: 0,
984        })?;
985
986    let mut vol_mu = make_uninit_matrix(rows, cols);
987    let mut anti_mu = make_uninit_matrix(rows, cols);
988
989    let warm_per_row: Vec<usize> = combos
990        .iter()
991        .map(|p| {
992            let needed = *[
993                p.vis_atr.unwrap(),
994                p.vis_std.unwrap(),
995                p.sed_atr.unwrap(),
996                p.sed_std.unwrap(),
997                3,
998            ]
999            .iter()
1000            .max()
1001            .unwrap();
1002            first + needed - 1
1003        })
1004        .collect();
1005
1006    init_matrix_prefixes(&mut vol_mu, cols, &warm_per_row);
1007    init_matrix_prefixes(&mut anti_mu, cols, &warm_per_row);
1008
1009    let mut vol_guard = core::mem::ManuallyDrop::new(vol_mu);
1010    let mut anti_guard = core::mem::ManuallyDrop::new(anti_mu);
1011    let vol: &mut [f64] = unsafe {
1012        core::slice::from_raw_parts_mut(vol_guard.as_mut_ptr() as *mut f64, vol_guard.len())
1013    };
1014    let anti: &mut [f64] = unsafe {
1015        core::slice::from_raw_parts_mut(anti_guard.as_mut_ptr() as *mut f64, anti_guard.len())
1016    };
1017
1018    let do_row = |row: usize, out_vol: &mut [f64], out_anti: &mut [f64]| unsafe {
1019        let prm = &combos[row];
1020        let needed = *[
1021            prm.vis_atr.unwrap(),
1022            prm.vis_std.unwrap(),
1023            prm.sed_atr.unwrap(),
1024            prm.sed_std.unwrap(),
1025            3,
1026        ]
1027        .iter()
1028        .max()
1029        .unwrap();
1030        let warm_end = (first + needed - 1).min(cols);
1031
1032        match kern {
1033            Kernel::Scalar | Kernel::ScalarBatch | Kernel::Auto => damiani_volatmeter_row_scalar(
1034                data,
1035                first,
1036                prm.vis_atr.unwrap(),
1037                prm.vis_std.unwrap(),
1038                prm.sed_atr.unwrap(),
1039                prm.sed_std.unwrap(),
1040                prm.threshold.unwrap(),
1041                out_vol,
1042                out_anti,
1043            ),
1044            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1045            Kernel::Avx2 | Kernel::Avx2Batch => damiani_volatmeter_row_avx2(
1046                data,
1047                first,
1048                prm.vis_atr.unwrap(),
1049                prm.vis_std.unwrap(),
1050                prm.sed_atr.unwrap(),
1051                prm.sed_std.unwrap(),
1052                prm.threshold.unwrap(),
1053                out_vol,
1054                out_anti,
1055            ),
1056            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1057            Kernel::Avx512 | Kernel::Avx512Batch => damiani_volatmeter_row_avx512(
1058                data,
1059                first,
1060                prm.vis_atr.unwrap(),
1061                prm.vis_std.unwrap(),
1062                prm.sed_atr.unwrap(),
1063                prm.sed_std.unwrap(),
1064                prm.threshold.unwrap(),
1065                out_vol,
1066                out_anti,
1067            ),
1068            _ => damiani_volatmeter_row_scalar(
1069                data,
1070                first,
1071                prm.vis_atr.unwrap(),
1072                prm.vis_std.unwrap(),
1073                prm.sed_atr.unwrap(),
1074                prm.sed_std.unwrap(),
1075                prm.threshold.unwrap(),
1076                out_vol,
1077                out_anti,
1078            ),
1079        }
1080
1081        let cut = (warm_end + 1).min(cols);
1082        for x in &mut out_vol[..cut] {
1083            *x = f64::NAN;
1084        }
1085        for x in &mut out_anti[..cut] {
1086            *x = f64::NAN;
1087        }
1088    };
1089    if parallel {
1090        #[cfg(not(target_arch = "wasm32"))]
1091        {
1092            vol.par_chunks_mut(cols)
1093                .zip(anti.par_chunks_mut(cols))
1094                .enumerate()
1095                .for_each(|(row, (outv, outa))| do_row(row, outv, outa));
1096        }
1097
1098        #[cfg(target_arch = "wasm32")]
1099        {
1100            for (row, (outv, outa)) in vol.chunks_mut(cols).zip(anti.chunks_mut(cols)).enumerate() {
1101                do_row(row, outv, outa);
1102            }
1103        }
1104    } else {
1105        for (row, (outv, outa)) in vol.chunks_mut(cols).zip(anti.chunks_mut(cols)).enumerate() {
1106            do_row(row, outv, outa);
1107        }
1108    }
1109
1110    let vol = unsafe {
1111        Vec::from_raw_parts(
1112            vol_guard.as_mut_ptr() as *mut f64,
1113            vol_guard.len(),
1114            vol_guard.capacity(),
1115        )
1116    };
1117
1118    let anti = unsafe {
1119        Vec::from_raw_parts(
1120            anti_guard.as_mut_ptr() as *mut f64,
1121            anti_guard.len(),
1122            anti_guard.capacity(),
1123        )
1124    };
1125
1126    Ok(DamianiVolatmeterBatchOutput {
1127        vol,
1128        anti,
1129        combos,
1130        rows,
1131        cols,
1132    })
1133}
1134
1135#[inline(always)]
1136fn damiani_volatmeter_batch_inner_into(
1137    data: &[f64],
1138    sweep: &DamianiVolatmeterBatchRange,
1139    kern: Kernel,
1140    parallel: bool,
1141    vol_out: &mut [f64],
1142    anti_out: &mut [f64],
1143) -> Result<Vec<DamianiVolatmeterParams>, DamianiVolatmeterError> {
1144    let combos = expand_grid(sweep)?;
1145    if combos.is_empty() {
1146        return Err(DamianiVolatmeterError::InvalidRange {
1147            start: 0,
1148            end: 0,
1149            step: 0,
1150        });
1151    }
1152    let first = data
1153        .iter()
1154        .position(|x| !x.is_nan())
1155        .ok_or(DamianiVolatmeterError::AllValuesNaN)?;
1156    let max_p = combos
1157        .iter()
1158        .flat_map(|p| {
1159            [
1160                p.vis_atr.unwrap_or(13),
1161                p.vis_std.unwrap_or(20),
1162                p.sed_atr.unwrap_or(40),
1163                p.sed_std.unwrap_or(100),
1164                3,
1165            ]
1166        })
1167        .max()
1168        .unwrap_or(100);
1169    if (data.len() - first) < max_p {
1170        return Err(DamianiVolatmeterError::NotEnoughValidData {
1171            needed: max_p,
1172            valid: data.len() - first,
1173        });
1174    }
1175    let rows = combos.len();
1176    let cols = data.len();
1177    let total_size = rows
1178        .checked_mul(cols)
1179        .ok_or(DamianiVolatmeterError::InvalidRange {
1180            start: rows as i64,
1181            end: cols as i64,
1182            step: 0,
1183        })?;
1184
1185    if vol_out.len() != total_size {
1186        return Err(DamianiVolatmeterError::OutputLengthMismatch {
1187            expected: total_size,
1188            got: vol_out.len(),
1189        });
1190    }
1191    if anti_out.len() != total_size {
1192        return Err(DamianiVolatmeterError::OutputLengthMismatch {
1193            expected: total_size,
1194            got: anti_out.len(),
1195        });
1196    }
1197
1198    let do_row = |row: usize, out_vol: &mut [f64], out_anti: &mut [f64]| {
1199        let p = &combos[row];
1200
1201        let close = data;
1202        let high = data;
1203        let low = data;
1204
1205        let vis_atr = p.vis_atr.unwrap_or(1);
1206        let vis_std = p.vis_std.unwrap_or(20);
1207        let sed_atr = p.sed_atr.unwrap_or(13);
1208        let sed_std = p.sed_std.unwrap_or(40);
1209        let threshold = p.threshold.unwrap_or(1.4);
1210
1211        #[allow(unused_mut)]
1212        let mut used_scalar_fallback = false;
1213        #[allow(unused_variables)]
1214        {
1215            match kern {
1216                Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch => {}
1217                _ => used_scalar_fallback = true,
1218            }
1219        }
1220
1221        if used_scalar_fallback {
1222            unsafe {
1223                match kern {
1224                    Kernel::Scalar | Kernel::ScalarBatch | Kernel::Auto => {
1225                        damiani_volatmeter_scalar(
1226                            high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first,
1227                            out_vol, out_anti,
1228                        )
1229                    }
1230                    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1231                    Kernel::Avx2 | Kernel::Avx2Batch => damiani_volatmeter_avx2(
1232                        high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first,
1233                        out_vol, out_anti,
1234                    ),
1235                    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1236                    Kernel::Avx512 | Kernel::Avx512Batch => damiani_volatmeter_avx512(
1237                        high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first,
1238                        out_vol, out_anti,
1239                    ),
1240                    _ => damiani_volatmeter_scalar(
1241                        high, low, close, vis_atr, vis_std, sed_atr, sed_std, threshold, first,
1242                        out_vol, out_anti,
1243                    ),
1244                }
1245            }
1246        }
1247
1248        let needed = *[vis_atr, vis_std, sed_atr, sed_std, 3]
1249            .iter()
1250            .max()
1251            .unwrap();
1252        let warm_end = (first + needed - 1).min(cols);
1253        let cut = (warm_end + 1).min(cols);
1254        for x in &mut out_vol[..cut] {
1255            *x = f64::NAN;
1256        }
1257        for x in &mut out_anti[..cut] {
1258            *x = f64::NAN;
1259        }
1260    };
1261
1262    let use_row_optimized = false
1263        && matches!(
1264            kern,
1265            Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch
1266        );
1267
1268    if use_row_optimized {
1269        let len = data.len();
1270
1271        let mut tr: Vec<f64> = vec![0.0; len];
1272        let mut prev_close = f64::NAN;
1273        let mut have_prev = false;
1274        for i in 0..len {
1275            let cl = data[i];
1276            let t = if have_prev && cl.is_finite() {
1277                (cl - prev_close).abs()
1278            } else {
1279                0.0
1280            };
1281            tr[i] = t;
1282            if cl.is_finite() {
1283                prev_close = cl;
1284                have_prev = true;
1285            }
1286        }
1287
1288        let mut s: Vec<f64> = vec![0.0; len + 1];
1289        let mut ss: Vec<f64> = vec![0.0; len + 1];
1290        for i in 0..len {
1291            let x = if data[i].is_nan() { 0.0 } else { data[i] };
1292            s[i + 1] = s[i] + x;
1293            ss[i + 1] = ss[i] + x * x;
1294        }
1295
1296        let process_row = |row: usize, outv: &mut [f64], outa: &mut [f64]| {
1297            let p = &combos[row];
1298            let vis_atr = p.vis_atr.unwrap_or(13);
1299            let vis_std = p.vis_std.unwrap_or(20);
1300            let sed_atr = p.sed_atr.unwrap_or(40);
1301            let sed_std = p.sed_std.unwrap_or(100);
1302            let threshold = p.threshold.unwrap_or(1.4);
1303
1304            let len = data.len();
1305            let needed = *[vis_atr, vis_std, sed_atr, sed_std, 3]
1306                .iter()
1307                .max()
1308                .unwrap();
1309            let start_idx = first + needed - 1;
1310
1311            let vis_atr_f = vis_atr as f64;
1312            let sed_atr_f = sed_atr as f64;
1313
1314            let mut atr_vis = f64::NAN;
1315            let mut atr_sed = f64::NAN;
1316            let mut sum_vis_tr = 0.0;
1317            let mut sum_sed_tr = 0.0;
1318
1319            let mut vh1 = f64::NAN;
1320            let mut vh2 = f64::NAN;
1321            let mut vh3 = f64::NAN;
1322
1323            for i in first..len {
1324                let t = tr[i];
1325                if i < vis_atr {
1326                    sum_vis_tr += t;
1327                    if i == vis_atr - 1 {
1328                        atr_vis = sum_vis_tr / vis_atr_f;
1329                    }
1330                } else if atr_vis.is_finite() {
1331                    atr_vis = ((vis_atr_f - 1.0) * atr_vis + t) / vis_atr_f;
1332                }
1333
1334                if i < sed_atr {
1335                    sum_sed_tr += t;
1336                    if i == sed_atr - 1 {
1337                        atr_sed = sum_sed_tr / sed_atr_f;
1338                    }
1339                } else if atr_sed.is_finite() {
1340                    atr_sed = ((sed_atr_f - 1.0) * atr_sed + t) / sed_atr_f;
1341                }
1342
1343                if i >= start_idx {
1344                    let p1 = if vh1.is_nan() { 0.0 } else { vh1 };
1345                    let p3 = if vh3.is_nan() { 0.0 } else { vh3 };
1346                    let sed_safe = if atr_sed.is_finite() && atr_sed != 0.0 {
1347                        atr_sed
1348                    } else {
1349                        atr_sed + f64::EPSILON
1350                    };
1351                    let v_now = (atr_vis / sed_safe) + 0.5 * (p1 - p3);
1352                    outv[i] = v_now;
1353                    vh3 = vh2;
1354                    vh2 = vh1;
1355                    vh1 = v_now;
1356
1357                    let sumv = s[i + 1] - s[i + 1 - vis_std];
1358                    let sumv2 = ss[i + 1] - ss[i + 1 - vis_std];
1359                    let meanv = sumv / (vis_std as f64);
1360                    let varv = (sumv2 / (vis_std as f64) - meanv * meanv).max(0.0);
1361                    let stdv = varv.sqrt();
1362
1363                    let sums = s[i + 1] - s[i + 1 - sed_std];
1364                    let sums2 = ss[i + 1] - ss[i + 1 - sed_std];
1365                    let means = sums / (sed_std as f64);
1366                    let vars = (sums2 / (sed_std as f64) - means * means).max(0.0);
1367                    let stds = vars.sqrt();
1368
1369                    let den = if stds != 0.0 {
1370                        stds
1371                    } else {
1372                        stds + f64::EPSILON
1373                    };
1374                    outa[i] = threshold - (stdv / den);
1375                }
1376            }
1377
1378            let warm_end = (first + needed - 1).min(len);
1379            let cut = (warm_end + 1).min(len);
1380            for j in 0..cut {
1381                outv[j] = f64::NAN;
1382                outa[j] = f64::NAN;
1383            }
1384        };
1385
1386        if parallel {
1387            #[cfg(not(target_arch = "wasm32"))]
1388            {
1389                vol_out
1390                    .par_chunks_mut(cols)
1391                    .zip(anti_out.par_chunks_mut(cols))
1392                    .enumerate()
1393                    .for_each(|(row, (outv, outa))| process_row(row, outv, outa));
1394            }
1395
1396            #[cfg(target_arch = "wasm32")]
1397            {
1398                for (row, (outv, outa)) in vol_out
1399                    .chunks_mut(cols)
1400                    .zip(anti_out.chunks_mut(cols))
1401                    .enumerate()
1402                {
1403                    process_row(row, outv, outa);
1404                }
1405            }
1406        } else {
1407            for (row, (outv, outa)) in vol_out
1408                .chunks_mut(cols)
1409                .zip(anti_out.chunks_mut(cols))
1410                .enumerate()
1411            {
1412                process_row(row, outv, outa);
1413            }
1414        }
1415
1416        return Ok(combos);
1417    }
1418
1419    if parallel {
1420        #[cfg(not(target_arch = "wasm32"))]
1421        {
1422            vol_out
1423                .par_chunks_mut(cols)
1424                .zip(anti_out.par_chunks_mut(cols))
1425                .enumerate()
1426                .for_each(|(row, (outv, outa))| do_row(row, outv, outa));
1427        }
1428
1429        #[cfg(target_arch = "wasm32")]
1430        {
1431            for (row, (outv, outa)) in vol_out
1432                .chunks_mut(cols)
1433                .zip(anti_out.chunks_mut(cols))
1434                .enumerate()
1435            {
1436                do_row(row, outv, outa);
1437            }
1438        }
1439    } else {
1440        for (row, (outv, outa)) in vol_out
1441            .chunks_mut(cols)
1442            .zip(anti_out.chunks_mut(cols))
1443            .enumerate()
1444        {
1445            do_row(row, outv, outa);
1446        }
1447    }
1448
1449    Ok(combos)
1450}
1451
1452#[inline(always)]
1453pub unsafe fn damiani_volatmeter_row_scalar(
1454    data: &[f64],
1455    first: usize,
1456    vis_atr: usize,
1457    vis_std: usize,
1458    sed_atr: usize,
1459    sed_std: usize,
1460    threshold: f64,
1461    vol: &mut [f64],
1462    anti: &mut [f64],
1463) {
1464    damiani_volatmeter_scalar(
1465        data, data, data, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol, anti,
1466    )
1467}
1468#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1469#[inline(always)]
1470pub unsafe fn damiani_volatmeter_row_avx2(
1471    data: &[f64],
1472    first: usize,
1473    vis_atr: usize,
1474    vis_std: usize,
1475    sed_atr: usize,
1476    sed_std: usize,
1477    threshold: f64,
1478    vol: &mut [f64],
1479    anti: &mut [f64],
1480) {
1481    damiani_volatmeter_scalar(
1482        data, data, data, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol, anti,
1483    )
1484}
1485#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1486#[inline(always)]
1487pub unsafe fn damiani_volatmeter_row_avx512(
1488    data: &[f64],
1489    first: usize,
1490    vis_atr: usize,
1491    vis_std: usize,
1492    sed_atr: usize,
1493    sed_std: usize,
1494    threshold: f64,
1495    vol: &mut [f64],
1496    anti: &mut [f64],
1497) {
1498    damiani_volatmeter_scalar(
1499        data, data, data, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol, anti,
1500    )
1501}
1502#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1503#[inline(always)]
1504pub unsafe fn damiani_volatmeter_row_avx512_short(
1505    data: &[f64],
1506    first: usize,
1507    vis_atr: usize,
1508    vis_std: usize,
1509    sed_atr: usize,
1510    sed_std: usize,
1511    threshold: f64,
1512    vol: &mut [f64],
1513    anti: &mut [f64],
1514) {
1515    damiani_volatmeter_scalar(
1516        data, data, data, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol, anti,
1517    )
1518}
1519#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1520#[inline(always)]
1521pub unsafe fn damiani_volatmeter_row_avx512_long(
1522    data: &[f64],
1523    first: usize,
1524    vis_atr: usize,
1525    vis_std: usize,
1526    sed_atr: usize,
1527    sed_std: usize,
1528    threshold: f64,
1529    vol: &mut [f64],
1530    anti: &mut [f64],
1531) {
1532    damiani_volatmeter_scalar(
1533        data, data, data, vis_atr, vis_std, sed_atr, sed_std, threshold, first, vol, anti,
1534    )
1535}
1536
1537#[derive(Debug, Clone)]
1538pub struct DamianiVolatmeterStream<'a> {
1539    high: &'a [f64],
1540    low: &'a [f64],
1541    close: &'a [f64],
1542
1543    vis_atr: usize,
1544    vis_std: usize,
1545    sed_atr: usize,
1546    sed_std: usize,
1547    threshold: f64,
1548
1549    start: usize,
1550    index: usize,
1551    needed_all: usize,
1552
1553    atr_vis_val: f64,
1554    atr_sed_val: f64,
1555    sum_vis_seed: f64,
1556    sum_sed_seed: f64,
1557    vis_seed_cnt: usize,
1558    sed_seed_cnt: usize,
1559    prev_close: f64,
1560    have_prev: bool,
1561
1562    ring_vis: Vec<f64>,
1563    ring_sed: Vec<f64>,
1564    idx_vis: usize,
1565    idx_sed: usize,
1566    filled_vis: usize,
1567    filled_sed: usize,
1568    sum_vis_std: f64,
1569    sum_sq_vis_std: f64,
1570    sum_sed_std: f64,
1571    sum_sq_sed_std: f64,
1572
1573    vol_hist: [f64; 3],
1574    lag_s: f64,
1575
1576    inv_vis_atr: f64,
1577    inv_sed_atr: f64,
1578    inv_vis_std: f64,
1579    inv_sed_std: f64,
1580    vis_atr_m1: f64,
1581    sed_atr_m1: f64,
1582}
1583
1584impl<'a> DamianiVolatmeterStream<'a> {
1585    #[inline]
1586    pub fn new_from_candles(
1587        candles: &'a Candles,
1588        src: &str,
1589        params: DamianiVolatmeterParams,
1590    ) -> Result<Self, DamianiVolatmeterError> {
1591        Self::new_from_slices(
1592            source_type(candles, "high"),
1593            source_type(candles, "low"),
1594            source_type(candles, src),
1595            params,
1596        )
1597    }
1598
1599    #[inline]
1600    pub fn new_from_slices(
1601        high: &'a [f64],
1602        low: &'a [f64],
1603        close: &'a [f64],
1604        params: DamianiVolatmeterParams,
1605    ) -> Result<Self, DamianiVolatmeterError> {
1606        let len = close.len();
1607        if len == 0 {
1608            return Err(DamianiVolatmeterError::EmptyData);
1609        }
1610
1611        let vis_atr = params.vis_atr.unwrap_or(13);
1612        let vis_std = params.vis_std.unwrap_or(20);
1613        let sed_atr = params.sed_atr.unwrap_or(40);
1614        let sed_std = params.sed_std.unwrap_or(100);
1615        let threshold = params.threshold.unwrap_or(1.4);
1616
1617        if vis_atr == 0
1618            || vis_std == 0
1619            || sed_atr == 0
1620            || sed_std == 0
1621            || vis_atr > len
1622            || vis_std > len
1623            || sed_atr > len
1624            || sed_std > len
1625        {
1626            return Err(DamianiVolatmeterError::InvalidPeriod {
1627                data_len: len,
1628                vis_atr,
1629                vis_std,
1630                sed_atr,
1631                sed_std,
1632            });
1633        }
1634
1635        let start = close
1636            .iter()
1637            .position(|&x| !x.is_nan())
1638            .ok_or(DamianiVolatmeterError::AllValuesNaN)?;
1639        let needed_all = *[vis_atr, vis_std, sed_atr, sed_std, 3]
1640            .iter()
1641            .max()
1642            .unwrap();
1643        if len - start < needed_all {
1644            return Err(DamianiVolatmeterError::NotEnoughValidData {
1645                needed: needed_all,
1646                valid: len - start,
1647            });
1648        }
1649
1650        Ok(Self {
1651            high,
1652            low,
1653            close,
1654            vis_atr,
1655            vis_std,
1656            sed_atr,
1657            sed_std,
1658            threshold,
1659
1660            start,
1661            index: start,
1662            needed_all,
1663
1664            atr_vis_val: f64::NAN,
1665            atr_sed_val: f64::NAN,
1666            sum_vis_seed: 0.0,
1667            sum_sed_seed: 0.0,
1668            vis_seed_cnt: 0,
1669            sed_seed_cnt: 0,
1670            prev_close: f64::NAN,
1671            have_prev: false,
1672
1673            ring_vis: vec![0.0; vis_std],
1674            ring_sed: vec![0.0; sed_std],
1675            idx_vis: 0,
1676            idx_sed: 0,
1677            filled_vis: 0,
1678            filled_sed: 0,
1679            sum_vis_std: 0.0,
1680            sum_sq_vis_std: 0.0,
1681            sum_sed_std: 0.0,
1682            sum_sq_sed_std: 0.0,
1683
1684            vol_hist: [f64::NAN; 3],
1685            lag_s: 0.5,
1686
1687            inv_vis_atr: 1.0 / (vis_atr as f64),
1688            inv_sed_atr: 1.0 / (sed_atr as f64),
1689            inv_vis_std: 1.0 / (vis_std as f64),
1690            inv_sed_std: 1.0 / (sed_std as f64),
1691            vis_atr_m1: (vis_atr as f64) - 1.0,
1692            sed_atr_m1: (sed_atr as f64) - 1.0,
1693        })
1694    }
1695
1696    #[inline(always)]
1697    pub fn update(&mut self) -> Option<(f64, f64)> {
1698        let i = self.index;
1699        let len = self.close.len();
1700        if i >= len {
1701            return None;
1702        }
1703
1704        let cl = self.close[i];
1705        let hi = self.high[i];
1706        let lo = self.low[i];
1707
1708        let tr = if self.have_prev && cl.is_finite() {
1709            let tr1 = hi - lo;
1710            let tr2 = (hi - self.prev_close).abs();
1711            let tr3 = (lo - self.prev_close).abs();
1712            tr1.max(tr2).max(tr3)
1713        } else {
1714            0.0
1715        };
1716
1717        if cl.is_finite() {
1718            self.prev_close = cl;
1719            self.have_prev = true;
1720        }
1721
1722        if self.vis_seed_cnt < self.vis_atr {
1723            self.sum_vis_seed += tr;
1724            self.vis_seed_cnt += 1;
1725            if self.vis_seed_cnt == self.vis_atr {
1726                self.atr_vis_val = self.sum_vis_seed * self.inv_vis_atr;
1727            }
1728        } else {
1729            self.atr_vis_val = self.atr_vis_val.mul_add(self.vis_atr_m1, tr) * self.inv_vis_atr;
1730        }
1731
1732        if self.sed_seed_cnt < self.sed_atr {
1733            self.sum_sed_seed += tr;
1734            self.sed_seed_cnt += 1;
1735            if self.sed_seed_cnt == self.sed_atr {
1736                self.atr_sed_val = self.sum_sed_seed * self.inv_sed_atr;
1737            }
1738        } else {
1739            self.atr_sed_val = self.atr_sed_val.mul_add(self.sed_atr_m1, tr) * self.inv_sed_atr;
1740        }
1741
1742        let v = if cl.is_nan() { 0.0 } else { cl };
1743
1744        let old_v = self.ring_vis[self.idx_vis];
1745        self.ring_vis[self.idx_vis] = v;
1746        self.idx_vis += 1;
1747        if self.idx_vis == self.vis_std {
1748            self.idx_vis = 0;
1749        }
1750        if self.filled_vis < self.vis_std {
1751            self.filled_vis += 1;
1752            self.sum_vis_std += v;
1753            self.sum_sq_vis_std = v.mul_add(v, self.sum_sq_vis_std);
1754        } else {
1755            self.sum_vis_std += v - old_v;
1756            self.sum_sq_vis_std += v.mul_add(v, -old_v * old_v);
1757        }
1758
1759        let old_s = self.ring_sed[self.idx_sed];
1760        self.ring_sed[self.idx_sed] = v;
1761        self.idx_sed += 1;
1762        if self.idx_sed == self.sed_std {
1763            self.idx_sed = 0;
1764        }
1765        if self.filled_sed < self.sed_std {
1766            self.filled_sed += 1;
1767            self.sum_sed_std += v;
1768            self.sum_sq_sed_std = v.mul_add(v, self.sum_sq_sed_std);
1769        } else {
1770            self.sum_sed_std += v - old_s;
1771            self.sum_sq_sed_std += v.mul_add(v, -old_s * old_s);
1772        }
1773
1774        self.index = i + 1;
1775
1776        if i < self.needed_all {
1777            return None;
1778        }
1779
1780        let p1 = if self.vol_hist[0].is_nan() {
1781            0.0
1782        } else {
1783            self.vol_hist[0]
1784        };
1785        let p3 = if self.vol_hist[2].is_nan() {
1786            0.0
1787        } else {
1788            self.vol_hist[2]
1789        };
1790        let sed_safe = if self.atr_sed_val.is_finite() && self.atr_sed_val != 0.0 {
1791            self.atr_sed_val
1792        } else {
1793            f64::EPSILON
1794        };
1795        let vol_now = (self.atr_vis_val / sed_safe) + self.lag_s * (p1 - p3);
1796
1797        self.vol_hist[2] = self.vol_hist[1];
1798        self.vol_hist[1] = self.vol_hist[0];
1799        self.vol_hist[0] = vol_now;
1800
1801        let mean_v = self.sum_vis_std * self.inv_vis_std;
1802        let mean2_v = self.sum_sq_vis_std * self.inv_vis_std;
1803        let var_v = (mean2_v - mean_v * mean_v).max(0.0);
1804
1805        let mean_s = self.sum_sed_std * self.inv_sed_std;
1806        let mean2_s = self.sum_sq_sed_std * self.inv_sed_std;
1807        let var_s = (mean2_s - mean_s * mean_s).max(0.0);
1808
1809        let ratio = Self::sqrt_ratio(var_v, var_s);
1810        let anti_now = self.threshold - ratio;
1811
1812        Some((vol_now, anti_now))
1813    }
1814
1815    #[inline(always)]
1816    fn sqrt_ratio(num_var: f64, den_var: f64) -> f64 {
1817        (num_var / (if den_var > 0.0 { den_var } else { f64::EPSILON })).sqrt()
1818    }
1819}
1820
1821#[derive(Debug, Clone)]
1822pub struct DamianiVolatmeterFeedStream {
1823    vis_atr: usize,
1824    vis_std: usize,
1825    sed_atr: usize,
1826    sed_std: usize,
1827    threshold: f64,
1828
1829    atr_vis: f64,
1830    atr_sed: f64,
1831    sum_vis: f64,
1832    sum_sed: f64,
1833    have_vis_seed: usize,
1834    have_sed_seed: usize,
1835    prev_close: f64,
1836    have_prev: bool,
1837
1838    ring_vis: Vec<f64>,
1839    ring_sed: Vec<f64>,
1840    idx_vis: usize,
1841    idx_sed: usize,
1842    fill_vis: usize,
1843    fill_sed: usize,
1844    sum_vis_std: f64,
1845    sum_sq_vis_std: f64,
1846    sum_sed_std: f64,
1847    sum_sq_sed_std: f64,
1848
1849    vol_hist: [f64; 3],
1850}
1851
1852impl DamianiVolatmeterFeedStream {
1853    pub fn try_new(p: DamianiVolatmeterParams) -> Result<Self, DamianiVolatmeterError> {
1854        let vis_atr = p.vis_atr.unwrap_or(13);
1855        let vis_std = p.vis_std.unwrap_or(20);
1856        let sed_atr = p.sed_atr.unwrap_or(40);
1857        let sed_std = p.sed_std.unwrap_or(100);
1858        let threshold = p.threshold.unwrap_or(1.4);
1859        if vis_atr == 0 || vis_std == 0 || sed_atr == 0 || sed_std == 0 {
1860            return Err(DamianiVolatmeterError::InvalidPeriod {
1861                data_len: 0,
1862                vis_atr,
1863                vis_std,
1864                sed_atr,
1865                sed_std,
1866            });
1867        }
1868        Ok(Self {
1869            vis_atr,
1870            vis_std,
1871            sed_atr,
1872            sed_std,
1873            threshold,
1874            atr_vis: f64::NAN,
1875            atr_sed: f64::NAN,
1876            sum_vis: 0.0,
1877            sum_sed: 0.0,
1878            have_vis_seed: 0,
1879            have_sed_seed: 0,
1880            prev_close: f64::NAN,
1881            have_prev: false,
1882            ring_vis: vec![0.0; vis_std],
1883            ring_sed: vec![0.0; sed_std],
1884            idx_vis: 0,
1885            idx_sed: 0,
1886            fill_vis: 0,
1887            fill_sed: 0,
1888            sum_vis_std: 0.0,
1889            sum_sq_vis_std: 0.0,
1890            sum_sed_std: 0.0,
1891            sum_sq_sed_std: 0.0,
1892            vol_hist: [f64::NAN; 3],
1893        })
1894    }
1895
1896    #[inline]
1897    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
1898        let tr = if self.have_prev {
1899            let tr1 = high - low;
1900            let tr2 = (high - self.prev_close).abs();
1901            let tr3 = (low - self.prev_close).abs();
1902            tr1.max(tr2).max(tr3)
1903        } else {
1904            0.0
1905        };
1906        self.prev_close = close;
1907        self.have_prev = true;
1908
1909        if self.have_vis_seed < self.vis_atr {
1910            self.sum_vis += tr;
1911            self.have_vis_seed += 1;
1912            if self.have_vis_seed == self.vis_atr {
1913                self.atr_vis = self.sum_vis / self.vis_atr as f64;
1914            }
1915        } else if self.atr_vis.is_finite() {
1916            self.atr_vis = ((self.vis_atr as f64 - 1.0) * self.atr_vis + tr) / self.vis_atr as f64;
1917        }
1918
1919        if self.have_sed_seed < self.sed_atr {
1920            self.sum_sed += tr;
1921            self.have_sed_seed += 1;
1922            if self.have_sed_seed == self.sed_atr {
1923                self.atr_sed = self.sum_sed / self.sed_atr as f64;
1924            }
1925        } else if self.atr_sed.is_finite() {
1926            self.atr_sed = ((self.sed_atr as f64 - 1.0) * self.atr_sed + tr) / self.sed_atr as f64;
1927        }
1928
1929        let v = if close.is_nan() { 0.0 } else { close };
1930
1931        let old_v = self.ring_vis[self.idx_vis];
1932        self.ring_vis[self.idx_vis] = v;
1933        self.idx_vis = (self.idx_vis + 1) % self.vis_std;
1934        if self.fill_vis < self.vis_std {
1935            self.fill_vis += 1;
1936            self.sum_vis_std += v;
1937            self.sum_sq_vis_std += v * v;
1938        } else {
1939            self.sum_vis_std -= old_v;
1940            self.sum_vis_std += v;
1941            self.sum_sq_vis_std -= old_v * old_v;
1942            self.sum_sq_vis_std += v * v;
1943        }
1944
1945        let old_s = self.ring_sed[self.idx_sed];
1946        self.ring_sed[self.idx_sed] = v;
1947        self.idx_sed = (self.idx_sed + 1) % self.sed_std;
1948        if self.fill_sed < self.sed_std {
1949            self.fill_sed += 1;
1950            self.sum_sed_std += v;
1951            self.sum_sq_sed_std += v * v;
1952        } else {
1953            self.sum_sed_std -= old_s;
1954            self.sum_sed_std += v;
1955            self.sum_sq_sed_std -= old_s * old_s;
1956            self.sum_sq_sed_std += v * v;
1957        }
1958
1959        let needed = self
1960            .vis_atr
1961            .max(self.vis_std)
1962            .max(self.sed_atr)
1963            .max(self.sed_std)
1964            .max(3);
1965
1966        if self.have_vis_seed < self.vis_atr || self.have_sed_seed < self.sed_atr {
1967            return None;
1968        }
1969        if self.fill_vis < self.vis_std || self.fill_sed < self.sed_std {
1970            return None;
1971        }
1972
1973        if self.vol_hist.iter().any(|x| x.is_nan()) {}
1974
1975        let lag_s = 0.5;
1976        let p1 = if self.vol_hist[0].is_nan() {
1977            0.0
1978        } else {
1979            self.vol_hist[0]
1980        };
1981        let p3 = if self.vol_hist[2].is_nan() {
1982            0.0
1983        } else {
1984            self.vol_hist[2]
1985        };
1986        let sed_safe = if self.atr_sed.is_finite() && self.atr_sed != 0.0 {
1987            self.atr_sed
1988        } else {
1989            self.atr_sed + f64::EPSILON
1990        };
1991        let vol = (self.atr_vis / sed_safe) + lag_s * (p1 - p3);
1992
1993        self.vol_hist[2] = self.vol_hist[1];
1994        self.vol_hist[1] = self.vol_hist[0];
1995        self.vol_hist[0] = vol;
1996
1997        let mean_vis = self.sum_vis_std / self.vis_std as f64;
1998        let mean_sq_vis = self.sum_sq_vis_std / self.vis_std as f64;
1999        let std_vis = (mean_sq_vis - mean_vis * mean_vis).max(0.0).sqrt();
2000
2001        let mean_sed = self.sum_sed_std / self.sed_std as f64;
2002        let mean_sq_sed = self.sum_sq_sed_std / self.sed_std as f64;
2003        let std_sed = (mean_sq_sed - mean_sed * mean_sed).max(0.0).sqrt();
2004
2005        let ratio = if std_sed != 0.0 {
2006            std_vis / std_sed
2007        } else {
2008            std_vis / (std_sed + f64::EPSILON)
2009        };
2010        let anti = self.threshold - ratio;
2011
2012        Some((vol, anti))
2013    }
2014}
2015
2016#[cfg(test)]
2017mod tests {
2018    use super::*;
2019    use crate::skip_if_unsupported;
2020    use crate::utilities::data_loader::read_candles_from_csv;
2021    #[cfg(feature = "proptest")]
2022    use proptest::prelude::*;
2023    fn check_damiani_partial_params(
2024        test_name: &str,
2025        kernel: Kernel,
2026    ) -> Result<(), Box<dyn std::error::Error>> {
2027        skip_if_unsupported!(kernel, test_name);
2028        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2029        let candles = read_candles_from_csv(file_path)?;
2030        let params = DamianiVolatmeterParams::default();
2031        let input = DamianiVolatmeterInput::from_candles(&candles, "close", params);
2032        let output = damiani_volatmeter_with_kernel(&input, kernel)?;
2033        assert_eq!(output.vol.len(), candles.close.len());
2034        assert_eq!(output.anti.len(), candles.close.len());
2035        Ok(())
2036    }
2037
2038    #[test]
2039    fn test_damiani_volatmeter_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
2040        let len = 256usize;
2041        let mut ts = Vec::with_capacity(len);
2042        let mut open = Vec::with_capacity(len);
2043        let mut high = Vec::with_capacity(len);
2044        let mut low = Vec::with_capacity(len);
2045        let mut close = Vec::with_capacity(len);
2046        let mut vol = Vec::with_capacity(len);
2047
2048        for i in 0..len {
2049            let i_f = i as f64;
2050            let o = 100.0 + 0.1 * i_f + (i_f * 0.05).sin() * 0.5;
2051            let c = o + (i_f * 0.3).cos() * 0.2;
2052            let mut h = o + 1.0 + (i % 7) as f64 * 0.01;
2053            let mut l = o - 1.0 - (i % 5) as f64 * 0.01;
2054            if h < o {
2055                h = o;
2056            }
2057            if h < c {
2058                h = c;
2059            }
2060            if l > o {
2061                l = o;
2062            }
2063            if l > c {
2064                l = c;
2065            }
2066
2067            ts.push(i as i64);
2068            open.push(o);
2069            high.push(h);
2070            low.push(l);
2071            close.push(c);
2072            vol.push(1000.0 + (i % 10) as f64);
2073        }
2074
2075        let candles = Candles::new(ts, open, high, low, close.clone(), vol);
2076        let input = DamianiVolatmeterInput::from_candles(
2077            &candles,
2078            "close",
2079            DamianiVolatmeterParams::default(),
2080        );
2081
2082        let base = damiani_volatmeter(&input)?;
2083
2084        let mut out_vol = vec![0.0; len];
2085        let mut out_anti = vec![0.0; len];
2086        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2087        {
2088            damiani_volatmeter_into(&input, &mut out_vol, &mut out_anti)?;
2089        }
2090        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2091        {
2092            damiani_volatmeter_into_slice(&mut out_vol, &mut out_anti, &input, Kernel::Auto)?;
2093        }
2094
2095        assert_eq!(out_vol.len(), base.vol.len());
2096        assert_eq!(out_anti.len(), base.anti.len());
2097
2098        fn eq_or_nan(a: f64, b: f64) -> bool {
2099            (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
2100        }
2101
2102        for i in 0..len {
2103            assert!(
2104                eq_or_nan(out_vol[i], base.vol[i]),
2105                "vol mismatch at {}: into={}, api={}",
2106                i,
2107                out_vol[i],
2108                base.vol[i]
2109            );
2110            assert!(
2111                eq_or_nan(out_anti[i], base.anti[i]),
2112                "anti mismatch at {}: into={}, api={}",
2113                i,
2114                out_anti[i],
2115                base.anti[i]
2116            );
2117        }
2118
2119        Ok(())
2120    }
2121    fn check_damiani_accuracy(
2122        test_name: &str,
2123        kernel: Kernel,
2124    ) -> Result<(), Box<dyn std::error::Error>> {
2125        skip_if_unsupported!(kernel, test_name);
2126        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2127        let candles = read_candles_from_csv(file_path)?;
2128        let input = DamianiVolatmeterInput::from_candles(
2129            &candles,
2130            "close",
2131            DamianiVolatmeterParams::default(),
2132        );
2133        let output = damiani_volatmeter_with_kernel(&input, kernel)?;
2134        let n = output.vol.len();
2135        let expected_vol = [
2136            0.9009485470514558,
2137            0.8333604467044887,
2138            0.815318380178986,
2139            0.8276892636184923,
2140            0.879447954127426,
2141        ];
2142        let expected_anti = [
2143            1.1227721577887388,
2144            1.1250333024152703,
2145            1.1325501989919875,
2146            1.1403866079746106,
2147            1.1392919184055932,
2148        ];
2149        let start = n - 5;
2150        for i in 0..5 {
2151            let diff_vol = (output.vol[start + i] - expected_vol[i]).abs();
2152            let diff_anti = (output.anti[start + i] - expected_anti[i]).abs();
2153            assert!(
2154                diff_vol < 1e-2,
2155                "vol mismatch at index {}: expected {}, got {}",
2156                start + i,
2157                expected_vol[i],
2158                output.vol[start + i]
2159            );
2160            assert!(
2161                diff_anti < 1e-2,
2162                "anti mismatch at index {}: expected {}, got {}",
2163                start + i,
2164                expected_anti[i],
2165                output.anti[start + i]
2166            );
2167        }
2168        Ok(())
2169    }
2170    fn check_damiani_zero_period(
2171        test_name: &str,
2172        kernel: Kernel,
2173    ) -> Result<(), Box<dyn std::error::Error>> {
2174        skip_if_unsupported!(kernel, test_name);
2175        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2176        let candles = read_candles_from_csv(file_path)?;
2177        let mut params = DamianiVolatmeterParams::default();
2178        params.vis_atr = Some(0);
2179        let input = DamianiVolatmeterInput::from_candles(&candles, "close", params);
2180        let res = damiani_volatmeter_with_kernel(&input, kernel);
2181        assert!(res.is_err(), "[{}] should fail with zero period", test_name);
2182        Ok(())
2183    }
2184    fn check_damiani_period_exceeds_length(
2185        test_name: &str,
2186        kernel: Kernel,
2187    ) -> Result<(), Box<dyn std::error::Error>> {
2188        skip_if_unsupported!(kernel, test_name);
2189        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2190        let candles = read_candles_from_csv(file_path)?;
2191        let mut params = DamianiVolatmeterParams::default();
2192        params.vis_atr = Some(99999);
2193        let input = DamianiVolatmeterInput::from_candles(&candles, "close", params);
2194        let res = damiani_volatmeter_with_kernel(&input, kernel);
2195        assert!(
2196            res.is_err(),
2197            "[{}] should fail if period exceeds length",
2198            test_name
2199        );
2200        Ok(())
2201    }
2202    fn check_damiani_very_small_dataset(
2203        test_name: &str,
2204        kernel: Kernel,
2205    ) -> Result<(), Box<dyn std::error::Error>> {
2206        skip_if_unsupported!(kernel, test_name);
2207        let data = [42.0];
2208        let params = DamianiVolatmeterParams {
2209            vis_atr: Some(9),
2210            vis_std: Some(9),
2211            sed_atr: Some(9),
2212            sed_std: Some(9),
2213            threshold: Some(1.4),
2214        };
2215        let input = DamianiVolatmeterInput::from_slice(&data, params);
2216        let res = damiani_volatmeter_with_kernel(&input, kernel);
2217        assert!(
2218            res.is_err(),
2219            "[{}] should fail with insufficient data",
2220            test_name
2221        );
2222        Ok(())
2223    }
2224    fn check_damiani_streaming(
2225        test_name: &str,
2226        kernel: Kernel,
2227    ) -> Result<(), Box<dyn std::error::Error>> {
2228        skip_if_unsupported!(kernel, test_name);
2229
2230        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2231        let candles = read_candles_from_csv(file_path)?;
2232        let input = DamianiVolatmeterInput::from_candles(
2233            &candles,
2234            "close",
2235            DamianiVolatmeterParams::default(),
2236        );
2237        let batch = damiani_volatmeter_with_kernel(&input, kernel)?;
2238
2239        let mut stream = DamianiVolatmeterStream::new_from_candles(
2240            &candles,
2241            "close",
2242            DamianiVolatmeterParams::default(),
2243        )?;
2244
2245        let mut stream_vol = Vec::with_capacity(candles.close.len());
2246        let mut stream_anti = Vec::with_capacity(candles.close.len());
2247
2248        for _ in 0..candles.close.len() {
2249            if let Some((v, a)) = stream.update() {
2250                stream_vol.push(v);
2251                stream_anti.push(a);
2252            } else {
2253                stream_vol.push(f64::NAN);
2254                stream_anti.push(f64::NAN);
2255            }
2256        }
2257
2258        for (i, (&bv, &sv)) in batch.vol.iter().zip(stream_vol.iter()).enumerate() {
2259            if bv.is_nan() && sv.is_nan() {
2260                continue;
2261            }
2262            let diff = (bv - sv).abs();
2263            assert!(
2264                diff < 1e-8,
2265                "[{}] streaming vol mismatch at idx {}: batch={}, stream={}",
2266                test_name,
2267                i,
2268                bv,
2269                sv
2270            );
2271        }
2272
2273        for (i, (&ba, &sa)) in batch.anti.iter().zip(stream_anti.iter()).enumerate() {
2274            if ba.is_nan() && sa.is_nan() {
2275                continue;
2276            }
2277            let diff = (ba - sa).abs();
2278            assert!(
2279                diff < 1e-8,
2280                "[{}] streaming anti mismatch at idx {}: batch={}, stream={}",
2281                test_name,
2282                i,
2283                ba,
2284                sa
2285            );
2286        }
2287
2288        Ok(())
2289    }
2290
2291    fn check_damiani_input_with_default_candles(
2292        _test_name: &str,
2293        _kernel: Kernel,
2294    ) -> Result<(), Box<dyn std::error::Error>> {
2295        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2296        let candles = read_candles_from_csv(file_path)?;
2297        let input = DamianiVolatmeterInput::with_default_candles(&candles);
2298        match input.data {
2299            DamianiVolatmeterData::Candles { source, .. } => {
2300                assert_eq!(source, "close");
2301            }
2302            _ => panic!("Expected DamianiVolatmeterData::Candles"),
2303        }
2304        Ok(())
2305    }
2306    fn check_damiani_params_with_defaults(
2307        _test_name: &str,
2308        _kernel: Kernel,
2309    ) -> Result<(), Box<dyn std::error::Error>> {
2310        let default_params = DamianiVolatmeterParams::default();
2311        assert_eq!(default_params.vis_atr, Some(13));
2312        assert_eq!(default_params.vis_std, Some(20));
2313        assert_eq!(default_params.sed_atr, Some(40));
2314        assert_eq!(default_params.sed_std, Some(100));
2315        assert_eq!(default_params.threshold, Some(1.4));
2316        Ok(())
2317    }
2318
2319    #[cfg(debug_assertions)]
2320    fn check_damiani_no_poison(
2321        test_name: &str,
2322        kernel: Kernel,
2323    ) -> Result<(), Box<dyn std::error::Error>> {
2324        skip_if_unsupported!(kernel, test_name);
2325
2326        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2327        let candles = read_candles_from_csv(file_path)?;
2328
2329        let test_params = vec![
2330            DamianiVolatmeterParams::default(),
2331            DamianiVolatmeterParams {
2332                vis_atr: Some(2),
2333                vis_std: Some(2),
2334                sed_atr: Some(2),
2335                sed_std: Some(2),
2336                threshold: Some(1.4),
2337            },
2338            DamianiVolatmeterParams {
2339                vis_atr: Some(5),
2340                vis_std: Some(10),
2341                sed_atr: Some(10),
2342                sed_std: Some(20),
2343                threshold: Some(0.5),
2344            },
2345            DamianiVolatmeterParams {
2346                vis_atr: Some(13),
2347                vis_std: Some(20),
2348                sed_atr: Some(40),
2349                sed_std: Some(100),
2350                threshold: Some(1.0),
2351            },
2352            DamianiVolatmeterParams {
2353                vis_atr: Some(20),
2354                vis_std: Some(30),
2355                sed_atr: Some(50),
2356                sed_std: Some(120),
2357                threshold: Some(2.0),
2358            },
2359            DamianiVolatmeterParams {
2360                vis_atr: Some(50),
2361                vis_std: Some(80),
2362                sed_atr: Some(100),
2363                sed_std: Some(200),
2364                threshold: Some(1.4),
2365            },
2366            DamianiVolatmeterParams {
2367                vis_atr: Some(15),
2368                vis_std: Some(15),
2369                sed_atr: Some(15),
2370                sed_std: Some(15),
2371                threshold: Some(1.5),
2372            },
2373            DamianiVolatmeterParams {
2374                vis_atr: Some(10),
2375                vis_std: Some(25),
2376                sed_atr: Some(30),
2377                sed_std: Some(75),
2378                threshold: Some(3.0),
2379            },
2380            DamianiVolatmeterParams {
2381                vis_atr: Some(3),
2382                vis_std: Some(50),
2383                sed_atr: Some(5),
2384                sed_std: Some(150),
2385                threshold: Some(1.2),
2386            },
2387            DamianiVolatmeterParams {
2388                vis_atr: Some(25),
2389                vis_std: Some(25),
2390                sed_atr: Some(80),
2391                sed_std: Some(80),
2392                threshold: Some(0.8),
2393            },
2394        ];
2395
2396        for (param_idx, params) in test_params.iter().enumerate() {
2397            let input = DamianiVolatmeterInput::from_candles(&candles, "close", params.clone());
2398            let output = damiani_volatmeter_with_kernel(&input, kernel)?;
2399
2400            for (i, &val) in output.vol.iter().enumerate() {
2401                if val.is_nan() {
2402                    continue;
2403                }
2404
2405                let bits = val.to_bits();
2406
2407                if bits == 0x11111111_11111111 {
2408                    panic!(
2409						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} in vol array \
2410						 with params: vis_atr={}, vis_std={}, sed_atr={}, sed_std={}, threshold={} (param set {})",
2411						test_name, val, bits, i,
2412						params.vis_atr.unwrap(), params.vis_std.unwrap(),
2413						params.sed_atr.unwrap(), params.sed_std.unwrap(),
2414						params.threshold.unwrap(), param_idx
2415					);
2416                }
2417
2418                if bits == 0x22222222_22222222 {
2419                    panic!(
2420						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} in vol array \
2421						 with params: vis_atr={}, vis_std={}, sed_atr={}, sed_std={}, threshold={} (param set {})",
2422						test_name, val, bits, i,
2423						params.vis_atr.unwrap(), params.vis_std.unwrap(),
2424						params.sed_atr.unwrap(), params.sed_std.unwrap(),
2425						params.threshold.unwrap(), param_idx
2426					);
2427                }
2428
2429                if bits == 0x33333333_33333333 {
2430                    panic!(
2431						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} in vol array \
2432						 with params: vis_atr={}, vis_std={}, sed_atr={}, sed_std={}, threshold={} (param set {})",
2433						test_name, val, bits, i,
2434						params.vis_atr.unwrap(), params.vis_std.unwrap(),
2435						params.sed_atr.unwrap(), params.sed_std.unwrap(),
2436						params.threshold.unwrap(), param_idx
2437					);
2438                }
2439            }
2440
2441            for (i, &val) in output.anti.iter().enumerate() {
2442                if val.is_nan() {
2443                    continue;
2444                }
2445
2446                let bits = val.to_bits();
2447
2448                if bits == 0x11111111_11111111 {
2449                    panic!(
2450						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} in anti array \
2451						 with params: vis_atr={}, vis_std={}, sed_atr={}, sed_std={}, threshold={} (param set {})",
2452						test_name, val, bits, i,
2453						params.vis_atr.unwrap(), params.vis_std.unwrap(),
2454						params.sed_atr.unwrap(), params.sed_std.unwrap(),
2455						params.threshold.unwrap(), param_idx
2456					);
2457                }
2458
2459                if bits == 0x22222222_22222222 {
2460                    panic!(
2461						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} in anti array \
2462						 with params: vis_atr={}, vis_std={}, sed_atr={}, sed_std={}, threshold={} (param set {})",
2463						test_name, val, bits, i,
2464						params.vis_atr.unwrap(), params.vis_std.unwrap(),
2465						params.sed_atr.unwrap(), params.sed_std.unwrap(),
2466						params.threshold.unwrap(), param_idx
2467					);
2468                }
2469
2470                if bits == 0x33333333_33333333 {
2471                    panic!(
2472						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} in anti array \
2473						 with params: vis_atr={}, vis_std={}, sed_atr={}, sed_std={}, threshold={} (param set {})",
2474						test_name, val, bits, i,
2475						params.vis_atr.unwrap(), params.vis_std.unwrap(),
2476						params.sed_atr.unwrap(), params.sed_std.unwrap(),
2477						params.threshold.unwrap(), param_idx
2478					);
2479                }
2480            }
2481        }
2482
2483        Ok(())
2484    }
2485
2486    #[cfg(not(debug_assertions))]
2487    fn check_damiani_no_poison(
2488        _test_name: &str,
2489        _kernel: Kernel,
2490    ) -> Result<(), Box<dyn std::error::Error>> {
2491        Ok(())
2492    }
2493    fn check_batch_default_row(
2494        test_name: &str,
2495        kernel: Kernel,
2496    ) -> Result<(), Box<dyn std::error::Error>> {
2497        skip_if_unsupported!(kernel, test_name);
2498        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2499        let c = read_candles_from_csv(file)?;
2500        let output = DamianiVolatmeterBatchBuilder::new()
2501            .kernel(kernel)
2502            .apply_candles(&c, "close")?;
2503        let def = DamianiVolatmeterParams::default();
2504        let vol_row = output.vol_for(&def).expect("default vol row missing");
2505        let anti_row = output.anti_for(&def).expect("default anti row missing");
2506        assert_eq!(vol_row.len(), c.close.len());
2507        assert_eq!(anti_row.len(), c.close.len());
2508
2509        let close_slice = source_type(&c, "close");
2510        let input = DamianiVolatmeterInput::from_slice(close_slice, def.clone());
2511        let expected_output = damiani_volatmeter(&input)?;
2512
2513        let start = vol_row.len() - 5;
2514        for i in 0..5 {
2515            let idx = start + i;
2516            assert!(
2517                (vol_row[idx] - expected_output.vol[idx]).abs() < 1e-10,
2518                "[{test_name}] default-vol-row mismatch at idx {i}: batch={} vs expected={}",
2519                vol_row[idx],
2520                expected_output.vol[idx]
2521            );
2522            assert!(
2523                (anti_row[idx] - expected_output.anti[idx]).abs() < 1e-10,
2524                "[{test_name}] default-anti-row mismatch at idx {i}: batch={} vs expected={}",
2525                anti_row[idx],
2526                expected_output.anti[idx]
2527            );
2528        }
2529        Ok(())
2530    }
2531
2532    #[cfg(debug_assertions)]
2533    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
2534        skip_if_unsupported!(kernel, test);
2535
2536        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2537        let c = read_candles_from_csv(file)?;
2538
2539        let test_configs = vec![
2540            (2, 10, 2, 2, 10, 2, 5, 15, 5, 10, 30, 10, 0.5, 2.0, 0.5),
2541            (
2542                10, 20, 5, 15, 35, 10, 30, 60, 15, 80, 120, 20, 1.0, 1.5, 0.25,
2543            ),
2544            (
2545                30, 60, 15, 50, 100, 25, 60, 120, 30, 150, 250, 50, 1.2, 1.8, 0.3,
2546            ),
2547            (5, 8, 1, 10, 15, 1, 15, 20, 1, 40, 50, 2, 1.4, 1.4, 0.0),
2548            (10, 10, 0, 10, 10, 0, 10, 10, 0, 10, 10, 0, 1.0, 3.0, 1.0),
2549            (2, 5, 1, 20, 80, 20, 3, 8, 1, 100, 200, 50, 0.8, 2.5, 0.35),
2550        ];
2551
2552        for (
2553            cfg_idx,
2554            &(
2555                va_s,
2556                va_e,
2557                va_st,
2558                vs_s,
2559                vs_e,
2560                vs_st,
2561                sa_s,
2562                sa_e,
2563                sa_st,
2564                ss_s,
2565                ss_e,
2566                ss_st,
2567                th_s,
2568                th_e,
2569                th_st,
2570            ),
2571        ) in test_configs.iter().enumerate()
2572        {
2573            let output = DamianiVolatmeterBatchBuilder::new()
2574                .kernel(kernel)
2575                .vis_atr_range(va_s, va_e, va_st)
2576                .vis_std_range(vs_s, vs_e, vs_st)
2577                .sed_atr_range(sa_s, sa_e, sa_st)
2578                .sed_std_range(ss_s, ss_e, ss_st)
2579                .threshold_range(th_s, th_e, th_st)
2580                .apply_candles(&c, "close")?;
2581
2582            for (idx, &val) in output.vol.iter().enumerate() {
2583                if val.is_nan() {
2584                    continue;
2585                }
2586
2587                let bits = val.to_bits();
2588                let row = idx / output.cols;
2589                let col = idx % output.cols;
2590                let combo = &output.combos[row];
2591
2592                if bits == 0x11111111_11111111 {
2593                    panic!(
2594						"[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) in vol \
2595						 at row {} col {} (flat index {}) with params: vis_atr={}, vis_std={}, sed_atr={}, \
2596						 sed_std={}, threshold={}",
2597						test, cfg_idx, val, bits, row, col, idx,
2598						combo.vis_atr.unwrap(), combo.vis_std.unwrap(),
2599						combo.sed_atr.unwrap(), combo.sed_std.unwrap(),
2600						combo.threshold.unwrap()
2601					);
2602                }
2603
2604                if bits == 0x22222222_22222222 {
2605                    panic!(
2606						"[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) in vol \
2607						 at row {} col {} (flat index {}) with params: vis_atr={}, vis_std={}, sed_atr={}, \
2608						 sed_std={}, threshold={}",
2609						test, cfg_idx, val, bits, row, col, idx,
2610						combo.vis_atr.unwrap(), combo.vis_std.unwrap(),
2611						combo.sed_atr.unwrap(), combo.sed_std.unwrap(),
2612						combo.threshold.unwrap()
2613					);
2614                }
2615
2616                if bits == 0x33333333_33333333 {
2617                    panic!(
2618						"[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) in vol \
2619						 at row {} col {} (flat index {}) with params: vis_atr={}, vis_std={}, sed_atr={}, \
2620						 sed_std={}, threshold={}",
2621						test, cfg_idx, val, bits, row, col, idx,
2622						combo.vis_atr.unwrap(), combo.vis_std.unwrap(),
2623						combo.sed_atr.unwrap(), combo.sed_std.unwrap(),
2624						combo.threshold.unwrap()
2625					);
2626                }
2627            }
2628
2629            for (idx, &val) in output.anti.iter().enumerate() {
2630                if val.is_nan() {
2631                    continue;
2632                }
2633
2634                let bits = val.to_bits();
2635                let row = idx / output.cols;
2636                let col = idx % output.cols;
2637                let combo = &output.combos[row];
2638
2639                if bits == 0x11111111_11111111 {
2640                    panic!(
2641						"[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) in anti \
2642						 at row {} col {} (flat index {}) with params: vis_atr={}, vis_std={}, sed_atr={}, \
2643						 sed_std={}, threshold={}",
2644						test, cfg_idx, val, bits, row, col, idx,
2645						combo.vis_atr.unwrap(), combo.vis_std.unwrap(),
2646						combo.sed_atr.unwrap(), combo.sed_std.unwrap(),
2647						combo.threshold.unwrap()
2648					);
2649                }
2650
2651                if bits == 0x22222222_22222222 {
2652                    panic!(
2653						"[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) in anti \
2654						 at row {} col {} (flat index {}) with params: vis_atr={}, vis_std={}, sed_atr={}, \
2655						 sed_std={}, threshold={}",
2656						test, cfg_idx, val, bits, row, col, idx,
2657						combo.vis_atr.unwrap(), combo.vis_std.unwrap(),
2658						combo.sed_atr.unwrap(), combo.sed_std.unwrap(),
2659						combo.threshold.unwrap()
2660					);
2661                }
2662
2663                if bits == 0x33333333_33333333 {
2664                    panic!(
2665						"[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) in anti \
2666						 at row {} col {} (flat index {}) with params: vis_atr={}, vis_std={}, sed_atr={}, \
2667						 sed_std={}, threshold={}",
2668						test, cfg_idx, val, bits, row, col, idx,
2669						combo.vis_atr.unwrap(), combo.vis_std.unwrap(),
2670						combo.sed_atr.unwrap(), combo.sed_std.unwrap(),
2671						combo.threshold.unwrap()
2672					);
2673                }
2674            }
2675        }
2676
2677        Ok(())
2678    }
2679
2680    #[cfg(not(debug_assertions))]
2681    fn check_batch_no_poison(
2682        _test: &str,
2683        _kernel: Kernel,
2684    ) -> Result<(), Box<dyn std::error::Error>> {
2685        Ok(())
2686    }
2687
2688    fn check_damiani_empty_input(
2689        test_name: &str,
2690        kernel: Kernel,
2691    ) -> Result<(), Box<dyn std::error::Error>> {
2692        skip_if_unsupported!(kernel, test_name);
2693        let empty: [f64; 0] = [];
2694        let params = DamianiVolatmeterParams::default();
2695        let input = DamianiVolatmeterInput::from_slice(&empty, params);
2696        let res = damiani_volatmeter_with_kernel(&input, kernel);
2697        assert!(
2698            matches!(res, Err(DamianiVolatmeterError::EmptyData)),
2699            "[{}] should fail with empty input",
2700            test_name
2701        );
2702        Ok(())
2703    }
2704
2705    fn check_damiani_all_nan(
2706        test_name: &str,
2707        kernel: Kernel,
2708    ) -> Result<(), Box<dyn std::error::Error>> {
2709        skip_if_unsupported!(kernel, test_name);
2710        let data = vec![f64::NAN; 200];
2711        let params = DamianiVolatmeterParams::default();
2712        let input = DamianiVolatmeterInput::from_slice(&data, params);
2713        let res = damiani_volatmeter_with_kernel(&input, kernel);
2714        assert!(
2715            matches!(res, Err(DamianiVolatmeterError::AllValuesNaN)),
2716            "[{}] should fail with all NaN values",
2717            test_name
2718        );
2719        Ok(())
2720    }
2721
2722    fn check_damiani_invalid_threshold(
2723        test_name: &str,
2724        kernel: Kernel,
2725    ) -> Result<(), Box<dyn std::error::Error>> {
2726        skip_if_unsupported!(kernel, test_name);
2727        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2728        let candles = read_candles_from_csv(file_path)?;
2729
2730        let mut params = DamianiVolatmeterParams::default();
2731        params.threshold = Some(f64::NAN);
2732        let input = DamianiVolatmeterInput::from_candles(&candles, "close", params.clone());
2733        let res = damiani_volatmeter_with_kernel(&input, kernel);
2734
2735        assert!(
2736            res.is_ok(),
2737            "[{}] should not fail with NaN threshold",
2738            test_name
2739        );
2740
2741        params.threshold = Some(-1.0);
2742        let input2 = DamianiVolatmeterInput::from_candles(&candles, "close", params);
2743        let res2 = damiani_volatmeter_with_kernel(&input2, kernel);
2744        assert!(
2745            res2.is_ok(),
2746            "[{}] should work with negative threshold",
2747            test_name
2748        );
2749        Ok(())
2750    }
2751
2752    fn check_damiani_invalid_periods(
2753        test_name: &str,
2754        kernel: Kernel,
2755    ) -> Result<(), Box<dyn std::error::Error>> {
2756        skip_if_unsupported!(kernel, test_name);
2757        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
2758
2759        let mut params = DamianiVolatmeterParams::default();
2760        params.vis_atr = Some(0);
2761        let input = DamianiVolatmeterInput::from_slice(&data, params);
2762        let res = damiani_volatmeter_with_kernel(&input, kernel);
2763        assert!(
2764            matches!(res, Err(DamianiVolatmeterError::InvalidPeriod { .. })),
2765            "[{}] should fail with zero vis_atr",
2766            test_name
2767        );
2768
2769        params = DamianiVolatmeterParams::default();
2770        params.vis_std = Some(0);
2771        let input2 = DamianiVolatmeterInput::from_slice(&data, params);
2772        let res2 = damiani_volatmeter_with_kernel(&input2, kernel);
2773        assert!(
2774            matches!(res2, Err(DamianiVolatmeterError::InvalidPeriod { .. })),
2775            "[{}] should fail with zero vis_std",
2776            test_name
2777        );
2778
2779        params = DamianiVolatmeterParams::default();
2780        params.sed_std = Some(1000);
2781        let input3 = DamianiVolatmeterInput::from_slice(&data, params);
2782        let res3 = damiani_volatmeter_with_kernel(&input3, kernel);
2783        assert!(
2784            matches!(res3, Err(DamianiVolatmeterError::InvalidPeriod { .. })),
2785            "[{}] should fail with period exceeding length",
2786            test_name
2787        );
2788        Ok(())
2789    }
2790
2791    fn check_damiani_into_existing_slice(
2792        test_name: &str,
2793        kernel: Kernel,
2794    ) -> Result<(), Box<dyn std::error::Error>> {
2795        skip_if_unsupported!(kernel, test_name);
2796        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2797        let candles = read_candles_from_csv(file_path)?;
2798        let params = DamianiVolatmeterParams::default();
2799        let input = DamianiVolatmeterInput::from_candles(&candles, "close", params);
2800
2801        let output1 = damiani_volatmeter_with_kernel(&input, kernel)?;
2802
2803        let mut vol2 = vec![0.0; candles.close.len()];
2804        let mut anti2 = vec![0.0; candles.close.len()];
2805        damiani_volatmeter_into_slice(&mut vol2, &mut anti2, &input, kernel)?;
2806
2807        assert_eq!(output1.vol.len(), vol2.len());
2808        assert_eq!(output1.anti.len(), anti2.len());
2809
2810        for i in 0..output1.vol.len() {
2811            if output1.vol[i].is_nan() && vol2[i].is_nan() {
2812                continue;
2813            }
2814            assert!(
2815                (output1.vol[i] - vol2[i]).abs() < 1e-10,
2816                "[{}] vol mismatch at index {}: {} vs {}",
2817                test_name,
2818                i,
2819                output1.vol[i],
2820                vol2[i]
2821            );
2822        }
2823
2824        for i in 0..output1.anti.len() {
2825            if output1.anti[i].is_nan() && anti2[i].is_nan() {
2826                continue;
2827            }
2828            assert!(
2829                (output1.anti[i] - anti2[i]).abs() < 1e-10,
2830                "[{}] anti mismatch at index {}: {} vs {}",
2831                test_name,
2832                i,
2833                output1.anti[i],
2834                anti2[i]
2835            );
2836        }
2837        Ok(())
2838    }
2839
2840    #[cfg(feature = "proptest")]
2841    #[allow(clippy::float_cmp)]
2842    fn check_damiani_property(
2843        test_name: &str,
2844        kernel: Kernel,
2845    ) -> Result<(), Box<dyn std::error::Error>> {
2846        use proptest::prelude::*;
2847        skip_if_unsupported!(kernel, test_name);
2848
2849        let strat = (
2850            5usize..=20,
2851            10usize..=30,
2852            20usize..=50,
2853            50usize..=150,
2854            0.5f64..3.0f64,
2855        )
2856            .prop_flat_map(|(vis_atr, vis_std, sed_atr, sed_std, threshold)| {
2857                let min_len = *[vis_atr, vis_std, sed_atr, sed_std].iter().max().unwrap() + 10;
2858                (
2859                    prop::collection::vec(
2860                        (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
2861                        min_len..400,
2862                    ),
2863                    Just(vis_atr),
2864                    Just(vis_std),
2865                    Just(sed_atr),
2866                    Just(sed_std),
2867                    Just(threshold),
2868                )
2869            });
2870
2871        proptest::test_runner::TestRunner::default()
2872            .run(
2873                &strat,
2874                |(data, vis_atr, vis_std, sed_atr, sed_std, threshold)| {
2875                    let params = DamianiVolatmeterParams {
2876                        vis_atr: Some(vis_atr),
2877                        vis_std: Some(vis_std),
2878                        sed_atr: Some(sed_atr),
2879                        sed_std: Some(sed_std),
2880                        threshold: Some(threshold),
2881                    };
2882                    let input = DamianiVolatmeterInput::from_slice(&data, params);
2883
2884                    let output = damiani_volatmeter_with_kernel(&input, kernel)?;
2885
2886                    let ref_output = damiani_volatmeter_with_kernel(&input, Kernel::Scalar)?;
2887
2888                    prop_assert_eq!(output.vol.len(), data.len(), "vol length mismatch");
2889                    prop_assert_eq!(output.anti.len(), data.len(), "anti length mismatch");
2890
2891                    let warmup = *[vis_atr, vis_std, sed_atr, sed_std, 3]
2892                        .iter()
2893                        .max()
2894                        .unwrap();
2895
2896                    for i in 0..warmup.min(data.len()) {
2897                        prop_assert!(
2898                            output.vol[i].is_nan(),
2899                            "vol[{}] should be NaN during warmup but got {}",
2900                            i,
2901                            output.vol[i]
2902                        );
2903                    }
2904
2905                    let first_valid_vol = output.vol.iter().position(|&x| !x.is_nan());
2906                    if let Some(idx) = first_valid_vol {
2907                        prop_assert!(
2908                            idx >= warmup - 1,
2909                            "First valid vol at {} but warmup is {}",
2910                            idx,
2911                            warmup
2912                        );
2913                    }
2914
2915                    for (i, &val) in output.vol.iter().enumerate() {
2916                        if !val.is_nan() {
2917                            prop_assert!(
2918                                val.is_finite(),
2919                                "vol[{}] should be finite but got {}",
2920                                i,
2921                                val
2922                            );
2923
2924                            prop_assert!(
2925                                val.abs() < 1e10,
2926                                "vol[{}] = {} is unreasonably large",
2927                                i,
2928                                val
2929                            );
2930                        }
2931                    }
2932
2933                    for (i, &val) in output.anti.iter().enumerate() {
2934                        if !val.is_nan() {
2935                            prop_assert!(
2936                                val.is_finite(),
2937                                "anti[{}] should be finite but got {}",
2938                                i,
2939                                val
2940                            );
2941                        }
2942                    }
2943
2944                    for i in 0..data.len() {
2945                        let vol = output.vol[i];
2946                        let ref_vol = ref_output.vol[i];
2947                        let anti = output.anti[i];
2948                        let ref_anti = ref_output.anti[i];
2949
2950                        if !vol.is_finite() || !ref_vol.is_finite() {
2951                            prop_assert!(
2952                                vol.to_bits() == ref_vol.to_bits(),
2953                                "vol finite/NaN mismatch at {}: {} vs {}",
2954                                i,
2955                                vol,
2956                                ref_vol
2957                            );
2958                        } else {
2959                            let vol_bits = vol.to_bits();
2960                            let ref_vol_bits = ref_vol.to_bits();
2961                            let ulp_diff = vol_bits.abs_diff(ref_vol_bits);
2962
2963                            prop_assert!(
2964                                (vol - ref_vol).abs() <= 1e-9 || ulp_diff <= 8,
2965                                "vol mismatch at {}: {} vs {} (ULP={})",
2966                                i,
2967                                vol,
2968                                ref_vol,
2969                                ulp_diff
2970                            );
2971                        }
2972
2973                        if !anti.is_finite() || !ref_anti.is_finite() {
2974                            prop_assert!(
2975                                anti.to_bits() == ref_anti.to_bits(),
2976                                "anti finite/NaN mismatch at {}: {} vs {}",
2977                                i,
2978                                anti,
2979                                ref_anti
2980                            );
2981                        } else {
2982                            let anti_bits = anti.to_bits();
2983                            let ref_anti_bits = ref_anti.to_bits();
2984                            let ulp_diff = anti_bits.abs_diff(ref_anti_bits);
2985
2986                            prop_assert!(
2987                                (anti - ref_anti).abs() <= 1e-9 || ulp_diff <= 8,
2988                                "anti mismatch at {}: {} vs {} (ULP={})",
2989                                i,
2990                                anti,
2991                                ref_anti,
2992                                ulp_diff
2993                            );
2994                        }
2995                    }
2996
2997                    if data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10) {
2998                        for (i, &val) in output.vol.iter().enumerate().skip(warmup) {
2999                            if !val.is_nan() {
3000                                prop_assert!(
3001                                    val.abs() < 1e-4,
3002                                    "vol[{}] = {} should be near zero for constant data",
3003                                    i,
3004                                    val
3005                                );
3006                            }
3007                        }
3008                    }
3009
3010                    let is_strong_uptrend = data.windows(2).all(|w| w[1] > w[0] + 0.01);
3011                    let is_strong_downtrend = data.windows(2).all(|w| w[1] < w[0] - 0.01);
3012
3013                    if is_strong_uptrend || is_strong_downtrend {
3014                        let valid_vols: Vec<f64> = output
3015                            .vol
3016                            .iter()
3017                            .skip(warmup)
3018                            .filter(|&&x| !x.is_nan())
3019                            .copied()
3020                            .collect();
3021
3022                        if !valid_vols.is_empty() {
3023                            let non_zero_count =
3024                                valid_vols.iter().filter(|&&v| v.abs() > 1e-10).count();
3025                            prop_assert!(
3026                                non_zero_count > 0,
3027                                "Expected non-zero volatility values for trending data"
3028                            );
3029                        }
3030                    }
3031
3032                    for i in warmup..data.len() {
3033                        if !output.vol[i].is_nan() && !output.anti[i].is_nan() {
3034                            prop_assert!(
3035                                output.vol[i].is_finite() && output.anti[i].is_finite(),
3036                                "vol[{}] and anti[{}] should both be finite",
3037                                i,
3038                                i
3039                            );
3040                        }
3041                    }
3042
3043                    Ok(())
3044                },
3045            )
3046            .unwrap();
3047
3048        Ok(())
3049    }
3050
3051    macro_rules! generate_all_damiani_tests {
3052        ($($test_fn:ident),*) => {
3053            paste::paste! {
3054                $(
3055                    #[test]
3056                    fn [<$test_fn _scalar_f64>]() {
3057                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
3058                    }
3059                )*
3060                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3061                $(
3062                    #[test]
3063                    fn [<$test_fn _avx2_f64>]() {
3064                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
3065                    }
3066                    #[test]
3067                    fn [<$test_fn _avx512_f64>]() {
3068                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
3069                    }
3070                )*
3071            }
3072        }
3073    }
3074    macro_rules! gen_batch_tests {
3075        ($fn_name:ident) => {
3076            paste::paste! {
3077                #[test] fn [<$fn_name _scalar>]()      {
3078                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3079                }
3080                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3081                #[test] fn [<$fn_name _avx2>]()        {
3082                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3083                }
3084                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3085                #[test] fn [<$fn_name _avx512>]()      {
3086                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3087                }
3088                #[test] fn [<$fn_name _auto_detect>]() {
3089                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3090                }
3091            }
3092        };
3093    }
3094    generate_all_damiani_tests!(
3095        check_damiani_partial_params,
3096        check_damiani_accuracy,
3097        check_damiani_zero_period,
3098        check_damiani_period_exceeds_length,
3099        check_damiani_very_small_dataset,
3100        check_damiani_streaming,
3101        check_damiani_input_with_default_candles,
3102        check_damiani_params_with_defaults,
3103        check_damiani_no_poison,
3104        check_damiani_empty_input,
3105        check_damiani_all_nan,
3106        check_damiani_invalid_threshold,
3107        check_damiani_invalid_periods,
3108        check_damiani_into_existing_slice
3109    );
3110
3111    #[cfg(feature = "proptest")]
3112    generate_all_damiani_tests!(check_damiani_property);
3113
3114    gen_batch_tests!(check_batch_default_row);
3115}
3116
3117#[cfg(feature = "python")]
3118#[pyfunction(name = "damiani")]
3119#[pyo3(signature = (data, vis_atr, vis_std, sed_atr, sed_std, threshold, kernel=None))]
3120pub fn damiani_py<'py>(
3121    py: Python<'py>,
3122    data: PyReadonlyArray1<'py, f64>,
3123    vis_atr: usize,
3124    vis_std: usize,
3125    sed_atr: usize,
3126    sed_std: usize,
3127    threshold: f64,
3128    kernel: Option<&str>,
3129) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
3130    let slice_in = data.as_slice()?;
3131    let kern = validate_kernel(kernel, false)?;
3132    let params = DamianiVolatmeterParams {
3133        vis_atr: Some(vis_atr),
3134        vis_std: Some(vis_std),
3135        sed_atr: Some(sed_atr),
3136        sed_std: Some(sed_std),
3137        threshold: Some(threshold),
3138    };
3139    let input = DamianiVolatmeterInput::from_slice(slice_in, params);
3140
3141    let len = slice_in.len();
3142
3143    let vol_np = unsafe { PyArray1::<f64>::new(py, [len], false) };
3144    let anti_np = unsafe { PyArray1::<f64>::new(py, [len], false) };
3145
3146    unsafe {
3147        let vol_sl = vol_np
3148            .as_slice_mut()
3149            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3150        let anti_sl = anti_np
3151            .as_slice_mut()
3152            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3153
3154        py.allow_threads(|| damiani_volatmeter_into_slice(vol_sl, anti_sl, &input, kern))
3155    }
3156    .map_err(|e| PyValueError::new_err(e.to_string()))?;
3157
3158    Ok((vol_np, anti_np))
3159}
3160
3161#[cfg(feature = "python")]
3162#[pyclass(name = "DamianiVolatmeterStream")]
3163pub struct DamianiVolatmeterStreamPy {
3164    high: Vec<f64>,
3165    low: Vec<f64>,
3166    close: Vec<f64>,
3167
3168    vis_atr: usize,
3169    vis_std: usize,
3170    sed_atr: usize,
3171    sed_std: usize,
3172    threshold: f64,
3173
3174    index: usize,
3175
3176    atr_vis_val: f64,
3177    atr_sed_val: f64,
3178    sum_vis: f64,
3179    sum_sed: f64,
3180    prev_close: f64,
3181    have_prev: bool,
3182
3183    ring_vis: Vec<f64>,
3184    ring_sed: Vec<f64>,
3185    sum_vis_std: f64,
3186    sum_sq_vis_std: f64,
3187    sum_sed_std: f64,
3188    sum_sq_sed_std: f64,
3189    idx_vis: usize,
3190    idx_sed: usize,
3191    filled_vis: usize,
3192    filled_sed: usize,
3193
3194    vol_history: [f64; 3],
3195    lag_s: f64,
3196}
3197
3198#[cfg(feature = "python")]
3199#[pymethods]
3200impl DamianiVolatmeterStreamPy {
3201    #[new]
3202    fn new(
3203        high: Vec<f64>,
3204        low: Vec<f64>,
3205        close: Vec<f64>,
3206        vis_atr: usize,
3207        vis_std: usize,
3208        sed_atr: usize,
3209        sed_std: usize,
3210        threshold: f64,
3211    ) -> PyResult<Self> {
3212        let len = close.len();
3213        if len == 0 {
3214            return Err(PyValueError::new_err("Empty data"));
3215        }
3216
3217        if vis_atr == 0
3218            || vis_std == 0
3219            || sed_atr == 0
3220            || sed_std == 0
3221            || vis_atr > len
3222            || vis_std > len
3223            || sed_atr > len
3224            || sed_std > len
3225        {
3226            return Err(PyValueError::new_err(format!(
3227				"Invalid period: data length = {}, vis_atr = {}, vis_std = {}, sed_atr = {}, sed_std = {}",
3228				len, vis_atr, vis_std, sed_atr, sed_std
3229			)));
3230        }
3231
3232        let first = close
3233            .iter()
3234            .position(|&x| !x.is_nan())
3235            .ok_or_else(|| PyValueError::new_err("All values are NaN"))?;
3236
3237        let needed = *[vis_atr, vis_std, sed_atr, sed_std, 3]
3238            .iter()
3239            .max()
3240            .unwrap();
3241        if (len - first) < needed {
3242            return Err(PyValueError::new_err(format!(
3243                "Not enough valid data: needed {}, valid {}",
3244                needed,
3245                len - first
3246            )));
3247        }
3248
3249        Ok(Self {
3250            high,
3251            low,
3252            close,
3253            vis_atr,
3254            vis_std,
3255            sed_atr,
3256            sed_std,
3257            threshold,
3258            index: first,
3259            atr_vis_val: f64::NAN,
3260            atr_sed_val: f64::NAN,
3261            sum_vis: 0.0,
3262            sum_sed: 0.0,
3263            prev_close: f64::NAN,
3264            have_prev: false,
3265            ring_vis: vec![0.0; vis_std],
3266            ring_sed: vec![0.0; sed_std],
3267            sum_vis_std: 0.0,
3268            sum_sq_vis_std: 0.0,
3269            sum_sed_std: 0.0,
3270            sum_sq_sed_std: 0.0,
3271            idx_vis: 0,
3272            idx_sed: 0,
3273            filled_vis: 0,
3274            filled_sed: 0,
3275            vol_history: [f64::NAN; 3],
3276            lag_s: 0.5,
3277        })
3278    }
3279
3280    fn update(&mut self) -> Option<(f64, f64)> {
3281        let i = self.index;
3282        let len = self.close.len();
3283        if i >= len {
3284            return None;
3285        }
3286
3287        let tr = if self.have_prev && self.close[i].is_finite() {
3288            let hi = self.high[i];
3289            let lo = self.low[i];
3290            let pc = self.prev_close;
3291
3292            let tr1 = hi - lo;
3293            let tr2 = (hi - pc).abs();
3294            let tr3 = (lo - pc).abs();
3295            tr1.max(tr2).max(tr3)
3296        } else {
3297            0.0
3298        };
3299
3300        if self.close[i].is_finite() {
3301            self.prev_close = self.close[i];
3302            self.have_prev = true;
3303        }
3304
3305        if i < self.vis_atr {
3306            self.sum_vis += tr;
3307            if i == self.vis_atr - 1 {
3308                self.atr_vis_val = self.sum_vis / (self.vis_atr as f64);
3309            }
3310        } else if self.atr_vis_val.is_finite() {
3311            self.atr_vis_val =
3312                ((self.vis_atr as f64 - 1.0) * self.atr_vis_val + tr) / (self.vis_atr as f64);
3313        }
3314
3315        if i < self.sed_atr {
3316            self.sum_sed += tr;
3317            if i == self.sed_atr - 1 {
3318                self.atr_sed_val = self.sum_sed / (self.sed_atr as f64);
3319            }
3320        } else if self.atr_sed_val.is_finite() {
3321            self.atr_sed_val =
3322                ((self.sed_atr as f64 - 1.0) * self.atr_sed_val + tr) / (self.sed_atr as f64);
3323        }
3324
3325        let val = if self.close[i].is_nan() {
3326            0.0
3327        } else {
3328            self.close[i]
3329        };
3330
3331        let old_v = self.ring_vis[self.idx_vis];
3332        self.ring_vis[self.idx_vis] = val;
3333        self.idx_vis = (self.idx_vis + 1) % self.vis_std;
3334        if self.filled_vis < self.vis_std {
3335            self.filled_vis += 1;
3336            self.sum_vis_std += val;
3337            self.sum_sq_vis_std += val * val;
3338        } else {
3339            self.sum_vis_std = self.sum_vis_std - old_v + val;
3340            self.sum_sq_vis_std = self.sum_sq_vis_std - (old_v * old_v) + (val * val);
3341        }
3342
3343        let old_s = self.ring_sed[self.idx_sed];
3344        self.ring_sed[self.idx_sed] = val;
3345        self.idx_sed = (self.idx_sed + 1) % self.sed_std;
3346        if self.filled_sed < self.sed_std {
3347            self.filled_sed += 1;
3348            self.sum_sed_std += val;
3349            self.sum_sq_sed_std += val * val;
3350        } else {
3351            self.sum_sed_std = self.sum_sed_std - old_s + val;
3352            self.sum_sq_sed_std = self.sum_sq_sed_std - (old_s * old_s) + (val * val);
3353        }
3354
3355        self.index += 1;
3356
3357        let needed = *[self.vis_atr, self.vis_std, self.sed_atr, self.sed_std, 3]
3358            .iter()
3359            .max()
3360            .unwrap();
3361        if i < needed {
3362            return None;
3363        }
3364
3365        let p1 = if !self.vol_history[0].is_nan() {
3366            self.vol_history[0]
3367        } else {
3368            0.0
3369        };
3370        let p3 = if !self.vol_history[2].is_nan() {
3371            self.vol_history[2]
3372        } else {
3373            0.0
3374        };
3375
3376        let sed_safe = if self.atr_sed_val.is_finite() && self.atr_sed_val != 0.0 {
3377            self.atr_sed_val
3378        } else {
3379            self.atr_sed_val + f64::EPSILON
3380        };
3381
3382        let vol_val = (self.atr_vis_val / sed_safe) + self.lag_s * (p1 - p3);
3383
3384        self.vol_history[2] = self.vol_history[1];
3385        self.vol_history[1] = self.vol_history[0];
3386        self.vol_history[0] = vol_val;
3387
3388        let anti_val = if self.filled_vis == self.vis_std && self.filled_sed == self.sed_std {
3389            let std_vis = stddev(self.sum_vis_std, self.sum_sq_vis_std, self.vis_std);
3390            let std_sed = stddev(self.sum_sed_std, self.sum_sq_sed_std, self.sed_std);
3391            let ratio = if std_sed != 0.0 {
3392                std_vis / std_sed
3393            } else {
3394                std_vis / (std_sed + f64::EPSILON)
3395            };
3396            self.threshold - ratio
3397        } else {
3398            f64::NAN
3399        };
3400
3401        Some((vol_val, anti_val))
3402    }
3403}
3404
3405#[cfg(feature = "python")]
3406#[pyfunction(name = "damiani_batch")]
3407#[pyo3(signature = (data, vis_atr_range, vis_std_range, sed_atr_range, sed_std_range, threshold_range, kernel=None))]
3408pub fn damiani_batch_py<'py>(
3409    py: Python<'py>,
3410    data: PyReadonlyArray1<'py, f64>,
3411    vis_atr_range: (usize, usize, usize),
3412    vis_std_range: (usize, usize, usize),
3413    sed_atr_range: (usize, usize, usize),
3414    sed_std_range: (usize, usize, usize),
3415    threshold_range: (f64, f64, f64),
3416    kernel: Option<&str>,
3417) -> PyResult<Bound<'py, PyDict>> {
3418    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
3419
3420    let slice_in = data.as_slice()?;
3421    let sweep = DamianiVolatmeterBatchRange {
3422        vis_atr: vis_atr_range,
3423        vis_std: vis_std_range,
3424        sed_atr: sed_atr_range,
3425        sed_std: sed_std_range,
3426        threshold: threshold_range,
3427    };
3428
3429    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
3430    let rows = combos.len();
3431    let cols = slice_in.len();
3432
3433    let expected = rows
3434        .checked_mul(cols)
3435        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
3436    let vol_np = unsafe { PyArray1::<f64>::new(py, [expected], false) };
3437    let anti_np = unsafe { PyArray1::<f64>::new(py, [expected], false) };
3438    let vol_sl = unsafe { vol_np.as_slice_mut()? };
3439    let anti_sl = unsafe { anti_np.as_slice_mut()? };
3440
3441    let kern = validate_kernel(kernel, true)?;
3442
3443    py.allow_threads(|| {
3444        let kernel = match kern {
3445            Kernel::Auto => detect_best_batch_kernel(),
3446            k => k,
3447        };
3448        let simd = match kernel {
3449            Kernel::Avx512Batch => Kernel::Avx512,
3450            Kernel::Avx2Batch => Kernel::Avx2,
3451            Kernel::ScalarBatch => Kernel::Scalar,
3452            _ => unreachable!(),
3453        };
3454        damiani_volatmeter_batch_inner_into(slice_in, &sweep, simd, true, vol_sl, anti_sl)
3455    })
3456    .map_err(|e| PyValueError::new_err(e.to_string()))?;
3457
3458    let d = PyDict::new(py);
3459    d.set_item("vol", vol_np.reshape((rows, cols))?)?;
3460    d.set_item("anti", anti_np.reshape((rows, cols))?)?;
3461    d.set_item(
3462        "vis_atr",
3463        combos
3464            .iter()
3465            .map(|p| p.vis_atr.unwrap() as u64)
3466            .collect::<Vec<_>>()
3467            .into_pyarray(py),
3468    )?;
3469    d.set_item(
3470        "vis_std",
3471        combos
3472            .iter()
3473            .map(|p| p.vis_std.unwrap() as u64)
3474            .collect::<Vec<_>>()
3475            .into_pyarray(py),
3476    )?;
3477    d.set_item(
3478        "sed_atr",
3479        combos
3480            .iter()
3481            .map(|p| p.sed_atr.unwrap() as u64)
3482            .collect::<Vec<_>>()
3483            .into_pyarray(py),
3484    )?;
3485    d.set_item(
3486        "sed_std",
3487        combos
3488            .iter()
3489            .map(|p| p.sed_std.unwrap() as u64)
3490            .collect::<Vec<_>>()
3491            .into_pyarray(py),
3492    )?;
3493    d.set_item(
3494        "threshold",
3495        combos
3496            .iter()
3497            .map(|p| p.threshold.unwrap())
3498            .collect::<Vec<_>>()
3499            .into_pyarray(py),
3500    )?;
3501    Ok(d)
3502}
3503
3504#[cfg(all(feature = "python", feature = "cuda"))]
3505#[pyclass(module = "ta_indicators.cuda", unsendable)]
3506pub struct DeviceArrayF32DamianiPy {
3507    pub(crate) inner: crate::cuda::damiani_volatmeter_wrapper::DeviceArrayF32Damiani,
3508}
3509
3510#[cfg(all(feature = "python", feature = "cuda"))]
3511#[pymethods]
3512impl DeviceArrayF32DamianiPy {
3513    #[getter]
3514    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
3515        let d = PyDict::new(py);
3516        d.set_item("shape", (self.inner.rows, self.inner.cols))?;
3517        d.set_item("typestr", "<f4")?;
3518        d.set_item(
3519            "strides",
3520            (
3521                self.inner.cols * std::mem::size_of::<f32>(),
3522                std::mem::size_of::<f32>(),
3523            ),
3524        )?;
3525        d.set_item("data", (self.inner.device_ptr() as usize, false))?;
3526
3527        d.set_item("version", 3)?;
3528        Ok(d)
3529    }
3530
3531    fn __dlpack_device__(&self) -> (i32, i32) {
3532        (2, self.inner.device_id as i32)
3533    }
3534
3535    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
3536    fn __dlpack__<'py>(
3537        &mut self,
3538        py: Python<'py>,
3539        stream: Option<pyo3::PyObject>,
3540        max_version: Option<pyo3::PyObject>,
3541        dl_device: Option<pyo3::PyObject>,
3542        copy: Option<pyo3::PyObject>,
3543    ) -> PyResult<PyObject> {
3544        use crate::cuda::damiani_volatmeter_wrapper::DeviceArrayF32Damiani;
3545        use cust::memory::DeviceBuffer;
3546
3547        let (kdl, alloc_dev) = self.__dlpack_device__();
3548        if let Some(dev_obj) = dl_device.as_ref() {
3549            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
3550                if dev_ty != kdl || dev_id != alloc_dev {
3551                    let wants_copy = copy
3552                        .as_ref()
3553                        .and_then(|c| c.extract::<bool>(py).ok())
3554                        .unwrap_or(false);
3555                    if wants_copy {
3556                        return Err(PyValueError::new_err(
3557                            "device copy not implemented for __dlpack__",
3558                        ));
3559                    } else {
3560                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
3561                    }
3562                }
3563            }
3564        }
3565
3566        if let Some(obj) = &stream {
3567            if let Ok(i) = obj.extract::<i64>(py) {
3568                if i == 0 {
3569                    return Err(PyValueError::new_err(
3570                        "__dlpack__: stream 0 is disallowed for CUDA",
3571                    ));
3572                }
3573            }
3574        }
3575
3576        let dummy =
3577            DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
3578        let ctx = self.inner.ctx.clone();
3579        let device_id = self.inner.device_id;
3580        let inner = std::mem::replace(
3581            &mut self.inner,
3582            DeviceArrayF32Damiani {
3583                buf: dummy,
3584                rows: 0,
3585                cols: 0,
3586                ctx,
3587                device_id,
3588            },
3589        );
3590
3591        let rows = inner.rows;
3592        let cols = inner.cols;
3593        let buf = inner.buf;
3594
3595        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
3596
3597        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
3598    }
3599}
3600
3601#[cfg(all(feature = "python", feature = "cuda"))]
3602#[pyfunction(name = "damiani_cuda_batch_dev")]
3603#[pyo3(signature = (data_f32, vis_atr_range, vis_std_range, sed_atr_range, sed_std_range, threshold_range, device_id=0))]
3604pub fn damiani_cuda_batch_dev_py<'py>(
3605    py: Python<'py>,
3606    data_f32: PyReadonlyArray1<'py, f32>,
3607    vis_atr_range: (usize, usize, usize),
3608    vis_std_range: (usize, usize, usize),
3609    sed_atr_range: (usize, usize, usize),
3610    sed_std_range: (usize, usize, usize),
3611    threshold_range: (f64, f64, f64),
3612    device_id: usize,
3613) -> PyResult<DeviceArrayF32DamianiPy> {
3614    use crate::cuda::cuda_available;
3615    if !cuda_available() {
3616        return Err(PyValueError::new_err("CUDA not available"));
3617    }
3618    let slice_in = data_f32.as_slice()?;
3619    let sweep = DamianiVolatmeterBatchRange {
3620        vis_atr: vis_atr_range,
3621        vis_std: vis_std_range,
3622        sed_atr: sed_atr_range,
3623        sed_std: sed_std_range,
3624        threshold: threshold_range,
3625    };
3626    let inner = py.allow_threads(|| {
3627        let cuda = crate::cuda::CudaDamianiVolatmeter::new(device_id)
3628            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3629        let (arr, _combos) = cuda
3630            .damiani_volatmeter_batch_dev(slice_in, &sweep)
3631            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3632        Ok::<_, pyo3::PyErr>(arr)
3633    })?;
3634
3635    Ok(DeviceArrayF32DamianiPy { inner })
3636}
3637
3638#[cfg(all(feature = "python", feature = "cuda"))]
3639#[pyfunction(name = "damiani_cuda_many_series_one_param_dev")]
3640#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, cols, rows, vis_atr, vis_std, sed_atr, sed_std, threshold, device_id=0))]
3641pub fn damiani_cuda_many_series_one_param_dev_py<'py>(
3642    py: Python<'py>,
3643    high_tm_f32: PyReadonlyArray1<'py, f32>,
3644    low_tm_f32: PyReadonlyArray1<'py, f32>,
3645    close_tm_f32: PyReadonlyArray1<'py, f32>,
3646    cols: usize,
3647    rows: usize,
3648    vis_atr: usize,
3649    vis_std: usize,
3650    sed_atr: usize,
3651    sed_std: usize,
3652    threshold: f64,
3653    device_id: usize,
3654) -> PyResult<DeviceArrayF32DamianiPy> {
3655    use crate::cuda::cuda_available;
3656    if !cuda_available() {
3657        return Err(PyValueError::new_err("CUDA not available"));
3658    }
3659    let h = high_tm_f32.as_slice()?;
3660    let l = low_tm_f32.as_slice()?;
3661    let c = close_tm_f32.as_slice()?;
3662    let expected = cols
3663        .checked_mul(rows)
3664        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
3665    if h.len() != expected || l.len() != expected || c.len() != expected {
3666        return Err(PyValueError::new_err("time-major input lengths mismatch"));
3667    }
3668    let params = DamianiVolatmeterParams {
3669        vis_atr: Some(vis_atr),
3670        vis_std: Some(vis_std),
3671        sed_atr: Some(sed_atr),
3672        sed_std: Some(sed_std),
3673        threshold: Some(threshold),
3674    };
3675    let inner = py.allow_threads(|| {
3676        let cuda = crate::cuda::CudaDamianiVolatmeter::new(device_id)
3677            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3678        cuda.damiani_volatmeter_many_series_one_param_time_major_dev(h, l, c, cols, rows, &params)
3679            .map_err(|e| PyValueError::new_err(e.to_string()))
3680    })?;
3681    Ok(DeviceArrayF32DamianiPy { inner })
3682}
3683
3684#[cfg(feature = "python")]
3685#[pyclass(name = "DamianiVolatmeterFeedStream")]
3686pub struct DamianiVolatmeterFeedStreamPy {
3687    stream: DamianiVolatmeterFeedStream,
3688}
3689
3690#[cfg(feature = "python")]
3691#[pymethods]
3692impl DamianiVolatmeterFeedStreamPy {
3693    #[new]
3694    fn new(
3695        vis_atr: usize,
3696        vis_std: usize,
3697        sed_atr: usize,
3698        sed_std: usize,
3699        threshold: f64,
3700    ) -> PyResult<Self> {
3701        let params = DamianiVolatmeterParams {
3702            vis_atr: Some(vis_atr),
3703            vis_std: Some(vis_std),
3704            sed_atr: Some(sed_atr),
3705            sed_std: Some(sed_std),
3706            threshold: Some(threshold),
3707        };
3708        let stream = DamianiVolatmeterFeedStream::try_new(params)
3709            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3710        Ok(DamianiVolatmeterFeedStreamPy { stream })
3711    }
3712
3713    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
3714        self.stream.update(high, low, close)
3715    }
3716}
3717
3718#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3719#[derive(Serialize, Deserialize)]
3720pub struct DamianiJsOutput {
3721    pub values: Vec<f64>,
3722    pub rows: usize,
3723    pub cols: usize,
3724}
3725
3726#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3727#[wasm_bindgen(js_name = damiani_volatmeter_js)]
3728pub fn damiani_volatmeter_wasm(
3729    data: &[f64],
3730    vis_atr: usize,
3731    vis_std: usize,
3732    sed_atr: usize,
3733    sed_std: usize,
3734    threshold: f64,
3735) -> Result<JsValue, JsValue> {
3736    let params = DamianiVolatmeterParams {
3737        vis_atr: Some(vis_atr),
3738        vis_std: Some(vis_std),
3739        sed_atr: Some(sed_atr),
3740        sed_std: Some(sed_std),
3741        threshold: Some(threshold),
3742    };
3743    let input = DamianiVolatmeterInput::from_slice(data, params);
3744    let out = damiani_volatmeter_with_kernel(&input, Kernel::Auto)
3745        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3746    let cols = data.len();
3747    let mut values = Vec::with_capacity(2 * cols);
3748    values.extend_from_slice(&out.vol);
3749    values.extend_from_slice(&out.anti);
3750    serde_wasm_bindgen::to_value(&DamianiJsOutput {
3751        values,
3752        rows: 2,
3753        cols,
3754    })
3755    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
3756}
3757
3758#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3759#[wasm_bindgen]
3760pub fn damiani_volatmeter_alloc(len: usize) -> *mut f64 {
3761    let mut vec = Vec::<f64>::with_capacity(len);
3762    let ptr = vec.as_mut_ptr();
3763    std::mem::forget(vec);
3764    ptr
3765}
3766
3767#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3768#[wasm_bindgen]
3769pub fn damiani_volatmeter_free(ptr: *mut f64, len: usize) {
3770    if !ptr.is_null() {
3771        unsafe {
3772            let _ = Vec::from_raw_parts(ptr, len, len);
3773        }
3774    }
3775}
3776
3777#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3778#[wasm_bindgen]
3779pub fn damiani_volatmeter_into(
3780    in_ptr: *const f64,
3781    out_vol_ptr: *mut f64,
3782    out_anti_ptr: *mut f64,
3783    len: usize,
3784    vis_atr: usize,
3785    vis_std: usize,
3786    sed_atr: usize,
3787    sed_std: usize,
3788    threshold: f64,
3789) -> Result<(), JsValue> {
3790    if in_ptr.is_null() || out_vol_ptr.is_null() || out_anti_ptr.is_null() {
3791        return Err(JsValue::from_str(
3792            "null pointer passed to damiani_volatmeter_into",
3793        ));
3794    }
3795
3796    if out_vol_ptr == out_anti_ptr {
3797        return Err(JsValue::from_str("vol_ptr and anti_ptr cannot be the same"));
3798    }
3799
3800    unsafe {
3801        let params = DamianiVolatmeterParams {
3802            vis_atr: Some(vis_atr),
3803            vis_std: Some(vis_std),
3804            sed_atr: Some(sed_atr),
3805            sed_std: Some(sed_std),
3806            threshold: Some(threshold),
3807        };
3808
3809        let in_addr = in_ptr as usize;
3810        let vol_addr = out_vol_ptr as usize;
3811        let anti_addr = out_anti_ptr as usize;
3812
3813        if in_addr == vol_addr || in_addr == anti_addr {
3814            let data_copy = std::slice::from_raw_parts(in_ptr, len).to_vec();
3815            let vol = std::slice::from_raw_parts_mut(out_vol_ptr, len);
3816            let anti = std::slice::from_raw_parts_mut(out_anti_ptr, len);
3817            let input = DamianiVolatmeterInput::from_slice(&data_copy, params);
3818            damiani_volatmeter_into_slice(vol, anti, &input, Kernel::Auto)
3819                .map_err(|e| JsValue::from_str(&e.to_string()))
3820        } else {
3821            let data = std::slice::from_raw_parts(in_ptr, len);
3822            let vol = std::slice::from_raw_parts_mut(out_vol_ptr, len);
3823            let anti = std::slice::from_raw_parts_mut(out_anti_ptr, len);
3824            let input = DamianiVolatmeterInput::from_slice(data, params);
3825            damiani_volatmeter_into_slice(vol, anti, &input, Kernel::Auto)
3826                .map_err(|e| JsValue::from_str(&e.to_string()))
3827        }
3828    }
3829}
3830
3831#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3832#[derive(Serialize, Deserialize)]
3833pub struct DamianiBatchJsOutput {
3834    pub vol: Vec<f64>,
3835    pub anti: Vec<f64>,
3836    pub combos: Vec<DamianiVolatmeterParams>,
3837    pub rows: usize,
3838    pub cols: usize,
3839}
3840
3841#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3842#[wasm_bindgen(js_name = damiani_volatmeter_batch)]
3843pub fn damiani_volatmeter_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
3844    let cfg: DamianiVolatmeterBatchRange = serde_wasm_bindgen::from_value(config)
3845        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
3846    let out = damiani_volatmeter_batch_inner(data, &cfg, detect_best_kernel(), false)
3847        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3848    serde_wasm_bindgen::to_value(&DamianiBatchJsOutput {
3849        vol: out.vol,
3850        anti: out.anti,
3851        combos: out.combos,
3852        rows: out.rows,
3853        cols: out.cols,
3854    })
3855    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
3856}
3857
3858#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3859#[wasm_bindgen]
3860pub fn damiani_volatmeter_batch_into(
3861    in_ptr: *const f64,
3862    vol_ptr: *mut f64,
3863    anti_ptr: *mut f64,
3864    len: usize,
3865    vis_atr_start: usize,
3866    vis_atr_end: usize,
3867    vis_atr_step: usize,
3868    vis_std_start: usize,
3869    vis_std_end: usize,
3870    vis_std_step: usize,
3871    sed_atr_start: usize,
3872    sed_atr_end: usize,
3873    sed_atr_step: usize,
3874    sed_std_start: usize,
3875    sed_std_end: usize,
3876    sed_std_step: usize,
3877    threshold_start: f64,
3878    threshold_end: f64,
3879    threshold_step: f64,
3880) -> Result<usize, JsValue> {
3881    if in_ptr.is_null() || vol_ptr.is_null() || anti_ptr.is_null() {
3882        return Err(JsValue::from_str("Null pointer provided"));
3883    }
3884
3885    unsafe {
3886        let data = std::slice::from_raw_parts(in_ptr, len);
3887
3888        let sweep = DamianiVolatmeterBatchRange {
3889            vis_atr: (vis_atr_start, vis_atr_end, vis_atr_step),
3890            vis_std: (vis_std_start, vis_std_end, vis_std_step),
3891            sed_atr: (sed_atr_start, sed_atr_end, sed_atr_step),
3892            sed_std: (sed_std_start, sed_std_end, sed_std_step),
3893            threshold: (threshold_start, threshold_end, threshold_step),
3894        };
3895
3896        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
3897        let rows = combos.len();
3898        let cols = len;
3899
3900        if vol_ptr == anti_ptr {
3901            return Err(JsValue::from_str("vol_ptr and anti_ptr cannot be the same"));
3902        }
3903
3904        if in_ptr == vol_ptr || in_ptr == anti_ptr {
3905            let result = damiani_volatmeter_batch_inner(data, &sweep, detect_best_kernel(), false)
3906                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3907
3908            let vol_out = std::slice::from_raw_parts_mut(vol_ptr, rows * cols);
3909            let anti_out = std::slice::from_raw_parts_mut(anti_ptr, rows * cols);
3910
3911            vol_out.copy_from_slice(&result.vol);
3912            anti_out.copy_from_slice(&result.anti);
3913        } else {
3914            let vol_out = std::slice::from_raw_parts_mut(vol_ptr, rows * cols);
3915            let anti_out = std::slice::from_raw_parts_mut(anti_ptr, rows * cols);
3916
3917            damiani_volatmeter_batch_inner_into(
3918                data,
3919                &sweep,
3920                detect_best_kernel(),
3921                false,
3922                vol_out,
3923                anti_out,
3924            )
3925            .map_err(|e| JsValue::from_str(&e.to_string()))?;
3926        }
3927
3928        Ok(rows)
3929    }
3930}