1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::collections::VecDeque;
26use std::error::Error;
27use thiserror::Error;
28
29#[derive(Debug, Clone)]
30pub enum DonchianChannelWidthData<'a> {
31 Candles { candles: &'a Candles },
32 Slices { high: &'a [f64], low: &'a [f64] },
33}
34
35#[derive(Debug, Clone)]
36pub struct DonchianChannelWidthOutput {
37 pub values: Vec<f64>,
38}
39
40#[derive(Debug, Clone)]
41#[cfg_attr(
42 all(target_arch = "wasm32", feature = "wasm"),
43 derive(Serialize, Deserialize)
44)]
45pub struct DonchianChannelWidthParams {
46 pub period: Option<usize>,
47}
48
49impl Default for DonchianChannelWidthParams {
50 fn default() -> Self {
51 Self { period: Some(20) }
52 }
53}
54
55#[derive(Debug, Clone)]
56pub struct DonchianChannelWidthInput<'a> {
57 pub data: DonchianChannelWidthData<'a>,
58 pub params: DonchianChannelWidthParams,
59}
60
61impl<'a> DonchianChannelWidthInput<'a> {
62 #[inline]
63 pub fn from_candles(candles: &'a Candles, params: DonchianChannelWidthParams) -> Self {
64 Self {
65 data: DonchianChannelWidthData::Candles { candles },
66 params,
67 }
68 }
69
70 #[inline]
71 pub fn from_slices(
72 high: &'a [f64],
73 low: &'a [f64],
74 params: DonchianChannelWidthParams,
75 ) -> Self {
76 Self {
77 data: DonchianChannelWidthData::Slices { high, low },
78 params,
79 }
80 }
81
82 #[inline]
83 pub fn with_default_candles(candles: &'a Candles) -> Self {
84 Self::from_candles(candles, DonchianChannelWidthParams::default())
85 }
86
87 #[inline]
88 pub fn get_period(&self) -> usize {
89 self.params.period.unwrap_or(20)
90 }
91}
92
93#[derive(Copy, Clone, Debug)]
94pub struct DonchianChannelWidthBuilder {
95 period: Option<usize>,
96 kernel: Kernel,
97}
98
99impl Default for DonchianChannelWidthBuilder {
100 fn default() -> Self {
101 Self {
102 period: None,
103 kernel: Kernel::Auto,
104 }
105 }
106}
107
108impl DonchianChannelWidthBuilder {
109 #[inline(always)]
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 #[inline(always)]
115 pub fn period(mut self, value: usize) -> Self {
116 self.period = Some(value);
117 self
118 }
119
120 #[inline(always)]
121 pub fn kernel(mut self, value: Kernel) -> Self {
122 self.kernel = value;
123 self
124 }
125
126 #[inline(always)]
127 pub fn apply(
128 self,
129 candles: &Candles,
130 ) -> Result<DonchianChannelWidthOutput, DonchianChannelWidthError> {
131 let params = DonchianChannelWidthParams {
132 period: self.period,
133 };
134 donchian_channel_width_with_kernel(
135 &DonchianChannelWidthInput::from_candles(candles, params),
136 self.kernel,
137 )
138 }
139
140 #[inline(always)]
141 pub fn apply_slices(
142 self,
143 high: &[f64],
144 low: &[f64],
145 ) -> Result<DonchianChannelWidthOutput, DonchianChannelWidthError> {
146 let params = DonchianChannelWidthParams {
147 period: self.period,
148 };
149 donchian_channel_width_with_kernel(
150 &DonchianChannelWidthInput::from_slices(high, low, params),
151 self.kernel,
152 )
153 }
154
155 #[inline(always)]
156 pub fn into_stream(self) -> Result<DonchianChannelWidthStream, DonchianChannelWidthError> {
157 DonchianChannelWidthStream::try_new(DonchianChannelWidthParams {
158 period: self.period,
159 })
160 }
161}
162
163#[derive(Debug, Error)]
164pub enum DonchianChannelWidthError {
165 #[error("donchian_channel_width: Input data slice is empty.")]
166 EmptyInputData,
167 #[error("donchian_channel_width: Input length mismatch: high = {high_len}, low = {low_len}")]
168 InputLengthMismatch { high_len: usize, low_len: usize },
169 #[error("donchian_channel_width: All values are NaN.")]
170 AllValuesNaN,
171 #[error("donchian_channel_width: Invalid period: period = {period}, data length = {data_len}")]
172 InvalidPeriod { period: usize, data_len: usize },
173 #[error("donchian_channel_width: Not enough valid data: needed = {needed}, valid = {valid}")]
174 NotEnoughValidData { needed: usize, valid: usize },
175 #[error("donchian_channel_width: Output length mismatch: expected = {expected}, got = {got}")]
176 OutputLengthMismatch { expected: usize, got: usize },
177 #[error("donchian_channel_width: Invalid range: start={start}, end={end}, step={step}")]
178 InvalidRange {
179 start: usize,
180 end: usize,
181 step: usize,
182 },
183 #[error("donchian_channel_width: Invalid kernel for batch: {0:?}")]
184 InvalidKernelForBatch(Kernel),
185 #[error(
186 "donchian_channel_width: Output length mismatch: dst = {dst_len}, expected = {expected_len}"
187 )]
188 MismatchedOutputLen { dst_len: usize, expected_len: usize },
189 #[error("donchian_channel_width: Invalid input: {msg}")]
190 InvalidInput { msg: String },
191}
192
193#[derive(Debug, Clone)]
194pub struct DonchianChannelWidthStream {
195 period: usize,
196 next_index: usize,
197 max_deque: VecDeque<(usize, f64)>,
198 min_deque: VecDeque<(usize, f64)>,
199}
200
201impl DonchianChannelWidthStream {
202 #[inline(always)]
203 pub fn try_new(params: DonchianChannelWidthParams) -> Result<Self, DonchianChannelWidthError> {
204 let period = params.period.unwrap_or(20);
205 if period == 0 {
206 return Err(DonchianChannelWidthError::InvalidPeriod {
207 period,
208 data_len: 0,
209 });
210 }
211 Ok(Self {
212 period,
213 next_index: 0,
214 max_deque: VecDeque::with_capacity(period.max(1)),
215 min_deque: VecDeque::with_capacity(period.max(1)),
216 })
217 }
218
219 #[inline(always)]
220 pub fn reset(&mut self) {
221 self.next_index = 0;
222 self.max_deque.clear();
223 self.min_deque.clear();
224 }
225
226 #[inline(always)]
227 pub fn update(&mut self, high: f64, low: f64) -> Option<f64> {
228 if !is_valid_pair(high, low) {
229 self.reset();
230 return None;
231 }
232
233 let idx = self.next_index;
234 self.next_index += 1;
235
236 while let Some((_, v)) = self.max_deque.back() {
237 if *v <= high {
238 self.max_deque.pop_back();
239 } else {
240 break;
241 }
242 }
243 self.max_deque.push_back((idx, high));
244
245 while let Some((_, v)) = self.min_deque.back() {
246 if *v >= low {
247 self.min_deque.pop_back();
248 } else {
249 break;
250 }
251 }
252 self.min_deque.push_back((idx, low));
253
254 let window_start = idx.saturating_add(1).saturating_sub(self.period);
255 while let Some((front_idx, _)) = self.max_deque.front() {
256 if *front_idx < window_start {
257 self.max_deque.pop_front();
258 } else {
259 break;
260 }
261 }
262 while let Some((front_idx, _)) = self.min_deque.front() {
263 if *front_idx < window_start {
264 self.min_deque.pop_front();
265 } else {
266 break;
267 }
268 }
269
270 if idx + 1 < self.period {
271 None
272 } else {
273 let upper = self.max_deque.front().map(|(_, v)| *v).unwrap_or(f64::NAN);
274 let lower = self.min_deque.front().map(|(_, v)| *v).unwrap_or(f64::NAN);
275 Some(upper - lower)
276 }
277 }
278
279 #[inline(always)]
280 pub fn get_warmup_period(&self) -> usize {
281 self.period.saturating_sub(1)
282 }
283}
284
285#[inline(always)]
286fn is_valid_pair(high: f64, low: f64) -> bool {
287 high.is_finite() && low.is_finite()
288}
289
290#[inline(always)]
291fn longest_valid_pair_run(high: &[f64], low: &[f64]) -> usize {
292 let mut best = 0usize;
293 let mut cur = 0usize;
294 for (&h, &l) in high.iter().zip(low.iter()) {
295 if is_valid_pair(h, l) {
296 cur += 1;
297 if cur > best {
298 best = cur;
299 }
300 } else {
301 cur = 0;
302 }
303 }
304 best
305}
306
307#[inline(always)]
308fn input_slices<'a>(
309 input: &'a DonchianChannelWidthInput<'a>,
310) -> Result<(&'a [f64], &'a [f64]), DonchianChannelWidthError> {
311 match &input.data {
312 DonchianChannelWidthData::Candles { candles } => {
313 Ok((candles.high.as_slice(), candles.low.as_slice()))
314 }
315 DonchianChannelWidthData::Slices { high, low } => Ok((*high, *low)),
316 }
317}
318
319#[inline(always)]
320fn validate_common(
321 high: &[f64],
322 low: &[f64],
323 period: usize,
324) -> Result<(), DonchianChannelWidthError> {
325 if high.is_empty() || low.is_empty() {
326 return Err(DonchianChannelWidthError::EmptyInputData);
327 }
328 if high.len() != low.len() {
329 return Err(DonchianChannelWidthError::InputLengthMismatch {
330 high_len: high.len(),
331 low_len: low.len(),
332 });
333 }
334 if period == 0 || period > high.len() {
335 return Err(DonchianChannelWidthError::InvalidPeriod {
336 period,
337 data_len: high.len(),
338 });
339 }
340
341 let max_run = longest_valid_pair_run(high, low);
342 if max_run == 0 {
343 return Err(DonchianChannelWidthError::AllValuesNaN);
344 }
345 if max_run < period {
346 return Err(DonchianChannelWidthError::NotEnoughValidData {
347 needed: period,
348 valid: max_run,
349 });
350 }
351 Ok(())
352}
353
354#[inline(always)]
355fn compute_row(high: &[f64], low: &[f64], period: usize, out: &mut [f64]) {
356 let mut max_deque: VecDeque<usize> = VecDeque::with_capacity(period.max(1));
357 let mut min_deque: VecDeque<usize> = VecDeque::with_capacity(period.max(1));
358 let mut seg_start = 0usize;
359 let mut in_segment = false;
360
361 for i in 0..high.len() {
362 let h = high[i];
363 let l = low[i];
364 if !is_valid_pair(h, l) {
365 out[i] = f64::NAN;
366 max_deque.clear();
367 min_deque.clear();
368 in_segment = false;
369 continue;
370 }
371
372 if !in_segment {
373 seg_start = i;
374 in_segment = true;
375 }
376
377 while let Some(&idx) = max_deque.back() {
378 if high[idx] <= h {
379 max_deque.pop_back();
380 } else {
381 break;
382 }
383 }
384 max_deque.push_back(i);
385
386 while let Some(&idx) = min_deque.back() {
387 if low[idx] >= l {
388 min_deque.pop_back();
389 } else {
390 break;
391 }
392 }
393 min_deque.push_back(i);
394
395 let raw_start = i.saturating_add(1).saturating_sub(period);
396 let window_start = raw_start.max(seg_start);
397
398 while let Some(&idx) = max_deque.front() {
399 if idx < window_start {
400 max_deque.pop_front();
401 } else {
402 break;
403 }
404 }
405 while let Some(&idx) = min_deque.front() {
406 if idx < window_start {
407 min_deque.pop_front();
408 } else {
409 break;
410 }
411 }
412
413 if i + 1 >= seg_start + period {
414 let upper = high[*max_deque.front().unwrap()];
415 let lower = low[*min_deque.front().unwrap()];
416 out[i] = upper - lower;
417 } else {
418 out[i] = f64::NAN;
419 }
420 }
421}
422
423#[inline]
424pub fn donchian_channel_width(
425 input: &DonchianChannelWidthInput,
426) -> Result<DonchianChannelWidthOutput, DonchianChannelWidthError> {
427 donchian_channel_width_with_kernel(input, Kernel::Auto)
428}
429
430pub fn donchian_channel_width_with_kernel(
431 input: &DonchianChannelWidthInput,
432 kernel: Kernel,
433) -> Result<DonchianChannelWidthOutput, DonchianChannelWidthError> {
434 let (high, low) = input_slices(input)?;
435 let period = input.get_period();
436 validate_common(high, low, period)?;
437
438 let _chosen = match kernel {
439 Kernel::Auto => detect_best_kernel(),
440 other => other,
441 };
442
443 let mut out = alloc_with_nan_prefix(high.len(), 0);
444 out.fill(f64::NAN);
445 compute_row(high, low, period, &mut out);
446 Ok(DonchianChannelWidthOutput { values: out })
447}
448
449pub fn donchian_channel_width_into_slice(
450 dst: &mut [f64],
451 input: &DonchianChannelWidthInput,
452 kernel: Kernel,
453) -> Result<(), DonchianChannelWidthError> {
454 let (high, low) = input_slices(input)?;
455 let period = input.get_period();
456 validate_common(high, low, period)?;
457
458 if dst.len() != high.len() {
459 return Err(DonchianChannelWidthError::OutputLengthMismatch {
460 expected: high.len(),
461 got: dst.len(),
462 });
463 }
464
465 let _chosen = match kernel {
466 Kernel::Auto => detect_best_kernel(),
467 other => other,
468 };
469
470 dst.fill(f64::NAN);
471 compute_row(high, low, period, dst);
472 Ok(())
473}
474
475#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
476pub fn donchian_channel_width_into(
477 input: &DonchianChannelWidthInput,
478 out: &mut [f64],
479) -> Result<(), DonchianChannelWidthError> {
480 donchian_channel_width_into_slice(out, input, Kernel::Auto)
481}
482
483#[derive(Debug, Clone, Copy)]
484pub struct DonchianChannelWidthBatchRange {
485 pub period: (usize, usize, usize),
486}
487
488impl Default for DonchianChannelWidthBatchRange {
489 fn default() -> Self {
490 Self {
491 period: (20, 20, 0),
492 }
493 }
494}
495
496#[derive(Debug, Clone)]
497pub struct DonchianChannelWidthBatchOutput {
498 pub values: Vec<f64>,
499 pub combos: Vec<DonchianChannelWidthParams>,
500 pub rows: usize,
501 pub cols: usize,
502}
503
504#[derive(Debug, Clone, Copy)]
505pub struct DonchianChannelWidthBatchBuilder {
506 range: DonchianChannelWidthBatchRange,
507 kernel: Kernel,
508}
509
510impl Default for DonchianChannelWidthBatchBuilder {
511 fn default() -> Self {
512 Self {
513 range: DonchianChannelWidthBatchRange::default(),
514 kernel: Kernel::Auto,
515 }
516 }
517}
518
519impl DonchianChannelWidthBatchBuilder {
520 #[inline(always)]
521 pub fn new() -> Self {
522 Self::default()
523 }
524
525 #[inline(always)]
526 pub fn kernel(mut self, value: Kernel) -> Self {
527 self.kernel = value;
528 self
529 }
530
531 #[inline(always)]
532 pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
533 self.range.period = (start, end, step);
534 self
535 }
536
537 #[inline(always)]
538 pub fn period_static(mut self, value: usize) -> Self {
539 self.range.period = (value, value, 0);
540 self
541 }
542
543 #[inline(always)]
544 pub fn apply_slices(
545 self,
546 high: &[f64],
547 low: &[f64],
548 ) -> Result<DonchianChannelWidthBatchOutput, DonchianChannelWidthError> {
549 donchian_channel_width_batch_with_kernel(high, low, &self.range, self.kernel)
550 }
551
552 #[inline(always)]
553 pub fn apply_candles(
554 self,
555 candles: &Candles,
556 ) -> Result<DonchianChannelWidthBatchOutput, DonchianChannelWidthError> {
557 donchian_channel_width_batch_with_kernel(
558 candles.high.as_slice(),
559 candles.low.as_slice(),
560 &self.range,
561 self.kernel,
562 )
563 }
564}
565
566#[inline(always)]
567fn expand_grid_checked(
568 range: &DonchianChannelWidthBatchRange,
569) -> Result<Vec<DonchianChannelWidthParams>, DonchianChannelWidthError> {
570 let (start, end, step) = range.period;
571 if start == 0 || end == 0 {
572 return Err(DonchianChannelWidthError::InvalidRange { start, end, step });
573 }
574 if step == 0 {
575 return Ok(vec![DonchianChannelWidthParams {
576 period: Some(start),
577 }]);
578 }
579 if start > end {
580 return Err(DonchianChannelWidthError::InvalidRange { start, end, step });
581 }
582
583 let mut out = Vec::new();
584 let mut cur = start;
585 loop {
586 out.push(DonchianChannelWidthParams { period: Some(cur) });
587 if cur >= end {
588 break;
589 }
590 let next = cur.saturating_add(step);
591 if next <= cur {
592 return Err(DonchianChannelWidthError::InvalidRange { start, end, step });
593 }
594 cur = next.min(end);
595 if cur == *out.last().and_then(|p| p.period.as_ref()).unwrap() {
596 break;
597 }
598 }
599 Ok(out)
600}
601
602#[inline(always)]
603pub fn expand_grid_donchian_channel_width(
604 range: &DonchianChannelWidthBatchRange,
605) -> Vec<DonchianChannelWidthParams> {
606 expand_grid_checked(range).unwrap_or_default()
607}
608
609pub fn donchian_channel_width_batch_with_kernel(
610 high: &[f64],
611 low: &[f64],
612 sweep: &DonchianChannelWidthBatchRange,
613 kernel: Kernel,
614) -> Result<DonchianChannelWidthBatchOutput, DonchianChannelWidthError> {
615 match kernel {
616 Kernel::Auto
617 | Kernel::Scalar
618 | Kernel::ScalarBatch
619 | Kernel::Avx2
620 | Kernel::Avx2Batch
621 | Kernel::Avx512
622 | Kernel::Avx512Batch => {}
623 other => return Err(DonchianChannelWidthError::InvalidKernelForBatch(other)),
624 }
625
626 let combos = expand_grid_checked(sweep)?;
627 let max_period = combos
628 .iter()
629 .map(|params| params.period.unwrap_or(20))
630 .max()
631 .unwrap_or(0);
632 validate_common(high, low, max_period)?;
633
634 let rows = combos.len();
635 let cols = high.len();
636 let mut values_mu = make_uninit_matrix(rows, cols);
637 let warmups: Vec<usize> = combos
638 .iter()
639 .map(|params| params.period.unwrap_or(20).saturating_sub(1))
640 .collect();
641 init_matrix_prefixes(&mut values_mu, cols, &warmups);
642 let mut values = unsafe {
643 Vec::from_raw_parts(
644 values_mu.as_mut_ptr() as *mut f64,
645 values_mu.len(),
646 values_mu.capacity(),
647 )
648 };
649 std::mem::forget(values_mu);
650
651 donchian_channel_width_batch_inner_into(high, low, sweep, kernel, true, &mut values)?;
652
653 Ok(DonchianChannelWidthBatchOutput {
654 values,
655 combos,
656 rows,
657 cols,
658 })
659}
660
661pub fn donchian_channel_width_batch_slice(
662 high: &[f64],
663 low: &[f64],
664 sweep: &DonchianChannelWidthBatchRange,
665 kernel: Kernel,
666) -> Result<DonchianChannelWidthBatchOutput, DonchianChannelWidthError> {
667 donchian_channel_width_batch_inner(high, low, sweep, kernel, false)
668}
669
670pub fn donchian_channel_width_batch_par_slice(
671 high: &[f64],
672 low: &[f64],
673 sweep: &DonchianChannelWidthBatchRange,
674 kernel: Kernel,
675) -> Result<DonchianChannelWidthBatchOutput, DonchianChannelWidthError> {
676 donchian_channel_width_batch_inner(high, low, sweep, kernel, true)
677}
678
679fn donchian_channel_width_batch_inner(
680 high: &[f64],
681 low: &[f64],
682 sweep: &DonchianChannelWidthBatchRange,
683 kernel: Kernel,
684 parallel: bool,
685) -> Result<DonchianChannelWidthBatchOutput, DonchianChannelWidthError> {
686 let combos = expand_grid_checked(sweep)?;
687 let rows = combos.len();
688 let cols = high.len();
689 let total = rows
690 .checked_mul(cols)
691 .ok_or_else(|| DonchianChannelWidthError::InvalidInput {
692 msg: "donchian_channel_width: rows*cols overflow in batch".to_string(),
693 })?;
694
695 let mut values_mu = make_uninit_matrix(rows, cols);
696 let warmups: Vec<usize> = combos
697 .iter()
698 .map(|params| params.period.unwrap_or(20).saturating_sub(1))
699 .collect();
700 init_matrix_prefixes(&mut values_mu, cols, &warmups);
701 let mut values = unsafe {
702 Vec::from_raw_parts(
703 values_mu.as_mut_ptr() as *mut f64,
704 values_mu.len(),
705 values_mu.capacity(),
706 )
707 };
708 std::mem::forget(values_mu);
709
710 debug_assert_eq!(values.len(), total);
711
712 donchian_channel_width_batch_inner_into(high, low, sweep, kernel, parallel, &mut values)?;
713
714 Ok(DonchianChannelWidthBatchOutput {
715 values,
716 combos,
717 rows,
718 cols,
719 })
720}
721
722fn donchian_channel_width_batch_inner_into(
723 high: &[f64],
724 low: &[f64],
725 sweep: &DonchianChannelWidthBatchRange,
726 kernel: Kernel,
727 parallel: bool,
728 out: &mut [f64],
729) -> Result<Vec<DonchianChannelWidthParams>, DonchianChannelWidthError> {
730 match kernel {
731 Kernel::Auto
732 | Kernel::Scalar
733 | Kernel::ScalarBatch
734 | Kernel::Avx2
735 | Kernel::Avx2Batch
736 | Kernel::Avx512
737 | Kernel::Avx512Batch => {}
738 other => return Err(DonchianChannelWidthError::InvalidKernelForBatch(other)),
739 }
740
741 let combos = expand_grid_checked(sweep)?;
742 let len = high.len();
743 if len == 0 || low.is_empty() {
744 return Err(DonchianChannelWidthError::EmptyInputData);
745 }
746 if len != low.len() {
747 return Err(DonchianChannelWidthError::InputLengthMismatch {
748 high_len: len,
749 low_len: low.len(),
750 });
751 }
752
753 let total =
754 combos
755 .len()
756 .checked_mul(len)
757 .ok_or_else(|| DonchianChannelWidthError::InvalidInput {
758 msg: "donchian_channel_width: rows*cols overflow in batch_into".to_string(),
759 })?;
760 if out.len() != total {
761 return Err(DonchianChannelWidthError::MismatchedOutputLen {
762 dst_len: out.len(),
763 expected_len: total,
764 });
765 }
766
767 let max_period = combos
768 .iter()
769 .map(|params| params.period.unwrap_or(20))
770 .max()
771 .unwrap_or(0);
772 validate_common(high, low, max_period)?;
773
774 let _chosen = match kernel {
775 Kernel::Auto => detect_best_batch_kernel(),
776 other => other,
777 };
778
779 let worker = |row: usize, dst: &mut [f64]| {
780 dst.fill(f64::NAN);
781 let period = combos[row].period.unwrap_or(20);
782 compute_row(high, low, period, dst);
783 };
784
785 #[cfg(not(target_arch = "wasm32"))]
786 if parallel {
787 out.par_chunks_mut(len)
788 .enumerate()
789 .for_each(|(row, dst)| worker(row, dst));
790 } else {
791 for (row, dst) in out.chunks_mut(len).enumerate() {
792 worker(row, dst);
793 }
794 }
795
796 #[cfg(target_arch = "wasm32")]
797 {
798 let _ = parallel;
799 for (row, dst) in out.chunks_mut(len).enumerate() {
800 worker(row, dst);
801 }
802 }
803
804 Ok(combos)
805}
806
807#[cfg(feature = "python")]
808#[pyfunction(name = "donchian_channel_width")]
809#[pyo3(signature = (high, low, period=20, kernel=None))]
810pub fn donchian_channel_width_py<'py>(
811 py: Python<'py>,
812 high: PyReadonlyArray1<'py, f64>,
813 low: PyReadonlyArray1<'py, f64>,
814 period: usize,
815 kernel: Option<&str>,
816) -> PyResult<Bound<'py, PyArray1<f64>>> {
817 let high = high.as_slice()?;
818 let low = low.as_slice()?;
819 let kern = validate_kernel(kernel, true)?;
820 let input = DonchianChannelWidthInput::from_slices(
821 high,
822 low,
823 DonchianChannelWidthParams {
824 period: Some(period),
825 },
826 );
827 let out = py
828 .allow_threads(|| donchian_channel_width_with_kernel(&input, kern))
829 .map_err(|e| PyValueError::new_err(e.to_string()))?;
830 Ok(out.values.into_pyarray(py))
831}
832
833#[cfg(feature = "python")]
834#[pyclass(name = "DonchianChannelWidthStream")]
835pub struct DonchianChannelWidthStreamPy {
836 stream: DonchianChannelWidthStream,
837}
838
839#[cfg(feature = "python")]
840#[pymethods]
841impl DonchianChannelWidthStreamPy {
842 #[new]
843 fn new(period: usize) -> PyResult<Self> {
844 let stream = DonchianChannelWidthStream::try_new(DonchianChannelWidthParams {
845 period: Some(period),
846 })
847 .map_err(|e| PyValueError::new_err(e.to_string()))?;
848 Ok(Self { stream })
849 }
850
851 fn update(&mut self, high: f64, low: f64) -> Option<f64> {
852 self.stream.update(high, low)
853 }
854
855 fn reset(&mut self) {
856 self.stream.reset();
857 }
858}
859
860#[cfg(feature = "python")]
861#[pyfunction(name = "donchian_channel_width_batch")]
862#[pyo3(signature = (high, low, period_range=(20,20,0), kernel=None))]
863pub fn donchian_channel_width_batch_py<'py>(
864 py: Python<'py>,
865 high: PyReadonlyArray1<'py, f64>,
866 low: PyReadonlyArray1<'py, f64>,
867 period_range: (usize, usize, usize),
868 kernel: Option<&str>,
869) -> PyResult<Bound<'py, PyDict>> {
870 let high = high.as_slice()?;
871 let low = low.as_slice()?;
872 let kern = validate_kernel(kernel, true)?;
873
874 let output = py
875 .allow_threads(|| {
876 donchian_channel_width_batch_with_kernel(
877 high,
878 low,
879 &DonchianChannelWidthBatchRange {
880 period: period_range,
881 },
882 kern,
883 )
884 })
885 .map_err(|e| PyValueError::new_err(e.to_string()))?;
886
887 let rows = output.rows;
888 let cols = output.cols;
889 let dict = PyDict::new(py);
890 dict.set_item(
891 "values",
892 output.values.into_pyarray(py).reshape((rows, cols))?,
893 )?;
894 dict.set_item(
895 "periods",
896 output
897 .combos
898 .iter()
899 .map(|params| params.period.unwrap_or(20) as u64)
900 .collect::<Vec<_>>()
901 .into_pyarray(py),
902 )?;
903 dict.set_item("rows", rows)?;
904 dict.set_item("cols", cols)?;
905 Ok(dict)
906}
907
908#[cfg(feature = "python")]
909pub fn register_donchian_channel_width_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
910 m.add_function(wrap_pyfunction!(donchian_channel_width_py, m)?)?;
911 m.add_function(wrap_pyfunction!(donchian_channel_width_batch_py, m)?)?;
912 m.add_class::<DonchianChannelWidthStreamPy>()?;
913 Ok(())
914}
915
916#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
917#[derive(Debug, Clone, Serialize, Deserialize)]
918pub struct DonchianChannelWidthBatchConfig {
919 pub period_range: Vec<usize>,
920}
921
922#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
923#[wasm_bindgen(js_name = donchian_channel_width_js)]
924pub fn donchian_channel_width_js(
925 high: &[f64],
926 low: &[f64],
927 period: usize,
928) -> Result<JsValue, JsValue> {
929 let input = DonchianChannelWidthInput::from_slices(
930 high,
931 low,
932 DonchianChannelWidthParams {
933 period: Some(period),
934 },
935 );
936 let out = donchian_channel_width_with_kernel(&input, Kernel::Auto)
937 .map_err(|e| JsValue::from_str(&e.to_string()))?;
938 serde_wasm_bindgen::to_value(&out.values).map_err(|e| JsValue::from_str(&e.to_string()))
939}
940
941#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
942#[wasm_bindgen(js_name = donchian_channel_width_batch_js)]
943pub fn donchian_channel_width_batch_js(
944 high: &[f64],
945 low: &[f64],
946 config: JsValue,
947) -> Result<JsValue, JsValue> {
948 let config: DonchianChannelWidthBatchConfig = serde_wasm_bindgen::from_value(config)
949 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
950 if config.period_range.len() != 3 {
951 return Err(JsValue::from_str(
952 "Invalid config: period_range must have exactly 3 elements [start, end, step]",
953 ));
954 }
955 let out = donchian_channel_width_batch_with_kernel(
956 high,
957 low,
958 &DonchianChannelWidthBatchRange {
959 period: (
960 config.period_range[0],
961 config.period_range[1],
962 config.period_range[2],
963 ),
964 },
965 Kernel::Auto,
966 )
967 .map_err(|e| JsValue::from_str(&e.to_string()))?;
968 let obj = js_sys::Object::new();
969 js_sys::Reflect::set(
970 &obj,
971 &JsValue::from_str("values"),
972 &serde_wasm_bindgen::to_value(&out.values).unwrap(),
973 )?;
974 js_sys::Reflect::set(
975 &obj,
976 &JsValue::from_str("rows"),
977 &JsValue::from_f64(out.rows as f64),
978 )?;
979 js_sys::Reflect::set(
980 &obj,
981 &JsValue::from_str("cols"),
982 &JsValue::from_f64(out.cols as f64),
983 )?;
984 js_sys::Reflect::set(
985 &obj,
986 &JsValue::from_str("combos"),
987 &serde_wasm_bindgen::to_value(&out.combos).unwrap(),
988 )?;
989 Ok(obj.into())
990}
991
992#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
993#[wasm_bindgen]
994pub fn donchian_channel_width_alloc(len: usize) -> *mut f64 {
995 let mut vec = Vec::<f64>::with_capacity(len);
996 let ptr = vec.as_mut_ptr();
997 std::mem::forget(vec);
998 ptr
999}
1000
1001#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1002#[wasm_bindgen]
1003pub fn donchian_channel_width_free(ptr: *mut f64, len: usize) {
1004 if !ptr.is_null() {
1005 unsafe {
1006 let _ = Vec::from_raw_parts(ptr, len, len);
1007 }
1008 }
1009}
1010
1011#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1012#[wasm_bindgen]
1013pub fn donchian_channel_width_into(
1014 high_ptr: *const f64,
1015 low_ptr: *const f64,
1016 out_ptr: *mut f64,
1017 len: usize,
1018 period: usize,
1019) -> Result<(), JsValue> {
1020 if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
1021 return Err(JsValue::from_str(
1022 "null pointer passed to donchian_channel_width_into",
1023 ));
1024 }
1025 unsafe {
1026 let high = std::slice::from_raw_parts(high_ptr, len);
1027 let low = std::slice::from_raw_parts(low_ptr, len);
1028 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1029 let input = DonchianChannelWidthInput::from_slices(
1030 high,
1031 low,
1032 DonchianChannelWidthParams {
1033 period: Some(period),
1034 },
1035 );
1036 donchian_channel_width_into_slice(out, &input, Kernel::Auto)
1037 .map_err(|e| JsValue::from_str(&e.to_string()))
1038 }
1039}
1040
1041#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1042#[wasm_bindgen]
1043pub fn donchian_channel_width_batch_into(
1044 high_ptr: *const f64,
1045 low_ptr: *const f64,
1046 out_ptr: *mut f64,
1047 len: usize,
1048 period_start: usize,
1049 period_end: usize,
1050 period_step: usize,
1051) -> Result<usize, JsValue> {
1052 if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
1053 return Err(JsValue::from_str(
1054 "null pointer passed to donchian_channel_width_batch_into",
1055 ));
1056 }
1057 let sweep = DonchianChannelWidthBatchRange {
1058 period: (period_start, period_end, period_step),
1059 };
1060 let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1061 let rows = combos.len();
1062 let total = rows.checked_mul(len).ok_or_else(|| {
1063 JsValue::from_str("rows*cols overflow in donchian_channel_width_batch_into")
1064 })?;
1065 unsafe {
1066 let high = std::slice::from_raw_parts(high_ptr, len);
1067 let low = std::slice::from_raw_parts(low_ptr, len);
1068 let out = std::slice::from_raw_parts_mut(out_ptr, total);
1069 donchian_channel_width_batch_inner_into(high, low, &sweep, Kernel::Auto, false, out)
1070 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1071 }
1072 Ok(rows)
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 use super::*;
1078 use crate::indicators::dispatch::{
1079 compute_cpu, IndicatorComputeRequest, IndicatorDataRef, ParamKV, ParamValue,
1080 };
1081
1082 fn sample_high_low(len: usize) -> (Vec<f64>, Vec<f64>) {
1083 let high: Vec<f64> = (0..len)
1084 .map(|i| {
1085 let x = i as f64;
1086 100.0 + x * 0.03 + (x * 0.11).sin() * 1.2 + (x * 0.017).cos() * 0.4
1087 })
1088 .collect();
1089 let low: Vec<f64> = high
1090 .iter()
1091 .enumerate()
1092 .map(|(i, &h)| h - 1.1 - ((i as f64) * 0.09).cos().abs() * 0.35)
1093 .collect();
1094 (high, low)
1095 }
1096
1097 fn naive_width(high: &[f64], low: &[f64], period: usize) -> Vec<f64> {
1098 let mut out = vec![f64::NAN; high.len()];
1099 let mut start = 0usize;
1100 while start < high.len() {
1101 while start < high.len() && !is_valid_pair(high[start], low[start]) {
1102 start += 1;
1103 }
1104 if start >= high.len() {
1105 break;
1106 }
1107 let mut end = start;
1108 while end < high.len() && is_valid_pair(high[end], low[end]) {
1109 end += 1;
1110 }
1111 if end - start >= period {
1112 for i in (start + period - 1)..end {
1113 let mut upper = f64::NEG_INFINITY;
1114 let mut lower = f64::INFINITY;
1115 for j in (i + 1 - period)..=i {
1116 upper = upper.max(high[j]);
1117 lower = lower.min(low[j]);
1118 }
1119 out[i] = upper - lower;
1120 }
1121 }
1122 start = end;
1123 }
1124 out
1125 }
1126
1127 fn assert_series_close(left: &[f64], right: &[f64], tol: f64) {
1128 assert_eq!(left.len(), right.len());
1129 for (a, b) in left.iter().zip(right.iter()) {
1130 if a.is_nan() || b.is_nan() {
1131 assert!(a.is_nan() && b.is_nan());
1132 } else {
1133 assert!((a - b).abs() <= tol, "left={a} right={b}");
1134 }
1135 }
1136 }
1137
1138 #[test]
1139 fn donchian_channel_width_matches_naive() -> Result<(), Box<dyn Error>> {
1140 let (high, low) = sample_high_low(256);
1141 let input = DonchianChannelWidthInput::from_slices(
1142 &high,
1143 &low,
1144 DonchianChannelWidthParams::default(),
1145 );
1146 let out = donchian_channel_width_with_kernel(&input, Kernel::Scalar)?;
1147 let expected = naive_width(&high, &low, 20);
1148 assert_series_close(&out.values, &expected, 1e-12);
1149 Ok(())
1150 }
1151
1152 #[test]
1153 fn donchian_channel_width_into_matches_api() -> Result<(), Box<dyn Error>> {
1154 let (high, low) = sample_high_low(200);
1155 let input = DonchianChannelWidthInput::from_slices(
1156 &high,
1157 &low,
1158 DonchianChannelWidthParams { period: Some(20) },
1159 );
1160 let base = donchian_channel_width(&input)?;
1161 let mut out = vec![f64::NAN; high.len()];
1162 donchian_channel_width_into_slice(&mut out, &input, Kernel::Auto)?;
1163 assert_series_close(&base.values, &out, 1e-12);
1164 Ok(())
1165 }
1166
1167 #[test]
1168 fn donchian_channel_width_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1169 let (high, low) = sample_high_low(256);
1170 let batch = donchian_channel_width(&DonchianChannelWidthInput::from_slices(
1171 &high,
1172 &low,
1173 DonchianChannelWidthParams { period: Some(20) },
1174 ))?;
1175
1176 let mut stream =
1177 DonchianChannelWidthStream::try_new(DonchianChannelWidthParams { period: Some(20) })?;
1178 let mut streamed = Vec::with_capacity(high.len());
1179 for (&h, &l) in high.iter().zip(low.iter()) {
1180 streamed.push(stream.update(h, l).unwrap_or(f64::NAN));
1181 }
1182 assert_series_close(&batch.values, &streamed, 1e-12);
1183 Ok(())
1184 }
1185
1186 #[test]
1187 fn donchian_channel_width_batch_single_matches_single() -> Result<(), Box<dyn Error>> {
1188 let (high, low) = sample_high_low(256);
1189 let single = donchian_channel_width(&DonchianChannelWidthInput::from_slices(
1190 &high,
1191 &low,
1192 DonchianChannelWidthParams { period: Some(20) },
1193 ))?;
1194 let batch = donchian_channel_width_batch_with_kernel(
1195 &high,
1196 &low,
1197 &DonchianChannelWidthBatchRange::default(),
1198 Kernel::Auto,
1199 )?;
1200 assert_eq!(batch.rows, 1);
1201 assert_eq!(batch.cols, high.len());
1202 assert_series_close(&single.values, &batch.values, 1e-12);
1203 Ok(())
1204 }
1205
1206 #[test]
1207 fn donchian_channel_width_rejects_invalid_params() {
1208 let (high, low) = sample_high_low(32);
1209 let err = donchian_channel_width(&DonchianChannelWidthInput::from_slices(
1210 &high,
1211 &low,
1212 DonchianChannelWidthParams { period: Some(0) },
1213 ))
1214 .unwrap_err();
1215 assert!(matches!(
1216 err,
1217 DonchianChannelWidthError::InvalidPeriod { .. }
1218 ));
1219
1220 let err = donchian_channel_width_batch_with_kernel(
1221 &high,
1222 &low,
1223 &DonchianChannelWidthBatchRange { period: (10, 5, 1) },
1224 Kernel::Auto,
1225 )
1226 .unwrap_err();
1227 assert!(matches!(
1228 err,
1229 DonchianChannelWidthError::InvalidRange { .. }
1230 ));
1231 }
1232
1233 #[test]
1234 fn donchian_channel_width_dispatch_compute_returns_value() -> Result<(), Box<dyn Error>> {
1235 let (high, low) = sample_high_low(192);
1236 let req = IndicatorComputeRequest {
1237 indicator_id: "donchian_channel_width",
1238 output_id: Some("value"),
1239 data: IndicatorDataRef::HighLow {
1240 high: &high,
1241 low: &low,
1242 },
1243 params: &[ParamKV {
1244 key: "period",
1245 value: ParamValue::Int(20),
1246 }],
1247 kernel: Kernel::Auto,
1248 };
1249 let out = compute_cpu(req)?;
1250 assert_eq!(out.output_id, "value");
1251 assert_eq!(out.rows, 1);
1252 assert_eq!(out.cols, high.len());
1253 Ok(())
1254 }
1255}