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, ¶ms)
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}