Skip to main content

vector_ta/indicators/
donchian_channel_width.rs

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}