1use crate::utilities::data_loader::{source_type, Candles};
2use crate::utilities::enums::Kernel;
3#[cfg(target_arch = "wasm32")]
4use crate::utilities::helpers::detect_wasm_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};
9use aligned_vec::{AVec, CACHELINE_ALIGN};
10#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
11use core::arch::x86_64::*;
12#[cfg(not(target_arch = "wasm32"))]
13use rayon::prelude::*;
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use serde::{Deserialize, Serialize};
16use std::collections::VecDeque;
17use thiserror::Error;
18#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
19use wasm_bindgen::prelude::*;
20
21#[derive(Debug, Clone)]
22pub enum DonchianData<'a> {
23 Candles { candles: &'a Candles },
24 Slices { high: &'a [f64], low: &'a [f64] },
25}
26
27#[derive(Debug, Clone)]
28pub struct DonchianOutput {
29 pub upperband: Vec<f64>,
30 pub middleband: Vec<f64>,
31 pub lowerband: Vec<f64>,
32}
33
34#[derive(Debug, Clone)]
35pub struct DonchianParams {
36 pub period: Option<usize>,
37}
38
39impl Default for DonchianParams {
40 fn default() -> Self {
41 Self { period: Some(20) }
42 }
43}
44
45#[derive(Debug, Clone)]
46pub struct DonchianInput<'a> {
47 pub data: DonchianData<'a>,
48 pub params: DonchianParams,
49}
50
51impl<'a> DonchianInput<'a> {
52 #[inline]
53 pub fn from_candles(candles: &'a Candles, params: DonchianParams) -> Self {
54 Self {
55 data: DonchianData::Candles { candles },
56 params,
57 }
58 }
59 #[inline]
60 pub fn from_slices(high: &'a [f64], low: &'a [f64], params: DonchianParams) -> Self {
61 Self {
62 data: DonchianData::Slices { high, low },
63 params,
64 }
65 }
66 #[inline]
67 pub fn with_default_candles(candles: &'a Candles) -> Self {
68 Self::from_candles(candles, DonchianParams::default())
69 }
70 #[inline]
71 pub fn get_period(&self) -> usize {
72 self.params.period.unwrap_or(20)
73 }
74}
75
76#[derive(Copy, Clone, Debug)]
77pub struct DonchianBuilder {
78 period: Option<usize>,
79 kernel: Kernel,
80}
81
82impl Default for DonchianBuilder {
83 fn default() -> Self {
84 Self {
85 period: None,
86 kernel: Kernel::Auto,
87 }
88 }
89}
90
91impl DonchianBuilder {
92 #[inline(always)]
93 pub fn new() -> Self {
94 Self::default()
95 }
96 #[inline(always)]
97 pub fn period(mut self, n: usize) -> Self {
98 self.period = Some(n);
99 self
100 }
101 #[inline(always)]
102 pub fn kernel(mut self, k: Kernel) -> Self {
103 self.kernel = k;
104 self
105 }
106 #[inline(always)]
107 pub fn apply(self, c: &Candles) -> Result<DonchianOutput, DonchianError> {
108 let p = DonchianParams {
109 period: self.period,
110 };
111 let i = DonchianInput::from_candles(c, p);
112 donchian_with_kernel(&i, self.kernel)
113 }
114 #[inline(always)]
115 pub fn apply_slices(self, high: &[f64], low: &[f64]) -> Result<DonchianOutput, DonchianError> {
116 let p = DonchianParams {
117 period: self.period,
118 };
119 let i = DonchianInput::from_slices(high, low, p);
120 donchian_with_kernel(&i, self.kernel)
121 }
122 #[inline(always)]
123 pub fn into_stream(self) -> Result<DonchianStream, DonchianError> {
124 let p = DonchianParams {
125 period: self.period,
126 };
127 DonchianStream::try_new(p)
128 }
129}
130
131#[derive(Debug, Error)]
132pub enum DonchianError {
133 #[error("donchian: Empty data provided.")]
134 EmptyInputData,
135 #[error("donchian: Invalid period: period = {period}, data length = {data_len}")]
136 InvalidPeriod { period: usize, data_len: usize },
137 #[error("donchian: Not enough valid data: needed = {needed}, valid = {valid}")]
138 NotEnoughValidData { needed: usize, valid: usize },
139 #[error("donchian: All values are NaN.")]
140 AllValuesNaN,
141 #[error("donchian: High/Low data slices have different lengths.")]
142 MismatchedLength,
143 #[error("donchian: Output length mismatch: expected={expected}, got={got}")]
144 OutputLengthMismatch { expected: usize, got: usize },
145 #[error("donchian: invalid range expansion: start={start} end={end} step={step}")]
146 InvalidRange {
147 start: usize,
148 end: usize,
149 step: usize,
150 },
151 #[error("donchian: invalid input: {0}")]
152 InvalidInput(String),
153 #[error("donchian: Invalid kernel for batch: {0:?}")]
154 InvalidKernelForBatch(Kernel),
155}
156
157#[inline]
158pub fn donchian(input: &DonchianInput) -> Result<DonchianOutput, DonchianError> {
159 donchian_with_kernel(input, Kernel::Auto)
160}
161
162pub fn donchian_with_kernel(
163 input: &DonchianInput,
164 kernel: Kernel,
165) -> Result<DonchianOutput, DonchianError> {
166 let (high, low): (&[f64], &[f64]) = match &input.data {
167 DonchianData::Candles { candles } => {
168 let high = source_type(candles, "high");
169 let low = source_type(candles, "low");
170 (high, low)
171 }
172 DonchianData::Slices { high, low } => (high, low),
173 };
174
175 if high.is_empty() || low.is_empty() {
176 return Err(DonchianError::EmptyInputData);
177 }
178 if high.len() != low.len() {
179 return Err(DonchianError::MismatchedLength);
180 }
181
182 let first_valid_high = high.iter().position(|&x| !x.is_nan());
183 let first_valid_low = low.iter().position(|&x| !x.is_nan());
184 let first_valid_idx = match (first_valid_high, first_valid_low) {
185 (Some(h), Some(l)) => h.max(l),
186 _ => return Err(DonchianError::AllValuesNaN),
187 };
188
189 let len = high.len();
190 let period = input.get_period();
191
192 if period == 0 || period > len {
193 return Err(DonchianError::InvalidPeriod {
194 period,
195 data_len: len,
196 });
197 }
198 if (len - first_valid_idx) < period {
199 return Err(DonchianError::NotEnoughValidData {
200 needed: period,
201 valid: len - first_valid_idx,
202 });
203 }
204
205 let chosen = match kernel {
206 Kernel::Auto => Kernel::Scalar,
207 k => k,
208 };
209
210 let warmup_period = first_valid_idx + period - 1;
211 let mut upperband = alloc_with_nan_prefix(len, warmup_period);
212 let mut middleband = alloc_with_nan_prefix(len, warmup_period);
213 let mut lowerband = alloc_with_nan_prefix(len, warmup_period);
214
215 unsafe {
216 match chosen {
217 Kernel::Scalar | Kernel::ScalarBatch => donchian_scalar(
218 high,
219 low,
220 period,
221 first_valid_idx,
222 &mut upperband,
223 &mut middleband,
224 &mut lowerband,
225 ),
226 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
227 Kernel::Avx2 | Kernel::Avx2Batch => donchian_avx2(
228 high,
229 low,
230 period,
231 first_valid_idx,
232 &mut upperband,
233 &mut middleband,
234 &mut lowerband,
235 ),
236 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
237 Kernel::Avx2 | Kernel::Avx2Batch => donchian_scalar(
238 high,
239 low,
240 period,
241 first_valid_idx,
242 &mut upperband,
243 &mut middleband,
244 &mut lowerband,
245 ),
246 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
247 Kernel::Avx512 | Kernel::Avx512Batch => donchian_avx512(
248 high,
249 low,
250 period,
251 first_valid_idx,
252 &mut upperband,
253 &mut middleband,
254 &mut lowerband,
255 ),
256 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
257 Kernel::Avx512 | Kernel::Avx512Batch => donchian_scalar(
258 high,
259 low,
260 period,
261 first_valid_idx,
262 &mut upperband,
263 &mut middleband,
264 &mut lowerband,
265 ),
266 _ => unreachable!(),
267 }
268 }
269
270 Ok(DonchianOutput {
271 upperband,
272 middleband,
273 lowerband,
274 })
275}
276
277#[inline]
278pub fn donchian_scalar(
279 high: &[f64],
280 low: &[f64],
281 period: usize,
282 first_valid: usize,
283 upper: &mut [f64],
284 middle: &mut [f64],
285 lower: &mut [f64],
286) {
287 let n = high.len();
288 if n == 0 || period == 0 {
289 return;
290 }
291 debug_assert_eq!(low.len(), n);
292 debug_assert_eq!(upper.len(), n);
293 debug_assert_eq!(middle.len(), n);
294 debug_assert_eq!(lower.len(), n);
295
296 let warmup = first_valid + period - 1;
297
298 if period == 1 {
299 let start = warmup;
300 unsafe {
301 let hp = high.as_ptr();
302 let lp = low.as_ptr();
303 let up = upper.as_mut_ptr();
304 let mp = middle.as_mut_ptr();
305 let lw = lower.as_mut_ptr();
306 for i in start..n {
307 let h = *hp.add(i);
308 let l = *lp.add(i);
309 if h.is_nan() || l.is_nan() {
310 *up.add(i) = f64::NAN;
311 *lw.add(i) = f64::NAN;
312 *mp.add(i) = f64::NAN;
313 } else {
314 *up.add(i) = h;
315 *lw.add(i) = l;
316 *mp.add(i) = (h - l).mul_add(0.5, l);
317 }
318 }
319 }
320 return;
321 }
322
323 if period <= 32 {
324 unsafe {
325 let hp = high.as_ptr();
326 let lp = low.as_ptr();
327 let up = upper.as_mut_ptr();
328 let mp = middle.as_mut_ptr();
329 let lw = lower.as_mut_ptr();
330 for i in warmup..n {
331 let start = i + 1 - period;
332 let mut maxv = f64::NEG_INFINITY;
333 let mut minv = f64::INFINITY;
334 let mut has_nan = false;
335 for k in 0..period {
336 let h = *hp.add(start + k);
337 let l = *lp.add(start + k);
338 if h.is_nan() || l.is_nan() {
339 has_nan = true;
340 break;
341 }
342 if h > maxv {
343 maxv = h;
344 }
345 if l < minv {
346 minv = l;
347 }
348 }
349 if has_nan {
350 *up.add(i) = f64::NAN;
351 *lw.add(i) = f64::NAN;
352 *mp.add(i) = f64::NAN;
353 } else {
354 *up.add(i) = maxv;
355 *lw.add(i) = minv;
356 *mp.add(i) = (maxv - minv).mul_add(0.5, minv);
357 }
358 }
359 }
360 return;
361 }
362
363 if period <= 32 {
364 unsafe {
365 let hp = high.as_ptr();
366 let lp = low.as_ptr();
367 let up = upper.as_mut_ptr();
368 let mp = middle.as_mut_ptr();
369 let lw = lower.as_mut_ptr();
370 for i in warmup..n {
371 let start = i + 1 - period;
372 let mut maxv = f64::NEG_INFINITY;
373 let mut minv = f64::INFINITY;
374 let mut has_nan = false;
375 for k in 0..period {
376 let h = *hp.add(start + k);
377 let l = *lp.add(start + k);
378 if h.is_nan() || l.is_nan() {
379 has_nan = true;
380 break;
381 }
382 if h > maxv {
383 maxv = h;
384 }
385 if l < minv {
386 minv = l;
387 }
388 }
389 if has_nan {
390 *up.add(i) = f64::NAN;
391 *lw.add(i) = f64::NAN;
392 *mp.add(i) = f64::NAN;
393 } else {
394 *up.add(i) = maxv;
395 *lw.add(i) = minv;
396 *mp.add(i) = (maxv - minv).mul_add(0.5, minv);
397 }
398 }
399 }
400 return;
401 }
402
403 let mut g_max = AVec::<f64>::with_capacity(CACHELINE_ALIGN, n);
404 let mut g_min = AVec::<f64>::with_capacity(CACHELINE_ALIGN, n);
405 let mut valid: Vec<u8> = Vec::with_capacity(n);
406 unsafe {
407 g_max.set_len(n);
408 g_min.set_len(n);
409 valid.set_len(n);
410 let hp = high.as_ptr();
411 let lp = low.as_ptr();
412 let gp_max = g_max.as_mut_ptr();
413 let gp_min = g_min.as_mut_ptr();
414 let vp = valid.as_mut_ptr();
415
416 let mut acc_max = f64::NEG_INFINITY;
417 let mut acc_min = f64::INFINITY;
418 let mut k: usize = 0;
419
420 for i in 0..n {
421 let h = *hp.add(i);
422 let l = *lp.add(i);
423 let ok = h.is_finite() & l.is_finite();
424 *vp.add(i) = ok as u8;
425 let hv = if ok { h } else { f64::NEG_INFINITY };
426 let lv = if ok { l } else { f64::INFINITY };
427 if k == 0 {
428 acc_max = hv;
429 acc_min = lv;
430 } else {
431 if hv > acc_max {
432 acc_max = hv;
433 }
434 if lv < acc_min {
435 acc_min = lv;
436 }
437 }
438 *gp_max.add(i) = acc_max;
439 *gp_min.add(i) = acc_min;
440 k += 1;
441 if k == period {
442 k = 0;
443 }
444 }
445 }
446
447 let mut ps: Vec<u32> = Vec::with_capacity(n + 1);
448 unsafe {
449 ps.set_len(n + 1);
450 let psp = ps.as_mut_ptr();
451 let vp = valid.as_ptr();
452 *psp.add(0) = 0;
453 for i in 0..n {
454 let prev = *psp.add(i);
455 let add = *vp.add(i) as u32;
456 *psp.add(i + 1) = prev + add;
457 }
458 }
459
460 unsafe {
461 let hp = high.as_ptr();
462 let lp = low.as_ptr();
463 let up = upper.as_mut_ptr();
464 let mp = middle.as_mut_ptr();
465 let lw = lower.as_mut_ptr();
466 let gp_max = g_max.as_ptr();
467 let gp_min = g_min.as_ptr();
468 let psp = ps.as_ptr();
469
470 let mut acc_max = f64::NEG_INFINITY;
471 let mut acc_min = f64::INFINITY;
472
473 for j in (0..n).rev() {
474 let h = *hp.add(j);
475 let l = *lp.add(j);
476 let ok = h.is_finite() & l.is_finite();
477 let hv = if ok { h } else { f64::NEG_INFINITY };
478 let lv = if ok { l } else { f64::INFINITY };
479
480 if j == n - 1 || ((j + 1) % period) == 0 {
481 acc_max = hv;
482 acc_min = lv;
483 } else {
484 if hv > acc_max {
485 acc_max = hv;
486 }
487 if lv < acc_min {
488 acc_min = lv;
489 }
490 }
491
492 let i = j + period - 1;
493 if i >= n || i < warmup {
494 continue;
495 }
496
497 let all_valid = {
498 let vcnt = *psp.add(i + 1) - *psp.add(i + 1 - period);
499 vcnt == period as u32
500 };
501 if all_valid {
502 let gm = *gp_max.add(i);
503 let gn = *gp_min.add(i);
504 let maxv = if acc_max > gm { acc_max } else { gm };
505 let minv = if acc_min < gn { acc_min } else { gn };
506 *up.add(i) = maxv;
507 *lw.add(i) = minv;
508 *mp.add(i) = (maxv - minv).mul_add(0.5, minv);
509 } else {
510 *up.add(i) = f64::NAN;
511 *lw.add(i) = f64::NAN;
512 *mp.add(i) = f64::NAN;
513 }
514 }
515 }
516}
517
518#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
519#[inline]
520pub fn donchian_avx512(
521 high: &[f64],
522 low: &[f64],
523 period: usize,
524 first_valid: usize,
525 upper: &mut [f64],
526 middle: &mut [f64],
527 lower: &mut [f64],
528) {
529 donchian_scalar(high, low, period, first_valid, upper, middle, lower)
530}
531
532#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
533#[inline]
534pub fn donchian_avx2(
535 high: &[f64],
536 low: &[f64],
537 period: usize,
538 first_valid: usize,
539 upper: &mut [f64],
540 middle: &mut [f64],
541 lower: &mut [f64],
542) {
543 donchian_scalar(high, low, period, first_valid, upper, middle, lower)
544}
545
546pub fn donchian_into_slice(
547 upper_dst: &mut [f64],
548 middle_dst: &mut [f64],
549 lower_dst: &mut [f64],
550 input: &DonchianInput,
551 kern: Kernel,
552) -> Result<(), DonchianError> {
553 let (high, low): (&[f64], &[f64]) = match &input.data {
554 DonchianData::Candles { candles } => {
555 let high = source_type(candles, "high");
556 let low = source_type(candles, "low");
557 (high, low)
558 }
559 DonchianData::Slices { high, low } => (high, low),
560 };
561
562 if high.is_empty() || low.is_empty() {
563 return Err(DonchianError::EmptyInputData);
564 }
565 if high.len() != low.len() {
566 return Err(DonchianError::MismatchedLength);
567 }
568 if upper_dst.len() != high.len()
569 || middle_dst.len() != high.len()
570 || lower_dst.len() != high.len()
571 {
572 return Err(DonchianError::OutputLengthMismatch {
573 expected: high.len(),
574 got: upper_dst.len().max(middle_dst.len()).max(lower_dst.len()),
575 });
576 }
577
578 let first_valid_high = high.iter().position(|&x| !x.is_nan());
579 let first_valid_low = low.iter().position(|&x| !x.is_nan());
580 let first_valid_idx = match (first_valid_high, first_valid_low) {
581 (Some(h), Some(l)) => h.max(l),
582 _ => return Err(DonchianError::AllValuesNaN),
583 };
584
585 let period = input.get_period();
586 if period == 0 || period > high.len() {
587 return Err(DonchianError::InvalidPeriod {
588 period,
589 data_len: high.len(),
590 });
591 }
592 if (high.len() - first_valid_idx) < period {
593 return Err(DonchianError::NotEnoughValidData {
594 needed: period,
595 valid: high.len() - first_valid_idx,
596 });
597 }
598
599 let chosen = match kern {
600 #[cfg(target_arch = "wasm32")]
601 Kernel::Auto => Kernel::Scalar,
602 #[cfg(not(target_arch = "wasm32"))]
603 Kernel::Auto => Kernel::Scalar,
604 k => k,
605 };
606
607 let warmup_period = first_valid_idx + period - 1;
608
609 for i in 0..warmup_period {
610 upper_dst[i] = f64::NAN;
611 middle_dst[i] = f64::NAN;
612 lower_dst[i] = f64::NAN;
613 }
614
615 unsafe {
616 match chosen {
617 Kernel::Scalar | Kernel::ScalarBatch => donchian_scalar(
618 high,
619 low,
620 period,
621 first_valid_idx,
622 upper_dst,
623 middle_dst,
624 lower_dst,
625 ),
626 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
627 Kernel::Avx2 | Kernel::Avx2Batch => donchian_avx2(
628 high,
629 low,
630 period,
631 first_valid_idx,
632 upper_dst,
633 middle_dst,
634 lower_dst,
635 ),
636 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
637 Kernel::Avx2 | Kernel::Avx2Batch => donchian_scalar(
638 high,
639 low,
640 period,
641 first_valid_idx,
642 upper_dst,
643 middle_dst,
644 lower_dst,
645 ),
646 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
647 Kernel::Avx512 | Kernel::Avx512Batch => donchian_avx512(
648 high,
649 low,
650 period,
651 first_valid_idx,
652 upper_dst,
653 middle_dst,
654 lower_dst,
655 ),
656 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
657 Kernel::Avx512 | Kernel::Avx512Batch => donchian_scalar(
658 high,
659 low,
660 period,
661 first_valid_idx,
662 upper_dst,
663 middle_dst,
664 lower_dst,
665 ),
666 _ => unreachable!(),
667 }
668 }
669
670 Ok(())
671}
672
673#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
674#[inline]
675pub fn donchian_into(
676 input: &DonchianInput,
677 upper: &mut [f64],
678 middle: &mut [f64],
679 lower: &mut [f64],
680) -> Result<(), DonchianError> {
681 donchian_into_slice(upper, middle, lower, input, Kernel::Auto)
682}
683
684#[inline(always)]
685pub fn donchian_batch_with_kernel(
686 high: &[f64],
687 low: &[f64],
688 sweep: &DonchianBatchRange,
689 k: Kernel,
690) -> Result<DonchianBatchOutput, DonchianError> {
691 let kernel = match k {
692 Kernel::Auto => detect_best_batch_kernel(),
693 other if other.is_batch() => other,
694 other => return Err(DonchianError::InvalidKernelForBatch(other)),
695 };
696 let simd = match kernel {
697 Kernel::Avx512Batch => Kernel::Avx512,
698 Kernel::Avx2Batch => Kernel::Avx2,
699 Kernel::ScalarBatch => Kernel::Scalar,
700 _ => unreachable!(),
701 };
702 donchian_batch_par_slice(high, low, sweep, simd)
703}
704
705#[derive(Clone, Debug)]
706pub struct DonchianBatchRange {
707 pub period: (usize, usize, usize),
708}
709
710impl Default for DonchianBatchRange {
711 fn default() -> Self {
712 Self {
713 period: (20, 269, 1),
714 }
715 }
716}
717
718#[derive(Clone, Debug, Default)]
719pub struct DonchianBatchBuilder {
720 range: DonchianBatchRange,
721 kernel: Kernel,
722}
723
724impl DonchianBatchBuilder {
725 pub fn new() -> Self {
726 Self::default()
727 }
728 pub fn kernel(mut self, k: Kernel) -> Self {
729 self.kernel = k;
730 self
731 }
732 pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
733 self.range.period = (start, end, step);
734 self
735 }
736 pub fn period_static(mut self, p: usize) -> Self {
737 self.range.period = (p, p, 0);
738 self
739 }
740 pub fn apply_slices(
741 self,
742 high: &[f64],
743 low: &[f64],
744 ) -> Result<DonchianBatchOutput, DonchianError> {
745 donchian_batch_with_kernel(high, low, &self.range, self.kernel)
746 }
747 pub fn with_default_slices(
748 high: &[f64],
749 low: &[f64],
750 k: Kernel,
751 ) -> Result<DonchianBatchOutput, DonchianError> {
752 DonchianBatchBuilder::new()
753 .kernel(k)
754 .apply_slices(high, low)
755 }
756 pub fn apply_candles(self, c: &Candles) -> Result<DonchianBatchOutput, DonchianError> {
757 let high = source_type(c, "high");
758 let low = source_type(c, "low");
759 self.apply_slices(high, low)
760 }
761 pub fn with_default_candles(c: &Candles) -> Result<DonchianBatchOutput, DonchianError> {
762 DonchianBatchBuilder::new()
763 .kernel(Kernel::Auto)
764 .apply_candles(c)
765 }
766}
767
768#[derive(Clone, Debug)]
769pub struct DonchianBatchOutput {
770 pub upper: Vec<f64>,
771 pub middle: Vec<f64>,
772 pub lower: Vec<f64>,
773 pub combos: Vec<DonchianParams>,
774 pub rows: usize,
775 pub cols: usize,
776}
777
778impl DonchianBatchOutput {
779 pub fn row_for_params(&self, p: &DonchianParams) -> Option<usize> {
780 self.combos
781 .iter()
782 .position(|c| c.period.unwrap_or(20) == p.period.unwrap_or(20))
783 }
784 pub fn upper_for(&self, p: &DonchianParams) -> Option<&[f64]> {
785 self.row_for_params(p)
786 .map(|row| &self.upper[row * self.cols..][..self.cols])
787 }
788 pub fn middle_for(&self, p: &DonchianParams) -> Option<&[f64]> {
789 self.row_for_params(p)
790 .map(|row| &self.middle[row * self.cols..][..self.cols])
791 }
792 pub fn lower_for(&self, p: &DonchianParams) -> Option<&[f64]> {
793 self.row_for_params(p)
794 .map(|row| &self.lower[row * self.cols..][..self.cols])
795 }
796}
797
798#[inline(always)]
799pub fn expand_grid(r: &DonchianBatchRange) -> Result<Vec<DonchianParams>, DonchianError> {
800 fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, DonchianError> {
801 if step == 0 || start == end {
802 return Ok(vec![start]);
803 }
804 if start < end {
805 Ok((start..=end).step_by(step).collect())
806 } else {
807 let mut v = Vec::new();
808 let mut cur = start;
809 while cur >= end {
810 v.push(cur);
811 if let Some(next) = cur.checked_sub(step) {
812 cur = next;
813 } else {
814 break;
815 }
816 if cur == usize::MAX {
817 break;
818 }
819 }
820 if v.is_empty() {
821 return Err(DonchianError::InvalidRange { start, end, step });
822 }
823 Ok(v)
824 }
825 }
826 let periods = axis_usize(r.period)?;
827 Ok(periods
828 .into_iter()
829 .map(|p| DonchianParams { period: Some(p) })
830 .collect())
831}
832
833#[inline(always)]
834pub fn donchian_batch_slice(
835 high: &[f64],
836 low: &[f64],
837 sweep: &DonchianBatchRange,
838 kern: Kernel,
839) -> Result<DonchianBatchOutput, DonchianError> {
840 donchian_batch_inner(high, low, sweep, kern, false)
841}
842
843#[inline(always)]
844pub fn donchian_batch_par_slice(
845 high: &[f64],
846 low: &[f64],
847 sweep: &DonchianBatchRange,
848 kern: Kernel,
849) -> Result<DonchianBatchOutput, DonchianError> {
850 donchian_batch_inner(high, low, sweep, kern, true)
851}
852
853#[inline(always)]
854fn donchian_batch_inner(
855 high: &[f64],
856 low: &[f64],
857 sweep: &DonchianBatchRange,
858 kern: Kernel,
859 parallel: bool,
860) -> Result<DonchianBatchOutput, DonchianError> {
861 let combos = expand_grid(sweep)?;
862 if combos.is_empty() {
863 return Err(DonchianError::InvalidRange {
864 start: sweep.period.0,
865 end: sweep.period.1,
866 step: sweep.period.2,
867 });
868 }
869 if high.len() != low.len() {
870 return Err(DonchianError::MismatchedLength);
871 }
872 let first = high
873 .iter()
874 .position(|x| !x.is_nan())
875 .zip(low.iter().position(|x| !x.is_nan()))
876 .map(|(a, b)| a.max(b));
877 let first = match first {
878 Some(idx) => idx,
879 None => return Err(DonchianError::AllValuesNaN),
880 };
881 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
882 if high.len() - first < max_p {
883 return Err(DonchianError::NotEnoughValidData {
884 needed: max_p,
885 valid: high.len() - first,
886 });
887 }
888 let rows = combos.len();
889 let cols = high.len();
890 let _size = rows
891 .checked_mul(cols)
892 .ok_or_else(|| DonchianError::InvalidInput("rows*cols overflow".into()))?;
893
894 let warmup_periods: Vec<usize> = combos
895 .iter()
896 .map(|c| first + c.period.unwrap() - 1)
897 .collect();
898
899 let mut upper_mu = make_uninit_matrix(rows, cols);
900 let mut middle_mu = make_uninit_matrix(rows, cols);
901 let mut lower_mu = make_uninit_matrix(rows, cols);
902
903 init_matrix_prefixes(&mut upper_mu, cols, &warmup_periods);
904 init_matrix_prefixes(&mut middle_mu, cols, &warmup_periods);
905 init_matrix_prefixes(&mut lower_mu, cols, &warmup_periods);
906
907 let mut upper_guard = core::mem::ManuallyDrop::new(upper_mu);
908 let mut middle_guard = core::mem::ManuallyDrop::new(middle_mu);
909 let mut lower_guard = core::mem::ManuallyDrop::new(lower_mu);
910
911 let upper: &mut [f64] = unsafe {
912 core::slice::from_raw_parts_mut(upper_guard.as_mut_ptr() as *mut f64, upper_guard.len())
913 };
914 let middle: &mut [f64] = unsafe {
915 core::slice::from_raw_parts_mut(middle_guard.as_mut_ptr() as *mut f64, middle_guard.len())
916 };
917 let lower: &mut [f64] = unsafe {
918 core::slice::from_raw_parts_mut(lower_guard.as_mut_ptr() as *mut f64, lower_guard.len())
919 };
920
921 let do_row =
922 |row: usize, out_upper: &mut [f64], out_middle: &mut [f64], out_lower: &mut [f64]| unsafe {
923 let period = combos[row].period.unwrap();
924 match kern {
925 Kernel::Scalar => {
926 donchian_row_scalar(high, low, first, period, out_upper, out_middle, out_lower)
927 }
928 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
929 Kernel::Avx2 => {
930 donchian_row_avx2(high, low, first, period, out_upper, out_middle, out_lower)
931 }
932 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
933 Kernel::Avx512 => {
934 donchian_row_avx512(high, low, first, period, out_upper, out_middle, out_lower)
935 }
936 _ => unreachable!(),
937 }
938 };
939
940 if parallel {
941 #[cfg(not(target_arch = "wasm32"))]
942 {
943 upper
944 .par_chunks_mut(cols)
945 .zip(middle.par_chunks_mut(cols))
946 .zip(lower.par_chunks_mut(cols))
947 .enumerate()
948 .for_each(|(row, ((upper, middle), lower))| do_row(row, upper, middle, lower));
949 }
950
951 #[cfg(target_arch = "wasm32")]
952 {
953 for (((upper, middle), lower), row) in upper
954 .chunks_mut(cols)
955 .zip(middle.chunks_mut(cols))
956 .zip(lower.chunks_mut(cols))
957 .zip(0..)
958 {
959 do_row(row, upper, middle, lower);
960 }
961 }
962 } else {
963 for (((upper, middle), lower), row) in upper
964 .chunks_mut(cols)
965 .zip(middle.chunks_mut(cols))
966 .zip(lower.chunks_mut(cols))
967 .zip(0..)
968 {
969 do_row(row, upper, middle, lower);
970 }
971 }
972
973 let upper = unsafe {
974 Vec::from_raw_parts(
975 upper_guard.as_mut_ptr() as *mut f64,
976 upper_guard.len(),
977 upper_guard.capacity(),
978 )
979 };
980 let middle = unsafe {
981 Vec::from_raw_parts(
982 middle_guard.as_mut_ptr() as *mut f64,
983 middle_guard.len(),
984 middle_guard.capacity(),
985 )
986 };
987 let lower = unsafe {
988 Vec::from_raw_parts(
989 lower_guard.as_mut_ptr() as *mut f64,
990 lower_guard.len(),
991 lower_guard.capacity(),
992 )
993 };
994
995 Ok(DonchianBatchOutput {
996 upper,
997 middle,
998 lower,
999 combos,
1000 rows,
1001 cols,
1002 })
1003}
1004
1005#[inline(always)]
1006unsafe fn donchian_row_scalar(
1007 high: &[f64],
1008 low: &[f64],
1009 first: usize,
1010 period: usize,
1011 upper: &mut [f64],
1012 middle: &mut [f64],
1013 lower: &mut [f64],
1014) {
1015 let n = high.len();
1016 if n == 0 || period == 0 {
1017 return;
1018 }
1019 let warmup = first + period - 1;
1020
1021 if period == 1 {
1022 let hp = high.as_ptr();
1023 let lp = low.as_ptr();
1024 let up = upper.as_mut_ptr();
1025 let mp = middle.as_mut_ptr();
1026 let lw = lower.as_mut_ptr();
1027 for i in warmup..n {
1028 let h = *hp.add(i);
1029 let l = *lp.add(i);
1030 if h.is_nan() || l.is_nan() {
1031 *up.add(i) = f64::NAN;
1032 *lw.add(i) = f64::NAN;
1033 *mp.add(i) = f64::NAN;
1034 } else {
1035 *up.add(i) = h;
1036 *lw.add(i) = l;
1037 *mp.add(i) = (h - l).mul_add(0.5, l);
1038 }
1039 }
1040 return;
1041 }
1042
1043 if period <= 32 {
1044 let hp = high.as_ptr();
1045 let lp = low.as_ptr();
1046 let up = upper.as_mut_ptr();
1047 let mp = middle.as_mut_ptr();
1048 let lw = lower.as_mut_ptr();
1049 for i in warmup..n {
1050 let start = i + 1 - period;
1051 let mut maxv = f64::NEG_INFINITY;
1052 let mut minv = f64::INFINITY;
1053 let mut has_nan = false;
1054 for k in 0..period {
1055 let h = *hp.add(start + k);
1056 let l = *lp.add(start + k);
1057 if h.is_nan() || l.is_nan() {
1058 has_nan = true;
1059 break;
1060 }
1061 if h > maxv {
1062 maxv = h;
1063 }
1064 if l < minv {
1065 minv = l;
1066 }
1067 }
1068 if has_nan {
1069 *up.add(i) = f64::NAN;
1070 *lw.add(i) = f64::NAN;
1071 *mp.add(i) = f64::NAN;
1072 } else {
1073 *up.add(i) = maxv;
1074 *lw.add(i) = minv;
1075 *mp.add(i) = (maxv - minv).mul_add(0.5, minv);
1076 }
1077 }
1078 return;
1079 }
1080
1081 let mut g_max = AVec::<f64>::with_capacity(CACHELINE_ALIGN, n);
1082 let mut g_min = AVec::<f64>::with_capacity(CACHELINE_ALIGN, n);
1083 let mut valid: Vec<u8> = Vec::with_capacity(n);
1084 g_max.set_len(n);
1085 g_min.set_len(n);
1086 valid.set_len(n);
1087 let hp = high.as_ptr();
1088 let lp = low.as_ptr();
1089 let gp_max = g_max.as_mut_ptr();
1090 let gp_min = g_min.as_mut_ptr();
1091 let vp = valid.as_mut_ptr();
1092
1093 let mut acc_max = f64::NEG_INFINITY;
1094 let mut acc_min = f64::INFINITY;
1095 let mut k: usize = 0;
1096 for i in 0..n {
1097 let h = *hp.add(i);
1098 let l = *lp.add(i);
1099 let ok = h.is_finite() & l.is_finite();
1100 *vp.add(i) = ok as u8;
1101 let hv = if ok { h } else { f64::NEG_INFINITY };
1102 let lv = if ok { l } else { f64::INFINITY };
1103 if k == 0 {
1104 acc_max = hv;
1105 acc_min = lv;
1106 } else {
1107 if hv > acc_max {
1108 acc_max = hv;
1109 }
1110 if lv < acc_min {
1111 acc_min = lv;
1112 }
1113 }
1114 *gp_max.add(i) = acc_max;
1115 *gp_min.add(i) = acc_min;
1116 k += 1;
1117 if k == period {
1118 k = 0;
1119 }
1120 }
1121
1122 let up = upper.as_mut_ptr();
1123 let mp = middle.as_mut_ptr();
1124 let lw = lower.as_mut_ptr();
1125 let gp_max = g_max.as_ptr();
1126 let gp_min = g_min.as_ptr();
1127 let vp = valid.as_ptr();
1128
1129 acc_max = f64::NEG_INFINITY;
1130 acc_min = f64::INFINITY;
1131 let mut have_vcnt = false;
1132 let mut vcnt: u32 = 0;
1133 for j in (0..n).rev() {
1134 let h = *hp.add(j);
1135 let l = *lp.add(j);
1136 let ok = h.is_finite() & l.is_finite();
1137 let hv = if ok { h } else { f64::NEG_INFINITY };
1138 let lv = if ok { l } else { f64::INFINITY };
1139
1140 if j == n - 1 || ((j + 1) % period) == 0 {
1141 acc_max = hv;
1142 acc_min = lv;
1143 } else {
1144 if hv > acc_max {
1145 acc_max = hv;
1146 }
1147 if lv < acc_min {
1148 acc_min = lv;
1149 }
1150 }
1151
1152 let i = j + period - 1;
1153 if i < n {
1154 if !have_vcnt {
1155 let start = i + 1 - period;
1156 let mut sum: u32 = 0;
1157 for t in start..=i {
1158 sum += *vp.add(t) as u32;
1159 }
1160 vcnt = sum;
1161 have_vcnt = true;
1162 } else {
1163 if i + 1 < n {
1164 vcnt = vcnt - (*vp.add(i + 1) as u32) + (*vp.add(i + 1 - period) as u32);
1165 }
1166 }
1167 }
1168
1169 if i >= n || i < warmup {
1170 continue;
1171 }
1172
1173 let all_valid = vcnt == period as u32;
1174 if all_valid {
1175 let gm = *gp_max.add(i);
1176 let gn = *gp_min.add(i);
1177 let maxv = if acc_max > gm { acc_max } else { gm };
1178 let minv = if acc_min < gn { acc_min } else { gn };
1179 *up.add(i) = maxv;
1180 *lw.add(i) = minv;
1181 *mp.add(i) = (maxv - minv).mul_add(0.5, minv);
1182 } else {
1183 *up.add(i) = f64::NAN;
1184 *lw.add(i) = f64::NAN;
1185 *mp.add(i) = f64::NAN;
1186 }
1187 }
1188}
1189
1190#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1191#[inline(always)]
1192pub unsafe fn donchian_row_avx2(
1193 high: &[f64],
1194 low: &[f64],
1195 first: usize,
1196 period: usize,
1197 upper: &mut [f64],
1198 middle: &mut [f64],
1199 lower: &mut [f64],
1200) {
1201 donchian_row_scalar(high, low, first, period, upper, middle, lower)
1202}
1203
1204#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1205#[inline(always)]
1206pub unsafe fn donchian_row_avx512(
1207 high: &[f64],
1208 low: &[f64],
1209 first: usize,
1210 period: usize,
1211 upper: &mut [f64],
1212 middle: &mut [f64],
1213 lower: &mut [f64],
1214) {
1215 if period <= 32 {
1216 donchian_row_avx512_short(high, low, first, period, upper, middle, lower)
1217 } else {
1218 donchian_row_avx512_long(high, low, first, period, upper, middle, lower)
1219 }
1220}
1221
1222#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1223#[inline(always)]
1224pub unsafe fn donchian_row_avx512_short(
1225 high: &[f64],
1226 low: &[f64],
1227 first: usize,
1228 period: usize,
1229 upper: &mut [f64],
1230 middle: &mut [f64],
1231 lower: &mut [f64],
1232) {
1233 donchian_row_scalar(high, low, first, period, upper, middle, lower)
1234}
1235
1236#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1237#[inline(always)]
1238pub unsafe fn donchian_row_avx512_long(
1239 high: &[f64],
1240 low: &[f64],
1241 first: usize,
1242 period: usize,
1243 upper: &mut [f64],
1244 middle: &mut [f64],
1245 lower: &mut [f64],
1246) {
1247 donchian_row_scalar(high, low, first, period, upper, middle, lower)
1248}
1249
1250#[derive(Debug, Clone)]
1251pub struct DonchianStream {
1252 period: usize,
1253
1254 valid_ring: Vec<u8>,
1255 head: usize,
1256 seen: usize,
1257 valid_count: usize,
1258
1259 max_deque: VecDeque<(f64, usize)>,
1260 min_deque: VecDeque<(f64, usize)>,
1261}
1262
1263impl DonchianStream {
1264 pub fn try_new(params: DonchianParams) -> Result<Self, DonchianError> {
1265 let period = params.period.unwrap_or(20);
1266 if period == 0 {
1267 return Err(DonchianError::InvalidPeriod {
1268 period,
1269 data_len: 0,
1270 });
1271 }
1272 Ok(Self {
1273 period,
1274 valid_ring: vec![0; period],
1275 head: 0,
1276 seen: 0,
1277 valid_count: 0,
1278 max_deque: VecDeque::with_capacity(period),
1279 min_deque: VecDeque::with_capacity(period),
1280 })
1281 }
1282
1283 #[inline(always)]
1284 fn evict_outdated(&mut self, window_start: usize) {
1285 while let Some(&(_, idx)) = self.max_deque.front() {
1286 if idx < window_start {
1287 self.max_deque.pop_front();
1288 } else {
1289 break;
1290 }
1291 }
1292 while let Some(&(_, idx)) = self.min_deque.front() {
1293 if idx < window_start {
1294 self.min_deque.pop_front();
1295 } else {
1296 break;
1297 }
1298 }
1299 }
1300
1301 #[inline(always)]
1302 pub fn update(&mut self, high: f64, low: f64) -> Option<(f64, f64, f64)> {
1303 let ok = high.is_finite() & low.is_finite();
1304
1305 let leaving = self.valid_ring[self.head] as usize;
1306 self.valid_ring[self.head] = ok as u8;
1307 self.head += 1;
1308 if self.head == self.period {
1309 self.head = 0;
1310 }
1311 self.valid_count = self.valid_count + (ok as usize) - leaving;
1312
1313 let t = self.seen;
1314 self.seen = t + 1;
1315 let window_start = self.seen.saturating_sub(self.period);
1316
1317 self.evict_outdated(window_start);
1318
1319 if ok {
1320 while let Some(&(v, _)) = self.max_deque.back() {
1321 if v <= high {
1322 self.max_deque.pop_back();
1323 } else {
1324 break;
1325 }
1326 }
1327 self.max_deque.push_back((high, t));
1328
1329 while let Some(&(v, _)) = self.min_deque.back() {
1330 if v >= low {
1331 self.min_deque.pop_back();
1332 } else {
1333 break;
1334 }
1335 }
1336 self.min_deque.push_back((low, t));
1337 }
1338
1339 if self.seen < self.period {
1340 return None;
1341 }
1342
1343 if self.valid_count != self.period {
1344 return Some((f64::NAN, f64::NAN, f64::NAN));
1345 }
1346
1347 debug_assert!(!self.max_deque.is_empty() && !self.min_deque.is_empty());
1348 let maxv = self.max_deque.front().unwrap().0;
1349 let minv = self.min_deque.front().unwrap().0;
1350
1351 let mid = (maxv - minv).mul_add(0.5, minv);
1352 Some((maxv, mid, minv))
1353 }
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359 use crate::skip_if_unsupported;
1360 use crate::utilities::data_loader::read_candles_from_csv;
1361
1362 fn check_donchian_partial_params(
1363 test_name: &str,
1364 kernel: Kernel,
1365 ) -> Result<(), Box<dyn std::error::Error>> {
1366 skip_if_unsupported!(kernel, test_name);
1367 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1368 let candles = read_candles_from_csv(file_path)?;
1369 let default_params = DonchianParams { period: None };
1370 let input = DonchianInput::from_candles(&candles, default_params);
1371 let output = donchian_with_kernel(&input, kernel)?;
1372 assert_eq!(output.upperband.len(), candles.close.len());
1373 Ok(())
1374 }
1375
1376 fn check_donchian_accuracy(
1377 test_name: &str,
1378 kernel: Kernel,
1379 ) -> Result<(), Box<dyn std::error::Error>> {
1380 skip_if_unsupported!(kernel, test_name);
1381 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1382 let candles = read_candles_from_csv(file_path)?;
1383 let params = DonchianParams { period: Some(20) };
1384 let input = DonchianInput::from_candles(&candles, params);
1385 let result = donchian_with_kernel(&input, kernel)?;
1386 let expected_last_five_upper = [61290.0, 61290.0, 61290.0, 61290.0, 61290.0];
1387 let expected_last_five_middle = [59583.0, 59583.0, 59583.0, 59583.0, 59583.0];
1388 let expected_last_five_lower = [57876.0, 57876.0, 57876.0, 57876.0, 57876.0];
1389 let start = result.upperband.len().saturating_sub(5);
1390 for i in 0..5 {
1391 assert!((result.upperband[start + i] - expected_last_five_upper[i]).abs() < 1e-1);
1392 assert!((result.middleband[start + i] - expected_last_five_middle[i]).abs() < 1e-1);
1393 assert!((result.lowerband[start + i] - expected_last_five_lower[i]).abs() < 1e-1);
1394 }
1395 Ok(())
1396 }
1397
1398 fn check_donchian_zero_period(
1399 test_name: &str,
1400 kernel: Kernel,
1401 ) -> Result<(), Box<dyn std::error::Error>> {
1402 skip_if_unsupported!(kernel, test_name);
1403 let high = [10.0, 20.0, 30.0];
1404 let low = [5.0, 3.0, 2.0];
1405 let params = DonchianParams { period: Some(0) };
1406 let input = DonchianInput::from_slices(&high, &low, params);
1407 let res = donchian_with_kernel(&input, kernel);
1408 assert!(res.is_err());
1409 Ok(())
1410 }
1411
1412 fn check_donchian_period_exceeds_length(
1413 test_name: &str,
1414 kernel: Kernel,
1415 ) -> Result<(), Box<dyn std::error::Error>> {
1416 skip_if_unsupported!(kernel, test_name);
1417 let high = [10.0, 20.0, 30.0];
1418 let low = [5.0, 3.0, 2.0];
1419 let params = DonchianParams { period: Some(10) };
1420 let input = DonchianInput::from_slices(&high, &low, params);
1421 let res = donchian_with_kernel(&input, kernel);
1422 assert!(res.is_err());
1423 Ok(())
1424 }
1425
1426 fn check_donchian_very_small_dataset(
1427 test_name: &str,
1428 kernel: Kernel,
1429 ) -> Result<(), Box<dyn std::error::Error>> {
1430 skip_if_unsupported!(kernel, test_name);
1431 let high = [100.0];
1432 let low = [90.0];
1433 let params = DonchianParams { period: Some(20) };
1434 let input = DonchianInput::from_slices(&high, &low, params);
1435 let res = donchian_with_kernel(&input, kernel);
1436 assert!(res.is_err());
1437 Ok(())
1438 }
1439
1440 fn check_donchian_mismatched_length(
1441 test_name: &str,
1442 kernel: Kernel,
1443 ) -> Result<(), Box<dyn std::error::Error>> {
1444 skip_if_unsupported!(kernel, test_name);
1445 let high = [10.0, 20.0, 30.0];
1446 let low = [5.0, 3.0];
1447 let params = DonchianParams { period: Some(2) };
1448 let input = DonchianInput::from_slices(&high, &low, params);
1449 let res = donchian_with_kernel(&input, kernel);
1450 assert!(res.is_err());
1451 Ok(())
1452 }
1453
1454 fn check_donchian_all_nan_data(
1455 test_name: &str,
1456 kernel: Kernel,
1457 ) -> Result<(), Box<dyn std::error::Error>> {
1458 skip_if_unsupported!(kernel, test_name);
1459 let high = [f64::NAN, f64::NAN];
1460 let low = [f64::NAN, f64::NAN];
1461 let params = DonchianParams { period: Some(2) };
1462 let input = DonchianInput::from_slices(&high, &low, params);
1463 let res = donchian_with_kernel(&input, kernel);
1464 assert!(res.is_err());
1465 Ok(())
1466 }
1467
1468 fn check_donchian_partial_computation(
1469 test_name: &str,
1470 kernel: Kernel,
1471 ) -> Result<(), Box<dyn std::error::Error>> {
1472 skip_if_unsupported!(kernel, test_name);
1473 let high = [f64::NAN, 3.0, 5.0, 8.0, 8.5, 9.0, 2.0, 1.0];
1474 let low = [f64::NAN, 2.0, 1.0, 4.0, 4.5, 1.0, 1.0, 0.5];
1475 let params = DonchianParams { period: Some(3) };
1476 let input = DonchianInput::from_slices(&high, &low, params);
1477 let output = donchian_with_kernel(&input, kernel)?;
1478 assert_eq!(output.upperband.len(), high.len());
1479 assert!(output.upperband[2].is_nan());
1480 assert!(!output.upperband[3].is_nan());
1481 Ok(())
1482 }
1483
1484 #[cfg(debug_assertions)]
1485 fn check_donchian_no_poison(
1486 test_name: &str,
1487 kernel: Kernel,
1488 ) -> Result<(), Box<dyn std::error::Error>> {
1489 skip_if_unsupported!(kernel, test_name);
1490
1491 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1492 let candles = read_candles_from_csv(file_path)?;
1493
1494 let test_params = vec![
1495 DonchianParams::default(),
1496 DonchianParams { period: Some(2) },
1497 DonchianParams { period: Some(5) },
1498 DonchianParams { period: Some(10) },
1499 DonchianParams { period: Some(20) },
1500 DonchianParams { period: Some(50) },
1501 DonchianParams { period: Some(100) },
1502 DonchianParams { period: Some(200) },
1503 DonchianParams { period: Some(500) },
1504 DonchianParams { period: Some(14) },
1505 DonchianParams { period: Some(26) },
1506 ];
1507
1508 for (param_idx, params) in test_params.iter().enumerate() {
1509 let input = DonchianInput::from_candles(&candles, params.clone());
1510 let output = donchian_with_kernel(&input, kernel)?;
1511
1512 let bands = [
1513 ("upperband", &output.upperband),
1514 ("middleband", &output.middleband),
1515 ("lowerband", &output.lowerband),
1516 ];
1517
1518 for (band_name, band_values) in &bands {
1519 for (i, &val) in band_values.iter().enumerate() {
1520 if val.is_nan() {
1521 continue;
1522 }
1523
1524 let bits = val.to_bits();
1525
1526 if bits == 0x11111111_11111111 {
1527 panic!(
1528 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1529 in {} with params: period={} (param set {})",
1530 test_name, val, bits, i, band_name,
1531 params.period.unwrap_or(20), param_idx
1532 );
1533 }
1534
1535 if bits == 0x22222222_22222222 {
1536 panic!(
1537 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1538 in {} with params: period={} (param set {})",
1539 test_name, val, bits, i, band_name,
1540 params.period.unwrap_or(20), param_idx
1541 );
1542 }
1543
1544 if bits == 0x33333333_33333333 {
1545 panic!(
1546 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1547 in {} with params: period={} (param set {})",
1548 test_name, val, bits, i, band_name,
1549 params.period.unwrap_or(20), param_idx
1550 );
1551 }
1552 }
1553 }
1554 }
1555
1556 Ok(())
1557 }
1558
1559 #[cfg(not(debug_assertions))]
1560 fn check_donchian_no_poison(
1561 _test_name: &str,
1562 _kernel: Kernel,
1563 ) -> Result<(), Box<dyn std::error::Error>> {
1564 Ok(())
1565 }
1566
1567 macro_rules! generate_all_donchian_tests {
1568 ($($test_fn:ident),*) => {
1569 paste::paste! {
1570 $(
1571 #[test]
1572 fn [<$test_fn _scalar_f64>]() {
1573 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1574 }
1575 )*
1576 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1577 $(
1578 #[test]
1579 fn [<$test_fn _avx2_f64>]() {
1580 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1581 }
1582 #[test]
1583 fn [<$test_fn _avx512_f64>]() {
1584 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1585 }
1586 )*
1587 }
1588 }
1589 }
1590
1591 #[test]
1592 fn test_donchian_into_matches_api() {
1593 let n = 256usize;
1594 let mut high = vec![f64::NAN; n];
1595 let mut low = vec![f64::NAN; n];
1596
1597 for i in 5..n {
1598 let base = (i as f64).sin() * 10.0 + 100.0;
1599 high[i] = base + 2.0 + ((i % 7) as f64) * 0.1;
1600 low[i] = base - 2.0 - ((i % 5) as f64) * 0.1;
1601 }
1602 for idx in [37usize, 88, 133, 210] {
1603 high[idx] = f64::NAN;
1604 }
1605 for idx in [59usize, 120, 178, 220] {
1606 low[idx] = f64::NAN;
1607 }
1608
1609 let input = DonchianInput::from_slices(&high, &low, DonchianParams::default());
1610 let expected = donchian(&input).expect("baseline donchian() failed");
1611
1612 let mut up = vec![0.0; n];
1613 let mut mid = vec![0.0; n];
1614 let mut lo = vec![0.0; n];
1615
1616 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1617 {
1618 donchian_into(&input, &mut up, &mut mid, &mut lo).expect("donchian_into failed");
1619 }
1620
1621 assert_eq!(expected.upperband.len(), up.len());
1622 assert_eq!(expected.middleband.len(), mid.len());
1623 assert_eq!(expected.lowerband.len(), lo.len());
1624
1625 let eq = |a: f64, b: f64| (a.is_nan() && b.is_nan()) || (a == b);
1626 for i in 0..n {
1627 assert!(eq(expected.upperband[i], up[i]), "upper mismatch at {}", i);
1628 assert!(
1629 eq(expected.middleband[i], mid[i]),
1630 "middle mismatch at {}",
1631 i
1632 );
1633 assert!(eq(expected.lowerband[i], lo[i]), "lower mismatch at {}", i);
1634 }
1635 }
1636
1637 #[cfg(feature = "proptest")]
1638 #[allow(clippy::float_cmp)]
1639 fn check_donchian_property(
1640 test_name: &str,
1641 kernel: Kernel,
1642 ) -> Result<(), Box<dyn std::error::Error>> {
1643 use proptest::prelude::*;
1644 skip_if_unsupported!(kernel, test_name);
1645
1646 let random_strat = (2usize..=64)
1647 .prop_flat_map(|period| {
1648 (
1649 prop::collection::vec((50f64..5000f64, 0.1f64..50f64), period..400),
1650 Just(period),
1651 )
1652 })
1653 .prop_map(|(price_pairs, period)| {
1654 let mut high = Vec::with_capacity(price_pairs.len());
1655 let mut low = Vec::with_capacity(price_pairs.len());
1656 for (base, spread) in price_pairs {
1657 low.push(base);
1658 high.push(base + spread);
1659 }
1660 (high, low, period)
1661 });
1662
1663 let constant_strat =
1664 (2usize..=64, 50f64..5000f64, 0f64..50f64).prop_map(|(period, base_price, spread)| {
1665 let len = period + 50;
1666 let high = vec![base_price + spread; len];
1667 let low = vec![base_price; len];
1668 (high, low, period)
1669 });
1670
1671 let trending_strat = (2usize..=64).prop_map(|period| {
1672 let len = period + 100;
1673 let mut high = Vec::with_capacity(len);
1674 let mut low = Vec::with_capacity(len);
1675 for i in 0..len {
1676 let base = 100.0 + i as f64 * 10.0;
1677 low.push(base);
1678 high.push(base + 5.0);
1679 }
1680 (high, low, period)
1681 });
1682
1683 let volatile_strat = (2usize..=64)
1684 .prop_flat_map(|period| {
1685 (
1686 prop::collection::vec((10f64..10000f64, 0.1f64..500f64), period..200),
1687 Just(period),
1688 )
1689 })
1690 .prop_map(|(price_pairs, period)| {
1691 let mut high = Vec::with_capacity(price_pairs.len());
1692 let mut low = Vec::with_capacity(price_pairs.len());
1693 for (i, (base, spread)) in price_pairs.iter().enumerate() {
1694 let volatility = if i % 3 == 0 { 2.0 } else { 0.5 };
1695 low.push(base - spread * 0.1);
1696 high.push(base + spread * volatility);
1697 }
1698 (high, low, period)
1699 });
1700
1701 let single_price_strat = (2usize..=64, 50f64..5000f64).prop_map(|(period, price)| {
1702 let len = period + 50;
1703 let high = vec![price; len];
1704 let low = vec![price; len];
1705 (high, low, period)
1706 });
1707
1708 let combined_strat = prop_oneof![
1709 random_strat,
1710 constant_strat,
1711 trending_strat,
1712 volatile_strat,
1713 single_price_strat,
1714 ];
1715
1716 proptest::test_runner::TestRunner::default()
1717 .run(&combined_strat, |(high, low, period)| {
1718 for i in 0..high.len() {
1719 prop_assert!(
1720 high[i] >= low[i],
1721 "Invalid input data at index {}: high ({}) < low ({})",
1722 i,
1723 high[i],
1724 low[i]
1725 );
1726 }
1727
1728 let params = DonchianParams {
1729 period: Some(period),
1730 };
1731 let input = DonchianInput::from_slices(&high, &low, params.clone());
1732
1733 let output = donchian_with_kernel(&input, kernel).unwrap();
1734 let ref_output = donchian_with_kernel(&input, Kernel::Scalar).unwrap();
1735
1736 for i in 0..(period - 1) {
1737 prop_assert!(
1738 output.upperband[i].is_nan(),
1739 "Expected NaN during warmup at index {}, got {} (period={})",
1740 i,
1741 output.upperband[i],
1742 period
1743 );
1744 prop_assert!(
1745 output.middleband[i].is_nan(),
1746 "Expected NaN during warmup at index {}, got {} (period={})",
1747 i,
1748 output.middleband[i],
1749 period
1750 );
1751 prop_assert!(
1752 output.lowerband[i].is_nan(),
1753 "Expected NaN during warmup at index {}, got {} (period={})",
1754 i,
1755 output.lowerband[i],
1756 period
1757 );
1758 }
1759
1760 for i in (period - 1)..high.len() {
1761 let start = i + 1 - period;
1762 let window_high = &high[start..=i];
1763 let window_low = &low[start..=i];
1764
1765 let expected_max = window_high
1766 .iter()
1767 .cloned()
1768 .fold(f64::NEG_INFINITY, f64::max);
1769 let expected_min = window_low.iter().cloned().fold(f64::INFINITY, f64::min);
1770 let expected_mid = 0.5 * (expected_max + expected_min);
1771
1772 let upper = output.upperband[i];
1773 let middle = output.middleband[i];
1774 let lower = output.lowerband[i];
1775
1776 prop_assert!(
1777 (upper - expected_max).abs() < 1e-9,
1778 "Upperband mismatch at idx {}: got {}, expected {} (period={})",
1779 i,
1780 upper,
1781 expected_max,
1782 period
1783 );
1784
1785 prop_assert!(
1786 (lower - expected_min).abs() < 1e-9,
1787 "Lowerband mismatch at idx {}: got {}, expected {} (period={})",
1788 i,
1789 lower,
1790 expected_min,
1791 period
1792 );
1793
1794 prop_assert!(
1795 (middle - expected_mid).abs() < 1e-9,
1796 "Middleband mismatch at idx {}: got {}, expected {} (period={})",
1797 i,
1798 middle,
1799 expected_mid,
1800 period
1801 );
1802
1803 prop_assert!(
1804 upper >= middle && middle >= lower,
1805 "Band ordering violated at idx {}: upper={}, middle={}, lower={} (period={})",
1806 i, upper, middle, lower, period
1807 );
1808
1809 let data_min = window_low.iter().cloned().fold(f64::INFINITY, f64::min);
1810 let data_max = window_high
1811 .iter()
1812 .cloned()
1813 .fold(f64::NEG_INFINITY, f64::max);
1814 prop_assert!(
1815 upper <= data_max + 1e-9 && lower >= data_min - 1e-9,
1816 "Bands outside data range at idx {}: upper={}, lower={}, data_range=[{}, {}]",
1817 i, upper, lower, data_min, data_max
1818 );
1819
1820 if period == 1 {
1821 prop_assert!(
1822 (upper - high[i]).abs() < 1e-9,
1823 "Period=1: upper should equal current high at idx {}: {} vs {}",
1824 i,
1825 upper,
1826 high[i]
1827 );
1828 prop_assert!(
1829 (lower - low[i]).abs() < 1e-9,
1830 "Period=1: lower should equal current low at idx {}: {} vs {}",
1831 i,
1832 lower,
1833 low[i]
1834 );
1835 }
1836
1837 let window_is_single_price = window_high
1838 .iter()
1839 .zip(window_low.iter())
1840 .all(|(h, l)| (h - l).abs() < f64::EPSILON);
1841
1842 if window_is_single_price {
1843 prop_assert!(
1844 (upper - lower).abs() < 1e-9,
1845 "Single price window: bands should converge at idx {}: upper={}, lower={}",
1846 i, upper, lower
1847 );
1848 prop_assert!(
1849 (middle - upper).abs() < 1e-9,
1850 "Single price window: middle should equal upper/lower at idx {}: middle={}, upper={}",
1851 i, middle, upper
1852 );
1853 }
1854
1855 let ref_upper = ref_output.upperband[i];
1856 let ref_middle = ref_output.middleband[i];
1857 let ref_lower = ref_output.lowerband[i];
1858
1859 if !upper.is_finite() || !ref_upper.is_finite() {
1860 prop_assert!(
1861 upper.to_bits() == ref_upper.to_bits(),
1862 "Upper finite/NaN mismatch at idx {}: {} vs {}",
1863 i,
1864 upper,
1865 ref_upper
1866 );
1867 } else {
1868 let ulp_diff = upper.to_bits().abs_diff(ref_upper.to_bits());
1869 prop_assert!(
1870 (upper - ref_upper).abs() <= 1e-9 || ulp_diff <= 4,
1871 "Upper kernel mismatch at idx {}: {} vs {} (ULP={})",
1872 i,
1873 upper,
1874 ref_upper,
1875 ulp_diff
1876 );
1877 }
1878
1879 if !middle.is_finite() || !ref_middle.is_finite() {
1880 prop_assert!(
1881 middle.to_bits() == ref_middle.to_bits(),
1882 "Middle finite/NaN mismatch at idx {}: {} vs {}",
1883 i,
1884 middle,
1885 ref_middle
1886 );
1887 } else {
1888 let ulp_diff = middle.to_bits().abs_diff(ref_middle.to_bits());
1889 prop_assert!(
1890 (middle - ref_middle).abs() <= 1e-9 || ulp_diff <= 4,
1891 "Middle kernel mismatch at idx {}: {} vs {} (ULP={})",
1892 i,
1893 middle,
1894 ref_middle,
1895 ulp_diff
1896 );
1897 }
1898
1899 if !lower.is_finite() || !ref_lower.is_finite() {
1900 prop_assert!(
1901 lower.to_bits() == ref_lower.to_bits(),
1902 "Lower finite/NaN mismatch at idx {}: {} vs {}",
1903 i,
1904 lower,
1905 ref_lower
1906 );
1907 } else {
1908 let ulp_diff = lower.to_bits().abs_diff(ref_lower.to_bits());
1909 prop_assert!(
1910 (lower - ref_lower).abs() <= 1e-9 || ulp_diff <= 4,
1911 "Lower kernel mismatch at idx {}: {} vs {} (ULP={})",
1912 i,
1913 lower,
1914 ref_lower,
1915 ulp_diff
1916 );
1917 }
1918
1919 for (band_name, val) in [("upper", upper), ("middle", middle), ("lower", lower)]
1920 {
1921 let bits = val.to_bits();
1922 prop_assert!(
1923 bits != 0x11111111_11111111,
1924 "Found alloc_with_nan_prefix poison in {} at idx {}: {} (0x{:016X})",
1925 band_name,
1926 i,
1927 val,
1928 bits
1929 );
1930 prop_assert!(
1931 bits != 0x22222222_22222222,
1932 "Found init_matrix_prefixes poison in {} at idx {}: {} (0x{:016X})",
1933 band_name,
1934 i,
1935 val,
1936 bits
1937 );
1938 prop_assert!(
1939 bits != 0x33333333_33333333,
1940 "Found make_uninit_matrix poison in {} at idx {}: {} (0x{:016X})",
1941 band_name,
1942 i,
1943 val,
1944 bits
1945 );
1946 }
1947 }
1948
1949 Ok(())
1950 })
1951 .unwrap();
1952
1953 Ok(())
1954 }
1955
1956 generate_all_donchian_tests!(
1957 check_donchian_partial_params,
1958 check_donchian_accuracy,
1959 check_donchian_zero_period,
1960 check_donchian_period_exceeds_length,
1961 check_donchian_very_small_dataset,
1962 check_donchian_mismatched_length,
1963 check_donchian_all_nan_data,
1964 check_donchian_partial_computation,
1965 check_donchian_no_poison
1966 );
1967
1968 #[cfg(feature = "proptest")]
1969 generate_all_donchian_tests!(check_donchian_property);
1970
1971 fn check_batch_default_row(
1972 test: &str,
1973 kernel: Kernel,
1974 ) -> Result<(), Box<dyn std::error::Error>> {
1975 skip_if_unsupported!(kernel, test);
1976 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1977 let c = read_candles_from_csv(file)?;
1978 let output = DonchianBatchBuilder::new()
1979 .kernel(kernel)
1980 .apply_candles(&c)?;
1981 let def = DonchianParams::default();
1982 let row = output.upper_for(&def).expect("default row missing");
1983 assert_eq!(row.len(), c.close.len());
1984 Ok(())
1985 }
1986
1987 macro_rules! gen_batch_tests {
1988 ($fn_name:ident) => {
1989 paste::paste! {
1990 #[test] fn [<$fn_name _scalar>]() {
1991 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1992 }
1993 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1994 #[test] fn [<$fn_name _avx2>]() {
1995 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1996 }
1997 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1998 #[test] fn [<$fn_name _avx512>]() {
1999 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2000 }
2001 #[test] fn [<$fn_name _auto_detect>]() {
2002 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2003 }
2004 }
2005 };
2006 }
2007 gen_batch_tests!(check_batch_default_row);
2008 gen_batch_tests!(check_batch_no_poison);
2009
2010 #[cfg(debug_assertions)]
2011 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
2012 skip_if_unsupported!(kernel, test);
2013
2014 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2015 let c = read_candles_from_csv(file)?;
2016
2017 let test_configs = vec![
2018 (2, 10, 2),
2019 (10, 50, 10),
2020 (20, 100, 20),
2021 (50, 150, 25),
2022 (2, 5, 1),
2023 (100, 300, 50),
2024 (14, 26, 4),
2025 (5, 20, 3),
2026 ];
2027
2028 for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
2029 let output = DonchianBatchBuilder::new()
2030 .kernel(kernel)
2031 .period_range(p_start, p_end, p_step)
2032 .apply_candles(&c)?;
2033
2034 let bands = [
2035 ("upper", &output.upper),
2036 ("middle", &output.middle),
2037 ("lower", &output.lower),
2038 ];
2039
2040 for (band_name, band_values) in &bands {
2041 for (idx, &val) in band_values.iter().enumerate() {
2042 if val.is_nan() {
2043 continue;
2044 }
2045
2046 let bits = val.to_bits();
2047 let row = idx / output.cols;
2048 let col = idx % output.cols;
2049 let combo = &output.combos[row];
2050
2051 if bits == 0x11111111_11111111 {
2052 panic!(
2053 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2054 in {} at row {} col {} (flat index {}) with params: period={}",
2055 test, cfg_idx, val, bits, band_name, row, col, idx,
2056 combo.period.unwrap_or(20)
2057 );
2058 }
2059
2060 if bits == 0x22222222_22222222 {
2061 panic!(
2062 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2063 in {} at row {} col {} (flat index {}) with params: period={}",
2064 test, cfg_idx, val, bits, band_name, row, col, idx,
2065 combo.period.unwrap_or(20)
2066 );
2067 }
2068
2069 if bits == 0x33333333_33333333 {
2070 panic!(
2071 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2072 in {} at row {} col {} (flat index {}) with params: period={}",
2073 test,
2074 cfg_idx,
2075 val,
2076 bits,
2077 band_name,
2078 row,
2079 col,
2080 idx,
2081 combo.period.unwrap_or(20)
2082 );
2083 }
2084 }
2085 }
2086 }
2087
2088 Ok(())
2089 }
2090
2091 #[cfg(not(debug_assertions))]
2092 fn check_batch_no_poison(
2093 _test: &str,
2094 _kernel: Kernel,
2095 ) -> Result<(), Box<dyn std::error::Error>> {
2096 Ok(())
2097 }
2098}
2099
2100#[cfg(feature = "python")]
2101use crate::utilities::kernel_validation::validate_kernel;
2102#[cfg(all(feature = "python", feature = "cuda"))]
2103use numpy::PyUntypedArrayMethods;
2104#[cfg(feature = "python")]
2105use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
2106#[cfg(feature = "python")]
2107use pyo3::exceptions::PyValueError;
2108#[cfg(feature = "python")]
2109use pyo3::prelude::*;
2110#[cfg(feature = "python")]
2111use pyo3::types::PyDict;
2112
2113#[cfg(feature = "python")]
2114#[pyfunction(name = "donchian")]
2115#[pyo3(signature = (high, low, period, kernel=None))]
2116pub fn donchian_py<'py>(
2117 py: Python<'py>,
2118 high: PyReadonlyArray1<'py, f64>,
2119 low: PyReadonlyArray1<'py, f64>,
2120 period: usize,
2121 kernel: Option<&str>,
2122) -> PyResult<(
2123 Bound<'py, PyArray1<f64>>,
2124 Bound<'py, PyArray1<f64>>,
2125 Bound<'py, PyArray1<f64>>,
2126)> {
2127 use numpy::{IntoPyArray, PyArrayMethods};
2128
2129 let high_slice = high.as_slice()?;
2130 let low_slice = low.as_slice()?;
2131 let kern = validate_kernel(kernel, false)?;
2132
2133 let params = DonchianParams {
2134 period: Some(period),
2135 };
2136 let input = DonchianInput::from_slices(high_slice, low_slice, params);
2137
2138 let (upper_vec, middle_vec, lower_vec) = py
2139 .allow_threads(|| {
2140 donchian_with_kernel(&input, kern).map(|o| (o.upperband, o.middleband, o.lowerband))
2141 })
2142 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2143
2144 Ok((
2145 upper_vec.into_pyarray(py),
2146 middle_vec.into_pyarray(py),
2147 lower_vec.into_pyarray(py),
2148 ))
2149}
2150
2151#[cfg(feature = "python")]
2152#[pyclass(name = "DonchianStream")]
2153pub struct DonchianStreamPy {
2154 stream: DonchianStream,
2155}
2156
2157#[cfg(feature = "python")]
2158#[pymethods]
2159impl DonchianStreamPy {
2160 #[new]
2161 fn new(period: usize) -> PyResult<Self> {
2162 let params = DonchianParams {
2163 period: Some(period),
2164 };
2165 let stream =
2166 DonchianStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2167 Ok(DonchianStreamPy { stream })
2168 }
2169
2170 fn update(&mut self, high: f64, low: f64) -> Option<(f64, f64, f64)> {
2171 self.stream.update(high, low)
2172 }
2173}
2174
2175#[inline(always)]
2176fn donchian_batch_inner_into(
2177 high: &[f64],
2178 low: &[f64],
2179 sweep: &DonchianBatchRange,
2180 kern: Kernel,
2181 parallel: bool,
2182 out_upper: &mut [f64],
2183 out_middle: &mut [f64],
2184 out_lower: &mut [f64],
2185) -> Result<Vec<DonchianParams>, DonchianError> {
2186 let combos = expand_grid(sweep)?;
2187 if combos.is_empty() {
2188 return Err(DonchianError::InvalidRange {
2189 start: sweep.period.0,
2190 end: sweep.period.1,
2191 step: sweep.period.2,
2192 });
2193 }
2194 if high.len() != low.len() {
2195 return Err(DonchianError::MismatchedLength);
2196 }
2197
2198 let first = high
2199 .iter()
2200 .position(|x| !x.is_nan())
2201 .zip(low.iter().position(|x| !x.is_nan()))
2202 .map(|(a, b)| a.max(b));
2203 let first = match first {
2204 Some(idx) => idx,
2205 None => return Err(DonchianError::AllValuesNaN),
2206 };
2207
2208 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
2209 if high.len() - first < max_p {
2210 return Err(DonchianError::NotEnoughValidData {
2211 needed: max_p,
2212 valid: high.len() - first,
2213 });
2214 }
2215
2216 let rows = combos.len();
2217 let cols = high.len();
2218
2219 for (row, combo) in combos.iter().enumerate() {
2220 let period = combo.period.unwrap();
2221 let warmup = first + period - 1;
2222 let row_start = row * cols;
2223 for i in 0..warmup {
2224 out_upper[row_start + i] = f64::NAN;
2225 out_middle[row_start + i] = f64::NAN;
2226 out_lower[row_start + i] = f64::NAN;
2227 }
2228 }
2229
2230 let do_row =
2231 |row: usize, out_upper: &mut [f64], out_middle: &mut [f64], out_lower: &mut [f64]| unsafe {
2232 let period = combos[row].period.unwrap();
2233 match kern {
2234 Kernel::Scalar => {
2235 donchian_row_scalar(high, low, first, period, out_upper, out_middle, out_lower)
2236 }
2237 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2238 Kernel::Avx2 => {
2239 donchian_row_avx2(high, low, first, period, out_upper, out_middle, out_lower)
2240 }
2241 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2242 Kernel::Avx512 => {
2243 donchian_row_avx512(high, low, first, period, out_upper, out_middle, out_lower)
2244 }
2245 _ => unreachable!(),
2246 }
2247 };
2248
2249 if parallel {
2250 #[cfg(not(target_arch = "wasm32"))]
2251 {
2252 out_upper
2253 .par_chunks_mut(cols)
2254 .zip(out_middle.par_chunks_mut(cols))
2255 .zip(out_lower.par_chunks_mut(cols))
2256 .enumerate()
2257 .for_each(|(row, ((upper, middle), lower))| do_row(row, upper, middle, lower));
2258 }
2259
2260 #[cfg(target_arch = "wasm32")]
2261 {
2262 for (((upper, middle), lower), row) in out_upper
2263 .chunks_mut(cols)
2264 .zip(out_middle.chunks_mut(cols))
2265 .zip(out_lower.chunks_mut(cols))
2266 .zip(0..)
2267 {
2268 do_row(row, upper, middle, lower);
2269 }
2270 }
2271 } else {
2272 for (((upper, middle), lower), row) in out_upper
2273 .chunks_mut(cols)
2274 .zip(out_middle.chunks_mut(cols))
2275 .zip(out_lower.chunks_mut(cols))
2276 .zip(0..)
2277 {
2278 do_row(row, upper, middle, lower);
2279 }
2280 }
2281
2282 Ok(combos)
2283}
2284
2285#[cfg(feature = "python")]
2286#[pyfunction(name = "donchian_batch")]
2287#[pyo3(signature = (high, low, period_range, kernel=None))]
2288pub fn donchian_batch_py<'py>(
2289 py: Python<'py>,
2290 high: PyReadonlyArray1<'py, f64>,
2291 low: PyReadonlyArray1<'py, f64>,
2292 period_range: (usize, usize, usize),
2293 kernel: Option<&str>,
2294) -> PyResult<Bound<'py, PyDict>> {
2295 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2296 use pyo3::types::PyDict;
2297
2298 let high_slice = high.as_slice()?;
2299 let low_slice = low.as_slice()?;
2300 let kern = validate_kernel(kernel, true)?;
2301
2302 let sweep = DonchianBatchRange {
2303 period: period_range,
2304 };
2305
2306 let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2307 let rows = combos.len();
2308 let cols = high_slice.len();
2309
2310 let upper_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2311 let middle_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2312 let lower_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2313
2314 let upper_slice = unsafe { upper_arr.as_slice_mut()? };
2315 let middle_slice = unsafe { middle_arr.as_slice_mut()? };
2316 let lower_slice = unsafe { lower_arr.as_slice_mut()? };
2317
2318 let combos = py
2319 .allow_threads(|| {
2320 let kernel = match kern {
2321 Kernel::Auto => detect_best_batch_kernel(),
2322 k => k,
2323 };
2324
2325 let simd = match kernel {
2326 Kernel::Avx512Batch => Kernel::Avx512,
2327 Kernel::Avx2Batch => Kernel::Avx2,
2328 Kernel::ScalarBatch => Kernel::Scalar,
2329 _ => kernel,
2330 };
2331
2332 donchian_batch_inner_into(
2333 high_slice,
2334 low_slice,
2335 &sweep,
2336 simd,
2337 true,
2338 upper_slice,
2339 middle_slice,
2340 lower_slice,
2341 )
2342 })
2343 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2344
2345 let dict = PyDict::new(py);
2346 dict.set_item("upper", upper_arr.reshape((rows, cols))?)?;
2347 dict.set_item("middle", middle_arr.reshape((rows, cols))?)?;
2348 dict.set_item("lower", lower_arr.reshape((rows, cols))?)?;
2349 dict.set_item(
2350 "periods",
2351 combos
2352 .iter()
2353 .map(|p| p.period.unwrap() as u64)
2354 .collect::<Vec<_>>()
2355 .into_pyarray(py),
2356 )?;
2357
2358 Ok(dict)
2359}
2360
2361#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2362#[wasm_bindgen]
2363pub struct DonchianResult {
2364 values: Vec<f64>,
2365 rows: usize,
2366 cols: usize,
2367}
2368
2369#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2370#[wasm_bindgen]
2371impl DonchianResult {
2372 #[wasm_bindgen(getter)]
2373 pub fn values(&self) -> Vec<f64> {
2374 self.values.clone()
2375 }
2376
2377 #[wasm_bindgen(getter)]
2378 pub fn rows(&self) -> usize {
2379 self.rows
2380 }
2381
2382 #[wasm_bindgen(getter)]
2383 pub fn cols(&self) -> usize {
2384 self.cols
2385 }
2386}
2387
2388#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2389#[wasm_bindgen]
2390pub fn donchian_js(high: &[f64], low: &[f64], period: usize) -> Result<DonchianResult, JsValue> {
2391 let params = DonchianParams {
2392 period: Some(period),
2393 };
2394 let input = DonchianInput::from_slices(high, low, params);
2395
2396 let len = high.len();
2397 let mut upper = vec![0.0; len];
2398 let mut middle = vec![0.0; len];
2399 let mut lower = vec![0.0; len];
2400
2401 donchian_into_slice(&mut upper, &mut middle, &mut lower, &input, Kernel::Auto)
2402 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2403
2404 let mut values = Vec::with_capacity(len * 3);
2405 values.extend_from_slice(&upper);
2406 values.extend_from_slice(&middle);
2407 values.extend_from_slice(&lower);
2408
2409 Ok(DonchianResult {
2410 values,
2411 rows: 3,
2412 cols: len,
2413 })
2414}
2415
2416#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2417#[wasm_bindgen]
2418pub fn donchian_into(
2419 high_ptr: *const f64,
2420 low_ptr: *const f64,
2421 upper_ptr: *mut f64,
2422 middle_ptr: *mut f64,
2423 lower_ptr: *mut f64,
2424 len: usize,
2425 period: usize,
2426) -> Result<(), JsValue> {
2427 if high_ptr.is_null()
2428 || low_ptr.is_null()
2429 || upper_ptr.is_null()
2430 || middle_ptr.is_null()
2431 || lower_ptr.is_null()
2432 {
2433 return Err(JsValue::from_str("Null pointer provided"));
2434 }
2435
2436 unsafe {
2437 let high = std::slice::from_raw_parts(high_ptr, len);
2438 let low = std::slice::from_raw_parts(low_ptr, len);
2439 let params = DonchianParams {
2440 period: Some(period),
2441 };
2442 let input = DonchianInput::from_slices(high, low, params);
2443
2444 let need_temp = high_ptr == upper_ptr as *const f64
2445 || high_ptr == middle_ptr as *const f64
2446 || high_ptr == lower_ptr as *const f64
2447 || low_ptr == upper_ptr as *const f64
2448 || low_ptr == middle_ptr as *const f64
2449 || low_ptr == lower_ptr as *const f64
2450 || upper_ptr == middle_ptr
2451 || upper_ptr == lower_ptr
2452 || middle_ptr == lower_ptr;
2453
2454 if need_temp {
2455 let mut temp_upper = vec![0.0; len];
2456 let mut temp_middle = vec![0.0; len];
2457 let mut temp_lower = vec![0.0; len];
2458
2459 donchian_into_slice(
2460 &mut temp_upper,
2461 &mut temp_middle,
2462 &mut temp_lower,
2463 &input,
2464 Kernel::Auto,
2465 )
2466 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2467
2468 let upper_out = std::slice::from_raw_parts_mut(upper_ptr, len);
2469 let middle_out = std::slice::from_raw_parts_mut(middle_ptr, len);
2470 let lower_out = std::slice::from_raw_parts_mut(lower_ptr, len);
2471
2472 upper_out.copy_from_slice(&temp_upper);
2473 middle_out.copy_from_slice(&temp_middle);
2474 lower_out.copy_from_slice(&temp_lower);
2475 } else {
2476 let upper_out = std::slice::from_raw_parts_mut(upper_ptr, len);
2477 let middle_out = std::slice::from_raw_parts_mut(middle_ptr, len);
2478 let lower_out = std::slice::from_raw_parts_mut(lower_ptr, len);
2479
2480 donchian_into_slice(upper_out, middle_out, lower_out, &input, Kernel::Auto)
2481 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2482 }
2483
2484 Ok(())
2485 }
2486}
2487
2488#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2489#[wasm_bindgen]
2490pub fn donchian_alloc(len: usize) -> *mut f64 {
2491 let mut vec = Vec::<f64>::with_capacity(len);
2492 let ptr = vec.as_mut_ptr();
2493 std::mem::forget(vec);
2494 ptr
2495}
2496
2497#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2498#[wasm_bindgen]
2499pub fn donchian_free(ptr: *mut f64, len: usize) {
2500 if !ptr.is_null() {
2501 unsafe {
2502 let _ = Vec::from_raw_parts(ptr, len, len);
2503 }
2504 }
2505}
2506
2507#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2508#[derive(Serialize, Deserialize)]
2509pub struct DonchianBatchConfig {
2510 pub period_range: (usize, usize, usize),
2511}
2512
2513#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2514#[derive(Serialize, Deserialize)]
2515pub struct DonchianBatchJsOutput {
2516 pub upper: Vec<f64>,
2517 pub middle: Vec<f64>,
2518 pub lower: Vec<f64>,
2519 pub periods: Vec<usize>,
2520 pub rows: usize,
2521 pub cols: usize,
2522}
2523
2524#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2525#[wasm_bindgen(js_name = donchian_batch)]
2526pub fn donchian_batch_js(high: &[f64], low: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2527 let config: DonchianBatchConfig = serde_wasm_bindgen::from_value(config)
2528 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2529
2530 let sweep = DonchianBatchRange {
2531 period: config.period_range,
2532 };
2533
2534 let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2535 let rows = combos.len();
2536 let cols = high.len();
2537
2538 let mut upper = vec![0.0; rows * cols];
2539 let mut middle = vec![0.0; rows * cols];
2540 let mut lower = vec![0.0; rows * cols];
2541
2542 donchian_batch_inner_into(
2543 high,
2544 low,
2545 &sweep,
2546 detect_best_kernel(),
2547 false,
2548 &mut upper,
2549 &mut middle,
2550 &mut lower,
2551 )
2552 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2553
2554 let js_output = DonchianBatchJsOutput {
2555 upper,
2556 middle,
2557 lower,
2558 periods: combos.iter().map(|p| p.period.unwrap()).collect(),
2559 rows,
2560 cols,
2561 };
2562
2563 serde_wasm_bindgen::to_value(&js_output)
2564 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2565}
2566
2567#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2568#[wasm_bindgen]
2569pub fn donchian_batch_into(
2570 high_ptr: *const f64,
2571 low_ptr: *const f64,
2572 upper_ptr: *mut f64,
2573 middle_ptr: *mut f64,
2574 lower_ptr: *mut f64,
2575 len: usize,
2576 period_start: usize,
2577 period_end: usize,
2578 period_step: usize,
2579) -> Result<usize, JsValue> {
2580 if high_ptr.is_null()
2581 || low_ptr.is_null()
2582 || upper_ptr.is_null()
2583 || middle_ptr.is_null()
2584 || lower_ptr.is_null()
2585 {
2586 return Err(JsValue::from_str("Null pointer provided"));
2587 }
2588
2589 unsafe {
2590 let high = std::slice::from_raw_parts(high_ptr, len);
2591 let low = std::slice::from_raw_parts(low_ptr, len);
2592
2593 let sweep = DonchianBatchRange {
2594 period: (period_start, period_end, period_step),
2595 };
2596
2597 let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2598 let rows = combos.len();
2599 let cols = len;
2600
2601 let upper_out = std::slice::from_raw_parts_mut(upper_ptr, rows * cols);
2602 let middle_out = std::slice::from_raw_parts_mut(middle_ptr, rows * cols);
2603 let lower_out = std::slice::from_raw_parts_mut(lower_ptr, rows * cols);
2604
2605 donchian_batch_inner_into(
2606 high,
2607 low,
2608 &sweep,
2609 detect_best_kernel(),
2610 false,
2611 upper_out,
2612 middle_out,
2613 lower_out,
2614 )
2615 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2616
2617 Ok(rows)
2618 }
2619}
2620
2621#[cfg(all(feature = "python", feature = "cuda"))]
2622use crate::cuda::cuda_available;
2623#[cfg(all(feature = "python", feature = "cuda"))]
2624use crate::cuda::donchian_wrapper::CudaDonchian;
2625#[cfg(all(feature = "python", feature = "cuda"))]
2626use crate::cuda::donchian_wrapper::DeviceArrayF32 as DeviceArrayF32Donch;
2627#[cfg(all(feature = "python", feature = "cuda"))]
2628use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
2629
2630#[cfg(all(feature = "python", feature = "cuda"))]
2631#[pyclass(module = "ta_indicators.cuda", unsendable)]
2632pub struct DeviceArrayF32DonchPy {
2633 pub(crate) inner: Option<DeviceArrayF32Donch>,
2634}
2635
2636#[cfg(all(feature = "python", feature = "cuda"))]
2637#[pymethods]
2638impl DeviceArrayF32DonchPy {
2639 #[getter]
2640 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
2641 let inner = self
2642 .inner
2643 .as_ref()
2644 .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2645 let d = PyDict::new(py);
2646 d.set_item("shape", (inner.rows, inner.cols))?;
2647 d.set_item("typestr", "<f4")?;
2648 let itemsize = std::mem::size_of::<f32>();
2649 let row_stride = inner
2650 .cols
2651 .checked_mul(itemsize)
2652 .ok_or_else(|| PyValueError::new_err("byte stride overflow"))?;
2653 d.set_item("strides", (row_stride, itemsize))?;
2654 d.set_item("data", (inner.device_ptr() as usize, false))?;
2655 d.set_item("version", 3)?;
2656 Ok(d)
2657 }
2658
2659 fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
2660 let inner = self
2661 .inner
2662 .as_ref()
2663 .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2664 Ok((2, inner.device_id as i32))
2665 }
2666
2667 #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
2668 fn __dlpack__<'py>(
2669 &mut self,
2670 py: Python<'py>,
2671 stream: Option<PyObject>,
2672 max_version: Option<PyObject>,
2673 dl_device: Option<PyObject>,
2674 copy: Option<PyObject>,
2675 ) -> PyResult<PyObject> {
2676 if copy.as_ref().and_then(|c| c.extract::<bool>(py).ok()) == Some(true) {
2677 return Err(PyValueError::new_err(
2678 "copy=True is not supported for donchian CUDA buffers",
2679 ));
2680 }
2681
2682 let (kdl, alloc_dev) = self.__dlpack_device__()?;
2683 if let Some(dev_obj) = dl_device.as_ref() {
2684 if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2685 if dev_ty != kdl || dev_id != alloc_dev {
2686 return Err(PyValueError::new_err(
2687 "dl_device mismatch for donchian CUDA buffer",
2688 ));
2689 }
2690 }
2691 }
2692 let _ = stream;
2693
2694 let inner = self
2695 .inner
2696 .take()
2697 .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
2698 let rows = inner.rows;
2699 let cols = inner.cols;
2700 let buf = inner.buf;
2701
2702 let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2703
2704 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2705 }
2706}
2707
2708#[cfg(all(feature = "python", feature = "cuda"))]
2709#[pyfunction(name = "donchian_cuda_batch_dev")]
2710#[pyo3(signature = (high_f32, low_f32, period_range, device_id=0))]
2711pub fn donchian_cuda_batch_dev_py<'py>(
2712 py: Python<'py>,
2713 high_f32: numpy::PyReadonlyArray1<'py, f32>,
2714 low_f32: numpy::PyReadonlyArray1<'py, f32>,
2715 period_range: (usize, usize, usize),
2716 device_id: usize,
2717) -> PyResult<Bound<'py, PyDict>> {
2718 use numpy::IntoPyArray;
2719 if !cuda_available() {
2720 return Err(PyValueError::new_err("CUDA not available"));
2721 }
2722 let h = high_f32.as_slice()?;
2723 let l = low_f32.as_slice()?;
2724 let sweep = DonchianBatchRange {
2725 period: period_range,
2726 };
2727 let (triplet, combos) = py.allow_threads(|| {
2728 let cuda =
2729 CudaDonchian::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2730 cuda.donchian_batch_dev(h, l, &sweep)
2731 .map_err(|e| PyValueError::new_err(e.to_string()))
2732 })?;
2733
2734 let d = PyDict::new(py);
2735 d.set_item(
2736 "upper",
2737 Py::new(
2738 py,
2739 DeviceArrayF32DonchPy {
2740 inner: Some(triplet.wt1),
2741 },
2742 )?,
2743 )?;
2744 d.set_item(
2745 "middle",
2746 Py::new(
2747 py,
2748 DeviceArrayF32DonchPy {
2749 inner: Some(triplet.wt2),
2750 },
2751 )?,
2752 )?;
2753 d.set_item(
2754 "lower",
2755 Py::new(
2756 py,
2757 DeviceArrayF32DonchPy {
2758 inner: Some(triplet.hist),
2759 },
2760 )?,
2761 )?;
2762 d.set_item(
2763 "periods",
2764 combos
2765 .iter()
2766 .map(|p| p.period.unwrap())
2767 .collect::<Vec<_>>()
2768 .into_pyarray(py),
2769 )?;
2770 d.set_item("rows", combos.len())?;
2771 d.set_item("cols", h.len())?;
2772 Ok(d)
2773}
2774
2775#[cfg(all(feature = "python", feature = "cuda"))]
2776#[pyfunction(name = "donchian_cuda_many_series_one_param_dev")]
2777#[pyo3(signature = (high_tm_f32, low_tm_f32, period, device_id=0))]
2778pub fn donchian_cuda_many_series_one_param_dev_py<'py>(
2779 py: Python<'py>,
2780 high_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
2781 low_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
2782 period: usize,
2783 device_id: usize,
2784) -> PyResult<Bound<'py, PyDict>> {
2785 if !cuda_available() {
2786 return Err(PyValueError::new_err("CUDA not available"));
2787 }
2788 let shape = high_tm_f32.shape();
2789 if shape.len() != 2 || low_tm_f32.shape() != shape {
2790 return Err(PyValueError::new_err(
2791 "expected matching 2D arrays [rows, cols]",
2792 ));
2793 }
2794 let rows = shape[0];
2795 let cols = shape[1];
2796 let high_tm = high_tm_f32.as_slice()?;
2797 let low_tm = low_tm_f32.as_slice()?;
2798 let params = DonchianParams {
2799 period: Some(period),
2800 };
2801 let triplet = py.allow_threads(|| {
2802 let cuda =
2803 CudaDonchian::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2804 cuda.donchian_many_series_one_param_time_major_dev(high_tm, low_tm, cols, rows, ¶ms)
2805 .map_err(|e| PyValueError::new_err(e.to_string()))
2806 })?;
2807 let d = PyDict::new(py);
2808 d.set_item(
2809 "upper",
2810 Py::new(
2811 py,
2812 DeviceArrayF32DonchPy {
2813 inner: Some(triplet.wt1),
2814 },
2815 )?,
2816 )?;
2817 d.set_item(
2818 "middle",
2819 Py::new(
2820 py,
2821 DeviceArrayF32DonchPy {
2822 inner: Some(triplet.wt2),
2823 },
2824 )?,
2825 )?;
2826 d.set_item(
2827 "lower",
2828 Py::new(
2829 py,
2830 DeviceArrayF32DonchPy {
2831 inner: Some(triplet.hist),
2832 },
2833 )?,
2834 )?;
2835 d.set_item("rows", rows)?;
2836 d.set_item("cols", cols)?;
2837 d.set_item("period", period)?;
2838 Ok(d)
2839}