Skip to main content

vector_ta/indicators/
normalized_volume_true_range.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, PyList};
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::Candles;
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{detect_best_batch_kernel, detect_best_kernel, make_uninit_matrix};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24use std::mem::ManuallyDrop;
25use thiserror::Error;
26
27const DEFAULT_OUTLIER_RANGE: f64 = 5.0;
28const DEFAULT_ATR_LENGTH: usize = 14;
29const DEFAULT_VOLUME_LENGTH: usize = 14;
30const MIN_LENGTH: usize = 2;
31const MIN_OUTLIER_RANGE: f64 = 0.5;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(
35    all(target_arch = "wasm32", feature = "wasm"),
36    derive(Serialize, Deserialize),
37    serde(rename_all = "snake_case")
38)]
39pub enum NormalizedVolumeTrueRangeStyle {
40    Body,
41    Hl,
42    Delta,
43}
44
45impl Default for NormalizedVolumeTrueRangeStyle {
46    fn default() -> Self {
47        Self::Body
48    }
49}
50
51impl NormalizedVolumeTrueRangeStyle {
52    #[inline(always)]
53    pub fn as_str(self) -> &'static str {
54        match self {
55            Self::Body => "body",
56            Self::Hl => "hl",
57            Self::Delta => "delta",
58        }
59    }
60}
61
62impl std::str::FromStr for NormalizedVolumeTrueRangeStyle {
63    type Err = String;
64
65    fn from_str(value: &str) -> Result<Self, Self::Err> {
66        match value.trim().to_ascii_lowercase().as_str() {
67            "body" => Ok(Self::Body),
68            "hl" | "high_low" | "high/low" => Ok(Self::Hl),
69            "delta" | "close_delta" | "close/close" => Ok(Self::Delta),
70            other => Err(format!(
71                "normalized_volume_true_range: invalid true_range_style: {other}"
72            )),
73        }
74    }
75}
76
77#[derive(Debug, Clone)]
78pub enum NormalizedVolumeTrueRangeData<'a> {
79    Candles {
80        candles: &'a Candles,
81    },
82    Slices {
83        open: &'a [f64],
84        high: &'a [f64],
85        low: &'a [f64],
86        close: &'a [f64],
87        volume: &'a [f64],
88    },
89}
90
91#[derive(Debug, Clone)]
92pub struct NormalizedVolumeTrueRangeOutput {
93    pub normalized_volume: Vec<f64>,
94    pub normalized_true_range: Vec<f64>,
95    pub baseline: Vec<f64>,
96    pub atr: Vec<f64>,
97    pub average_volume: Vec<f64>,
98}
99
100#[derive(Debug, Clone)]
101#[cfg_attr(
102    all(target_arch = "wasm32", feature = "wasm"),
103    derive(Serialize, Deserialize)
104)]
105pub struct NormalizedVolumeTrueRangeParams {
106    pub true_range_style: Option<NormalizedVolumeTrueRangeStyle>,
107    pub outlier_range: Option<f64>,
108    pub atr_length: Option<usize>,
109    pub volume_length: Option<usize>,
110}
111
112impl Default for NormalizedVolumeTrueRangeParams {
113    fn default() -> Self {
114        Self {
115            true_range_style: Some(NormalizedVolumeTrueRangeStyle::Body),
116            outlier_range: Some(DEFAULT_OUTLIER_RANGE),
117            atr_length: Some(DEFAULT_ATR_LENGTH),
118            volume_length: Some(DEFAULT_VOLUME_LENGTH),
119        }
120    }
121}
122
123#[derive(Debug, Clone)]
124pub struct NormalizedVolumeTrueRangeInput<'a> {
125    pub data: NormalizedVolumeTrueRangeData<'a>,
126    pub params: NormalizedVolumeTrueRangeParams,
127}
128
129impl<'a> NormalizedVolumeTrueRangeInput<'a> {
130    #[inline]
131    pub fn from_candles(candles: &'a Candles, params: NormalizedVolumeTrueRangeParams) -> Self {
132        Self {
133            data: NormalizedVolumeTrueRangeData::Candles { candles },
134            params,
135        }
136    }
137
138    #[inline]
139    pub fn from_slices(
140        open: &'a [f64],
141        high: &'a [f64],
142        low: &'a [f64],
143        close: &'a [f64],
144        volume: &'a [f64],
145        params: NormalizedVolumeTrueRangeParams,
146    ) -> Self {
147        Self {
148            data: NormalizedVolumeTrueRangeData::Slices {
149                open,
150                high,
151                low,
152                close,
153                volume,
154            },
155            params,
156        }
157    }
158
159    #[inline]
160    pub fn with_default_candles(candles: &'a Candles) -> Self {
161        Self::from_candles(candles, NormalizedVolumeTrueRangeParams::default())
162    }
163
164    #[inline(always)]
165    pub fn get_true_range_style(&self) -> NormalizedVolumeTrueRangeStyle {
166        self.params.true_range_style.unwrap_or_default()
167    }
168
169    #[inline(always)]
170    pub fn get_outlier_range(&self) -> f64 {
171        self.params.outlier_range.unwrap_or(DEFAULT_OUTLIER_RANGE)
172    }
173
174    #[inline(always)]
175    pub fn get_atr_length(&self) -> usize {
176        self.params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH)
177    }
178
179    #[inline(always)]
180    pub fn get_volume_length(&self) -> usize {
181        self.params.volume_length.unwrap_or(DEFAULT_VOLUME_LENGTH)
182    }
183}
184
185#[derive(Copy, Clone, Debug)]
186pub struct NormalizedVolumeTrueRangeBuilder {
187    true_range_style: Option<NormalizedVolumeTrueRangeStyle>,
188    outlier_range: Option<f64>,
189    atr_length: Option<usize>,
190    volume_length: Option<usize>,
191    kernel: Kernel,
192}
193
194impl Default for NormalizedVolumeTrueRangeBuilder {
195    fn default() -> Self {
196        Self {
197            true_range_style: None,
198            outlier_range: None,
199            atr_length: None,
200            volume_length: None,
201            kernel: Kernel::Auto,
202        }
203    }
204}
205
206impl NormalizedVolumeTrueRangeBuilder {
207    #[inline(always)]
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    #[inline(always)]
213    pub fn true_range_style(mut self, style: NormalizedVolumeTrueRangeStyle) -> Self {
214        self.true_range_style = Some(style);
215        self
216    }
217
218    #[inline(always)]
219    pub fn outlier_range(mut self, outlier_range: f64) -> Self {
220        self.outlier_range = Some(outlier_range);
221        self
222    }
223
224    #[inline(always)]
225    pub fn atr_length(mut self, atr_length: usize) -> Self {
226        self.atr_length = Some(atr_length);
227        self
228    }
229
230    #[inline(always)]
231    pub fn volume_length(mut self, volume_length: usize) -> Self {
232        self.volume_length = Some(volume_length);
233        self
234    }
235
236    #[inline(always)]
237    pub fn kernel(mut self, kernel: Kernel) -> Self {
238        self.kernel = kernel;
239        self
240    }
241
242    #[inline(always)]
243    fn params(self) -> NormalizedVolumeTrueRangeParams {
244        NormalizedVolumeTrueRangeParams {
245            true_range_style: self.true_range_style,
246            outlier_range: self.outlier_range,
247            atr_length: self.atr_length,
248            volume_length: self.volume_length,
249        }
250    }
251
252    #[inline(always)]
253    pub fn apply(
254        self,
255        candles: &Candles,
256    ) -> Result<NormalizedVolumeTrueRangeOutput, NormalizedVolumeTrueRangeError> {
257        let input = NormalizedVolumeTrueRangeInput::from_candles(candles, self.params());
258        normalized_volume_true_range_with_kernel(&input, self.kernel)
259    }
260
261    #[inline(always)]
262    pub fn apply_slices(
263        self,
264        open: &[f64],
265        high: &[f64],
266        low: &[f64],
267        close: &[f64],
268        volume: &[f64],
269    ) -> Result<NormalizedVolumeTrueRangeOutput, NormalizedVolumeTrueRangeError> {
270        let input = NormalizedVolumeTrueRangeInput::from_slices(
271            open,
272            high,
273            low,
274            close,
275            volume,
276            self.params(),
277        );
278        normalized_volume_true_range_with_kernel(&input, self.kernel)
279    }
280
281    #[inline(always)]
282    pub fn into_stream(
283        self,
284    ) -> Result<NormalizedVolumeTrueRangeStream, NormalizedVolumeTrueRangeError> {
285        NormalizedVolumeTrueRangeStream::try_new(self.params())
286    }
287}
288
289#[derive(Debug, Error)]
290pub enum NormalizedVolumeTrueRangeError {
291    #[error("normalized_volume_true_range: input data slice is empty.")]
292    EmptyInputData,
293    #[error("normalized_volume_true_range: all values are NaN.")]
294    AllValuesNaN,
295    #[error(
296        "normalized_volume_true_range: invalid outlier_range: {outlier_range}. Expected >= 0.5."
297    )]
298    InvalidOutlierRange { outlier_range: f64 },
299    #[error("normalized_volume_true_range: invalid atr_length: {atr_length}. Expected >= 2.")]
300    InvalidAtrLength { atr_length: usize },
301    #[error(
302        "normalized_volume_true_range: invalid volume_length: {volume_length}. Expected >= 2."
303    )]
304    InvalidVolumeLength { volume_length: usize },
305    #[error("normalized_volume_true_range: inconsistent slice lengths: open={open_len}, high={high_len}, low={low_len}, close={close_len}, volume={volume_len}")]
306    InconsistentSliceLengths {
307        open_len: usize,
308        high_len: usize,
309        low_len: usize,
310        close_len: usize,
311        volume_len: usize,
312    },
313    #[error(
314        "normalized_volume_true_range: output length mismatch: expected = {expected}, got = {got}"
315    )]
316    OutputLengthMismatch { expected: usize, got: usize },
317    #[error("normalized_volume_true_range: invalid outlier range sweep: start={start}, end={end}, step={step}")]
318    InvalidOutlierRangeSweep { start: f64, end: f64, step: f64 },
319    #[error("normalized_volume_true_range: invalid atr length sweep: start={start}, end={end}, step={step}")]
320    InvalidAtrLengthSweep {
321        start: usize,
322        end: usize,
323        step: usize,
324    },
325    #[error("normalized_volume_true_range: invalid volume length sweep: start={start}, end={end}, step={step}")]
326    InvalidVolumeLengthSweep {
327        start: usize,
328        end: usize,
329        step: usize,
330    },
331    #[error("normalized_volume_true_range: invalid kernel for batch: {0:?}")]
332    InvalidKernelForBatch(Kernel),
333}
334
335#[derive(Debug, Clone, Copy)]
336struct PreparedNormalizedVolumeTrueRange<'a> {
337    open: &'a [f64],
338    high: &'a [f64],
339    low: &'a [f64],
340    close: &'a [f64],
341    volume: &'a [f64],
342    len: usize,
343    style: NormalizedVolumeTrueRangeStyle,
344    outlier_range: f64,
345    atr_length: usize,
346    volume_length: usize,
347}
348
349#[derive(Debug, Clone)]
350struct PositiveDeviationState {
351    variance_sum: f64,
352    qualifying_count: usize,
353    current: f64,
354}
355
356impl Default for PositiveDeviationState {
357    fn default() -> Self {
358        Self {
359            variance_sum: 0.0,
360            qualifying_count: 0,
361            current: f64::NAN,
362        }
363    }
364}
365
366impl PositiveDeviationState {
367    #[inline(always)]
368    fn update(&mut self, source: f64, average: f64) -> f64 {
369        if source > average {
370            let delta = source - average;
371            self.variance_sum += delta * delta;
372            self.qualifying_count += 1;
373        }
374        if self.qualifying_count >= 2 {
375            self.current = (self.variance_sum / (self.qualifying_count - 1) as f64).sqrt();
376        }
377        self.current
378    }
379}
380
381#[derive(Debug, Clone)]
382struct FilledSmaState {
383    len: usize,
384    first_value: f64,
385    ready: bool,
386    ring: Vec<f64>,
387    head: usize,
388    sum: f64,
389}
390
391impl FilledSmaState {
392    #[inline]
393    fn new(len: usize) -> Self {
394        Self {
395            len,
396            first_value: f64::NAN,
397            ready: false,
398            ring: vec![0.0; len],
399            head: 0,
400            sum: 0.0,
401        }
402    }
403
404    #[inline]
405    fn update(&mut self, value: f64) -> f64 {
406        if !self.ready {
407            if !value.is_finite() {
408                return f64::NAN;
409            }
410            self.first_value = value;
411            self.ready = true;
412            self.ring.fill(value);
413            self.sum = value * self.len as f64;
414            self.head = 1 % self.len;
415            return value;
416        }
417
418        let sanitized = if value.is_finite() {
419            value
420        } else {
421            self.first_value
422        };
423        let old = self.ring[self.head];
424        self.ring[self.head] = sanitized;
425        self.sum += sanitized - old;
426        self.head += 1;
427        if self.head == self.len {
428            self.head = 0;
429        }
430        self.sum / self.len as f64
431    }
432}
433
434#[derive(Debug, Clone)]
435struct NormalizedVolumeTrueRangeCore {
436    style: NormalizedVolumeTrueRangeStyle,
437    outlier_range: f64,
438    abs_sum: f64,
439    volume_sum: f64,
440    count: usize,
441    abs_positive_deviation: PositiveDeviationState,
442    volume_positive_deviation: PositiveDeviationState,
443    atr_sma: FilledSmaState,
444    volume_sma: FilledSmaState,
445    prev_close: f64,
446    have_prev_close: bool,
447}
448
449impl NormalizedVolumeTrueRangeCore {
450    #[inline]
451    fn try_new(
452        params: &NormalizedVolumeTrueRangeParams,
453    ) -> Result<Self, NormalizedVolumeTrueRangeError> {
454        let style = params.true_range_style.unwrap_or_default();
455        let outlier_range = params.outlier_range.unwrap_or(DEFAULT_OUTLIER_RANGE);
456        let atr_length = params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
457        let volume_length = params.volume_length.unwrap_or(DEFAULT_VOLUME_LENGTH);
458
459        if !outlier_range.is_finite() || outlier_range < MIN_OUTLIER_RANGE {
460            return Err(NormalizedVolumeTrueRangeError::InvalidOutlierRange { outlier_range });
461        }
462        if atr_length < MIN_LENGTH {
463            return Err(NormalizedVolumeTrueRangeError::InvalidAtrLength { atr_length });
464        }
465        if volume_length < MIN_LENGTH {
466            return Err(NormalizedVolumeTrueRangeError::InvalidVolumeLength { volume_length });
467        }
468
469        Ok(Self {
470            style,
471            outlier_range,
472            abs_sum: 0.0,
473            volume_sum: 0.0,
474            count: 0,
475            abs_positive_deviation: PositiveDeviationState::default(),
476            volume_positive_deviation: PositiveDeviationState::default(),
477            atr_sma: FilledSmaState::new(atr_length),
478            volume_sma: FilledSmaState::new(volume_length),
479            prev_close: f64::NAN,
480            have_prev_close: false,
481        })
482    }
483
484    #[inline(always)]
485    fn update(
486        &mut self,
487        open: f64,
488        high: f64,
489        low: f64,
490        close: f64,
491        volume: f64,
492    ) -> Option<(f64, f64, f64, f64, f64)> {
493        let valid = match self.style {
494            NormalizedVolumeTrueRangeStyle::Body => {
495                open.is_finite() && close.is_finite() && volume.is_finite()
496            }
497            NormalizedVolumeTrueRangeStyle::Hl => {
498                high.is_finite() && low.is_finite() && volume.is_finite()
499            }
500            NormalizedVolumeTrueRangeStyle::Delta => close.is_finite() && volume.is_finite(),
501        };
502        if !valid {
503            if close.is_finite() {
504                self.prev_close = close;
505                self.have_prev_close = true;
506            }
507            return None;
508        }
509
510        let prev_close = if self.have_prev_close {
511            self.prev_close
512        } else {
513            close
514        };
515        let (start, finish) = match self.style {
516            NormalizedVolumeTrueRangeStyle::Body => (open, close),
517            NormalizedVolumeTrueRangeStyle::Hl => (low, high),
518            NormalizedVolumeTrueRangeStyle::Delta => (prev_close, close),
519        };
520
521        self.prev_close = close;
522        self.have_prev_close = true;
523
524        let denom = start.min(finish);
525        if !denom.is_finite() || denom <= 0.0 {
526            return None;
527        }
528
529        let abs_percent = (finish - start).abs() / denom;
530        if !abs_percent.is_finite() {
531            return None;
532        }
533
534        self.count += 1;
535        self.abs_sum += abs_percent;
536        self.volume_sum += volume;
537
538        let count_f64 = self.count as f64;
539        let avg_abs_percent = self.abs_sum / count_f64;
540        let avg_volume = self.volume_sum / count_f64;
541
542        let p_stdev_abs_percent = self
543            .abs_positive_deviation
544            .update(abs_percent, avg_abs_percent);
545        let p_stdev_volume = self.volume_positive_deviation.update(volume, avg_volume);
546
547        let abs_percent_max = if p_stdev_abs_percent.is_finite() {
548            avg_abs_percent + p_stdev_abs_percent * self.outlier_range
549        } else {
550            f64::NAN
551        };
552
553        let normalized_avg_percent = if abs_percent_max.is_finite() && abs_percent_max > 0.0 {
554            avg_abs_percent / abs_percent_max
555        } else {
556            f64::NAN
557        };
558
559        let scale_factor = if normalized_avg_percent.is_finite()
560            && normalized_avg_percent > 0.0
561            && normalized_avg_percent < 1.0
562            && p_stdev_volume.is_finite()
563            && p_stdev_volume > 0.0
564        {
565            avg_volume * (1.0 - normalized_avg_percent) / (normalized_avg_percent * p_stdev_volume)
566        } else {
567            f64::NAN
568        };
569
570        let max_volume = if scale_factor.is_finite() && p_stdev_volume.is_finite() {
571            avg_volume + p_stdev_volume * scale_factor
572        } else {
573            f64::NAN
574        };
575
576        let normalized_abs_percent = if abs_percent_max.is_finite() && abs_percent_max > 0.0 {
577            abs_percent.min(abs_percent_max) / abs_percent_max
578        } else {
579            f64::NAN
580        };
581
582        let normalized_volume_ratio = if max_volume.is_finite() && max_volume > 0.0 {
583            volume.min(max_volume) / max_volume
584        } else {
585            f64::NAN
586        };
587
588        let normalized_avg_volume_ratio = if max_volume.is_finite() && max_volume > 0.0 {
589            avg_volume / max_volume
590        } else {
591            f64::NAN
592        };
593
594        let normalized_volume = normalized_volume_ratio * 100.0;
595        let normalized_true_range = normalized_abs_percent * 100.0;
596        let baseline = normalized_avg_volume_ratio * 100.0;
597        let atr = self.atr_sma.update(normalized_true_range);
598        let average_volume = self.volume_sma.update(normalized_volume);
599
600        if !(normalized_volume.is_finite()
601            && normalized_true_range.is_finite()
602            && baseline.is_finite()
603            && atr.is_finite()
604            && average_volume.is_finite())
605        {
606            return None;
607        }
608
609        Some((
610            normalized_volume,
611            normalized_true_range,
612            baseline,
613            atr,
614            average_volume,
615        ))
616    }
617}
618
619#[inline(always)]
620fn normalize_single_kernel(kernel: Kernel) -> Kernel {
621    match kernel {
622        Kernel::Auto => detect_best_kernel(),
623        other => other,
624    }
625}
626
627#[inline(always)]
628fn normalize_single_kernel_to_scalar(kernel: Kernel) -> Kernel {
629    match normalize_single_kernel(kernel) {
630        Kernel::Auto
631        | Kernel::Scalar
632        | Kernel::ScalarBatch
633        | Kernel::Avx2
634        | Kernel::Avx2Batch
635        | Kernel::Avx512
636        | Kernel::Avx512Batch => Kernel::Scalar,
637    }
638}
639
640#[inline(always)]
641fn validate_params(
642    params: &NormalizedVolumeTrueRangeParams,
643) -> Result<(), NormalizedVolumeTrueRangeError> {
644    let _ = NormalizedVolumeTrueRangeCore::try_new(params)?;
645    Ok(())
646}
647
648#[inline(always)]
649fn prepare_input<'a>(
650    input: &'a NormalizedVolumeTrueRangeInput<'a>,
651) -> Result<PreparedNormalizedVolumeTrueRange<'a>, NormalizedVolumeTrueRangeError> {
652    let (open, high, low, close, volume) = match &input.data {
653        NormalizedVolumeTrueRangeData::Candles { candles } => (
654            candles.open.as_slice(),
655            candles.high.as_slice(),
656            candles.low.as_slice(),
657            candles.close.as_slice(),
658            candles.volume.as_slice(),
659        ),
660        NormalizedVolumeTrueRangeData::Slices {
661            open,
662            high,
663            low,
664            close,
665            volume,
666        } => {
667            if open.len() != high.len()
668                || high.len() != low.len()
669                || low.len() != close.len()
670                || close.len() != volume.len()
671            {
672                return Err(NormalizedVolumeTrueRangeError::InconsistentSliceLengths {
673                    open_len: open.len(),
674                    high_len: high.len(),
675                    low_len: low.len(),
676                    close_len: close.len(),
677                    volume_len: volume.len(),
678                });
679            }
680            (*open, *high, *low, *close, *volume)
681        }
682    };
683
684    let len = close.len();
685    if len == 0 {
686        return Err(NormalizedVolumeTrueRangeError::EmptyInputData);
687    }
688
689    validate_params(&input.params)?;
690
691    let style = input.get_true_range_style();
692    let any_valid = (0..len).any(|idx| match style {
693        NormalizedVolumeTrueRangeStyle::Body => {
694            open[idx].is_finite() && close[idx].is_finite() && volume[idx].is_finite()
695        }
696        NormalizedVolumeTrueRangeStyle::Hl => {
697            high[idx].is_finite() && low[idx].is_finite() && volume[idx].is_finite()
698        }
699        NormalizedVolumeTrueRangeStyle::Delta => close[idx].is_finite() && volume[idx].is_finite(),
700    });
701    if !any_valid {
702        return Err(NormalizedVolumeTrueRangeError::AllValuesNaN);
703    }
704
705    Ok(PreparedNormalizedVolumeTrueRange {
706        open,
707        high,
708        low,
709        close,
710        volume,
711        len,
712        style,
713        outlier_range: input.get_outlier_range(),
714        atr_length: input.get_atr_length(),
715        volume_length: input.get_volume_length(),
716    })
717}
718
719#[inline]
720fn ensure_output_len(expected: usize, got: usize) -> Result<(), NormalizedVolumeTrueRangeError> {
721    if expected == got {
722        Ok(())
723    } else {
724        Err(NormalizedVolumeTrueRangeError::OutputLengthMismatch { expected, got })
725    }
726}
727
728#[inline]
729fn compute_into_slices(
730    prepared: PreparedNormalizedVolumeTrueRange<'_>,
731    normalized_volume: &mut [f64],
732    normalized_true_range: &mut [f64],
733    baseline: &mut [f64],
734    atr: &mut [f64],
735    average_volume: &mut [f64],
736) -> Result<(), NormalizedVolumeTrueRangeError> {
737    let len = prepared.len;
738    ensure_output_len(len, normalized_volume.len())?;
739    ensure_output_len(len, normalized_true_range.len())?;
740    ensure_output_len(len, baseline.len())?;
741    ensure_output_len(len, atr.len())?;
742    ensure_output_len(len, average_volume.len())?;
743
744    normalized_volume.fill(f64::NAN);
745    normalized_true_range.fill(f64::NAN);
746    baseline.fill(f64::NAN);
747    atr.fill(f64::NAN);
748    average_volume.fill(f64::NAN);
749
750    let mut core = NormalizedVolumeTrueRangeCore::try_new(&NormalizedVolumeTrueRangeParams {
751        true_range_style: Some(prepared.style),
752        outlier_range: Some(prepared.outlier_range),
753        atr_length: Some(prepared.atr_length),
754        volume_length: Some(prepared.volume_length),
755    })?;
756
757    for idx in 0..len {
758        if let Some((nv, ntr, base, atr_value, avg_vol)) = core.update(
759            prepared.open[idx],
760            prepared.high[idx],
761            prepared.low[idx],
762            prepared.close[idx],
763            prepared.volume[idx],
764        ) {
765            normalized_volume[idx] = nv;
766            normalized_true_range[idx] = ntr;
767            baseline[idx] = base;
768            atr[idx] = atr_value;
769            average_volume[idx] = avg_vol;
770        }
771    }
772
773    Ok(())
774}
775
776#[inline]
777pub fn normalized_volume_true_range(
778    input: &NormalizedVolumeTrueRangeInput<'_>,
779) -> Result<NormalizedVolumeTrueRangeOutput, NormalizedVolumeTrueRangeError> {
780    normalized_volume_true_range_with_kernel(input, Kernel::Auto)
781}
782
783pub fn normalized_volume_true_range_with_kernel(
784    input: &NormalizedVolumeTrueRangeInput<'_>,
785    kernel: Kernel,
786) -> Result<NormalizedVolumeTrueRangeOutput, NormalizedVolumeTrueRangeError> {
787    let _kernel = normalize_single_kernel_to_scalar(kernel);
788    let prepared = prepare_input(input)?;
789    let len = prepared.len;
790    let mut normalized_volume = vec![0.0; len];
791    let mut normalized_true_range = vec![0.0; len];
792    let mut baseline = vec![0.0; len];
793    let mut atr = vec![0.0; len];
794    let mut average_volume = vec![0.0; len];
795    compute_into_slices(
796        prepared,
797        &mut normalized_volume,
798        &mut normalized_true_range,
799        &mut baseline,
800        &mut atr,
801        &mut average_volume,
802    )?;
803    Ok(NormalizedVolumeTrueRangeOutput {
804        normalized_volume,
805        normalized_true_range,
806        baseline,
807        atr,
808        average_volume,
809    })
810}
811
812#[inline]
813pub fn normalized_volume_true_range_into_slice(
814    normalized_volume: &mut [f64],
815    normalized_true_range: &mut [f64],
816    baseline: &mut [f64],
817    atr: &mut [f64],
818    average_volume: &mut [f64],
819    input: &NormalizedVolumeTrueRangeInput<'_>,
820    kernel: Kernel,
821) -> Result<(), NormalizedVolumeTrueRangeError> {
822    let _kernel = normalize_single_kernel_to_scalar(kernel);
823    let prepared = prepare_input(input)?;
824    compute_into_slices(
825        prepared,
826        normalized_volume,
827        normalized_true_range,
828        baseline,
829        atr,
830        average_volume,
831    )
832}
833
834#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
835#[inline]
836pub fn normalized_volume_true_range_into(
837    input: &NormalizedVolumeTrueRangeInput<'_>,
838    normalized_volume: &mut [f64],
839    normalized_true_range: &mut [f64],
840    baseline: &mut [f64],
841    atr: &mut [f64],
842    average_volume: &mut [f64],
843) -> Result<(), NormalizedVolumeTrueRangeError> {
844    normalized_volume_true_range_into_slice(
845        normalized_volume,
846        normalized_true_range,
847        baseline,
848        atr,
849        average_volume,
850        input,
851        Kernel::Auto,
852    )
853}
854
855#[derive(Debug, Clone)]
856pub struct NormalizedVolumeTrueRangeStream {
857    params: NormalizedVolumeTrueRangeParams,
858    core: NormalizedVolumeTrueRangeCore,
859}
860
861impl NormalizedVolumeTrueRangeStream {
862    pub fn try_new(
863        params: NormalizedVolumeTrueRangeParams,
864    ) -> Result<Self, NormalizedVolumeTrueRangeError> {
865        let core = NormalizedVolumeTrueRangeCore::try_new(&params)?;
866        Ok(Self { params, core })
867    }
868
869    #[inline]
870    pub fn update(
871        &mut self,
872        open: f64,
873        high: f64,
874        low: f64,
875        close: f64,
876        volume: f64,
877    ) -> Option<(f64, f64, f64, f64, f64)> {
878        self.core.update(open, high, low, close, volume)
879    }
880
881    pub fn reset(&mut self) {
882        self.core = NormalizedVolumeTrueRangeCore::try_new(&self.params)
883            .expect("normalized_volume_true_range stream reset should revalidate existing params");
884    }
885}
886
887#[derive(Debug, Clone, Copy)]
888pub struct NormalizedVolumeTrueRangeBatchRange {
889    pub outlier_range: (f64, f64, f64),
890    pub atr_length: (usize, usize, usize),
891    pub volume_length: (usize, usize, usize),
892    pub true_range_style: Option<NormalizedVolumeTrueRangeStyle>,
893}
894
895impl Default for NormalizedVolumeTrueRangeBatchRange {
896    fn default() -> Self {
897        Self {
898            outlier_range: (DEFAULT_OUTLIER_RANGE, DEFAULT_OUTLIER_RANGE, 0.0),
899            atr_length: (DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0),
900            volume_length: (DEFAULT_VOLUME_LENGTH, DEFAULT_VOLUME_LENGTH, 0),
901            true_range_style: Some(NormalizedVolumeTrueRangeStyle::Body),
902        }
903    }
904}
905
906#[derive(Debug, Clone, Copy)]
907pub struct NormalizedVolumeTrueRangeBatchBuilder {
908    range: NormalizedVolumeTrueRangeBatchRange,
909    kernel: Kernel,
910}
911
912impl Default for NormalizedVolumeTrueRangeBatchBuilder {
913    fn default() -> Self {
914        Self {
915            range: NormalizedVolumeTrueRangeBatchRange::default(),
916            kernel: Kernel::Auto,
917        }
918    }
919}
920
921impl NormalizedVolumeTrueRangeBatchBuilder {
922    pub fn new() -> Self {
923        Self::default()
924    }
925
926    pub fn outlier_range_range(mut self, start: f64, end: f64, step: f64) -> Self {
927        self.range.outlier_range = (start, end, step);
928        self
929    }
930
931    pub fn outlier_range_static(mut self, outlier_range: f64) -> Self {
932        self.range.outlier_range = (outlier_range, outlier_range, 0.0);
933        self
934    }
935
936    pub fn atr_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
937        self.range.atr_length = (start, end, step);
938        self
939    }
940
941    pub fn atr_length_static(mut self, atr_length: usize) -> Self {
942        self.range.atr_length = (atr_length, atr_length, 0);
943        self
944    }
945
946    pub fn volume_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
947        self.range.volume_length = (start, end, step);
948        self
949    }
950
951    pub fn volume_length_static(mut self, volume_length: usize) -> Self {
952        self.range.volume_length = (volume_length, volume_length, 0);
953        self
954    }
955
956    pub fn true_range_style(mut self, style: NormalizedVolumeTrueRangeStyle) -> Self {
957        self.range.true_range_style = Some(style);
958        self
959    }
960
961    pub fn kernel(mut self, kernel: Kernel) -> Self {
962        self.kernel = kernel;
963        self
964    }
965
966    pub fn apply_slices(
967        self,
968        open: &[f64],
969        high: &[f64],
970        low: &[f64],
971        close: &[f64],
972        volume: &[f64],
973    ) -> Result<NormalizedVolumeTrueRangeBatchOutput, NormalizedVolumeTrueRangeError> {
974        normalized_volume_true_range_batch_with_kernel(
975            open,
976            high,
977            low,
978            close,
979            volume,
980            &self.range,
981            self.kernel,
982        )
983    }
984
985    pub fn apply(
986        self,
987        candles: &Candles,
988    ) -> Result<NormalizedVolumeTrueRangeBatchOutput, NormalizedVolumeTrueRangeError> {
989        normalized_volume_true_range_batch_with_kernel(
990            candles.open.as_slice(),
991            candles.high.as_slice(),
992            candles.low.as_slice(),
993            candles.close.as_slice(),
994            candles.volume.as_slice(),
995            &self.range,
996            self.kernel,
997        )
998    }
999}
1000
1001#[derive(Debug, Clone)]
1002pub struct NormalizedVolumeTrueRangeBatchOutput {
1003    pub normalized_volume: Vec<f64>,
1004    pub normalized_true_range: Vec<f64>,
1005    pub baseline: Vec<f64>,
1006    pub atr: Vec<f64>,
1007    pub average_volume: Vec<f64>,
1008    pub combos: Vec<NormalizedVolumeTrueRangeParams>,
1009    pub rows: usize,
1010    pub cols: usize,
1011}
1012
1013impl NormalizedVolumeTrueRangeBatchOutput {
1014    pub fn row_for_params(&self, params: &NormalizedVolumeTrueRangeParams) -> Option<usize> {
1015        let outlier = params.outlier_range.unwrap_or(DEFAULT_OUTLIER_RANGE);
1016        let atr_length = params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
1017        let volume_length = params.volume_length.unwrap_or(DEFAULT_VOLUME_LENGTH);
1018        let style = params.true_range_style.unwrap_or_default();
1019        self.combos.iter().position(|combo| {
1020            (combo.outlier_range.unwrap_or(DEFAULT_OUTLIER_RANGE) - outlier).abs() <= 1e-12
1021                && combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH) == atr_length
1022                && combo.volume_length.unwrap_or(DEFAULT_VOLUME_LENGTH) == volume_length
1023                && combo.true_range_style.unwrap_or_default() == style
1024        })
1025    }
1026}
1027
1028pub fn expand_grid_normalized_volume_true_range(
1029    range: &NormalizedVolumeTrueRangeBatchRange,
1030) -> Result<Vec<NormalizedVolumeTrueRangeParams>, NormalizedVolumeTrueRangeError> {
1031    fn float_axis(
1032        (start, end, step): (f64, f64, f64),
1033    ) -> Result<Vec<f64>, NormalizedVolumeTrueRangeError> {
1034        if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1035            return Err(NormalizedVolumeTrueRangeError::InvalidOutlierRangeSweep {
1036                start,
1037                end,
1038                step,
1039            });
1040        }
1041        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
1042            return Ok(vec![start]);
1043        }
1044
1045        let mut values = Vec::new();
1046        let step_abs = step.abs();
1047        if start < end {
1048            let mut current = start;
1049            while current <= end + 1e-12 {
1050                values.push(current);
1051                current += step_abs;
1052            }
1053        } else {
1054            let mut current = start;
1055            while current + 1e-12 >= end {
1056                values.push(current);
1057                current -= step_abs;
1058            }
1059        }
1060
1061        if values.is_empty() {
1062            return Err(NormalizedVolumeTrueRangeError::InvalidOutlierRangeSweep {
1063                start,
1064                end,
1065                step,
1066            });
1067        }
1068        Ok(values)
1069    }
1070
1071    fn usize_axis(
1072        (start, end, step): (usize, usize, usize),
1073        make_error: fn(usize, usize, usize) -> NormalizedVolumeTrueRangeError,
1074    ) -> Result<Vec<usize>, NormalizedVolumeTrueRangeError> {
1075        if step == 0 || start == end {
1076            return Ok(vec![start]);
1077        }
1078        let mut values = Vec::new();
1079        if start < end {
1080            let mut current = start;
1081            while current <= end {
1082                values.push(current);
1083                match current.checked_add(step) {
1084                    Some(next) => current = next,
1085                    None => break,
1086                }
1087            }
1088        } else {
1089            let mut current = start;
1090            while current >= end {
1091                values.push(current);
1092                if current < step {
1093                    break;
1094                }
1095                current -= step;
1096            }
1097        }
1098        if values.is_empty() {
1099            return Err(make_error(start, end, step));
1100        }
1101        Ok(values)
1102    }
1103
1104    let styles = vec![range.true_range_style.unwrap_or_default()];
1105    let outlier_values = float_axis(range.outlier_range)?;
1106    let atr_values = usize_axis(range.atr_length, |start, end, step| {
1107        NormalizedVolumeTrueRangeError::InvalidAtrLengthSweep { start, end, step }
1108    })?;
1109    let volume_values = usize_axis(range.volume_length, |start, end, step| {
1110        NormalizedVolumeTrueRangeError::InvalidVolumeLengthSweep { start, end, step }
1111    })?;
1112
1113    let mut combos = Vec::with_capacity(
1114        styles.len() * outlier_values.len() * atr_values.len() * volume_values.len(),
1115    );
1116    for style in styles {
1117        for &outlier_range in &outlier_values {
1118            for &atr_length in &atr_values {
1119                for &volume_length in &volume_values {
1120                    let params = NormalizedVolumeTrueRangeParams {
1121                        true_range_style: Some(style),
1122                        outlier_range: Some(outlier_range),
1123                        atr_length: Some(atr_length),
1124                        volume_length: Some(volume_length),
1125                    };
1126                    validate_params(&params)?;
1127                    combos.push(params);
1128                }
1129            }
1130        }
1131    }
1132    Ok(combos)
1133}
1134
1135#[inline(always)]
1136pub fn normalized_volume_true_range_batch_slice(
1137    open: &[f64],
1138    high: &[f64],
1139    low: &[f64],
1140    close: &[f64],
1141    volume: &[f64],
1142    sweep: &NormalizedVolumeTrueRangeBatchRange,
1143) -> Result<NormalizedVolumeTrueRangeBatchOutput, NormalizedVolumeTrueRangeError> {
1144    normalized_volume_true_range_batch_inner(
1145        open,
1146        high,
1147        low,
1148        close,
1149        volume,
1150        sweep,
1151        Kernel::Scalar,
1152        false,
1153    )
1154}
1155
1156#[inline(always)]
1157pub fn normalized_volume_true_range_batch_par_slice(
1158    open: &[f64],
1159    high: &[f64],
1160    low: &[f64],
1161    close: &[f64],
1162    volume: &[f64],
1163    sweep: &NormalizedVolumeTrueRangeBatchRange,
1164) -> Result<NormalizedVolumeTrueRangeBatchOutput, NormalizedVolumeTrueRangeError> {
1165    normalized_volume_true_range_batch_inner(
1166        open,
1167        high,
1168        low,
1169        close,
1170        volume,
1171        sweep,
1172        Kernel::Scalar,
1173        true,
1174    )
1175}
1176
1177pub fn normalized_volume_true_range_batch_with_kernel(
1178    open: &[f64],
1179    high: &[f64],
1180    low: &[f64],
1181    close: &[f64],
1182    volume: &[f64],
1183    sweep: &NormalizedVolumeTrueRangeBatchRange,
1184    kernel: Kernel,
1185) -> Result<NormalizedVolumeTrueRangeBatchOutput, NormalizedVolumeTrueRangeError> {
1186    let batch_kernel = match kernel {
1187        Kernel::Auto => detect_best_batch_kernel(),
1188        other if other.is_batch() => other,
1189        other => return Err(NormalizedVolumeTrueRangeError::InvalidKernelForBatch(other)),
1190    };
1191    let scalar_kernel = match batch_kernel {
1192        Kernel::ScalarBatch => Kernel::Scalar,
1193        Kernel::Avx2Batch => Kernel::Avx2,
1194        Kernel::Avx512Batch => Kernel::Avx512,
1195        _ => unreachable!(),
1196    };
1197    normalized_volume_true_range_batch_inner(
1198        open,
1199        high,
1200        low,
1201        close,
1202        volume,
1203        sweep,
1204        scalar_kernel,
1205        !matches!(batch_kernel, Kernel::ScalarBatch),
1206    )
1207}
1208
1209fn normalized_volume_true_range_batch_inner(
1210    open: &[f64],
1211    high: &[f64],
1212    low: &[f64],
1213    close: &[f64],
1214    volume: &[f64],
1215    sweep: &NormalizedVolumeTrueRangeBatchRange,
1216    kernel: Kernel,
1217    parallel: bool,
1218) -> Result<NormalizedVolumeTrueRangeBatchOutput, NormalizedVolumeTrueRangeError> {
1219    let _kernel = normalize_single_kernel_to_scalar(kernel);
1220    let input = NormalizedVolumeTrueRangeInput::from_slices(
1221        open,
1222        high,
1223        low,
1224        close,
1225        volume,
1226        NormalizedVolumeTrueRangeParams {
1227            true_range_style: sweep.true_range_style,
1228            outlier_range: Some(sweep.outlier_range.0),
1229            atr_length: Some(sweep.atr_length.0),
1230            volume_length: Some(sweep.volume_length.0),
1231        },
1232    );
1233    let prepared = prepare_input(&input)?;
1234    let combos = expand_grid_normalized_volume_true_range(sweep)?;
1235    let rows = combos.len();
1236    let cols = prepared.len;
1237
1238    let normalized_volume_mu = make_uninit_matrix(rows, cols);
1239    let normalized_true_range_mu = make_uninit_matrix(rows, cols);
1240    let baseline_mu = make_uninit_matrix(rows, cols);
1241    let atr_mu = make_uninit_matrix(rows, cols);
1242    let average_volume_mu = make_uninit_matrix(rows, cols);
1243
1244    let mut normalized_volume_guard = ManuallyDrop::new(normalized_volume_mu);
1245    let mut normalized_true_range_guard = ManuallyDrop::new(normalized_true_range_mu);
1246    let mut baseline_guard = ManuallyDrop::new(baseline_mu);
1247    let mut atr_guard = ManuallyDrop::new(atr_mu);
1248    let mut average_volume_guard = ManuallyDrop::new(average_volume_mu);
1249
1250    let normalized_volume_out: &mut [f64] = unsafe {
1251        core::slice::from_raw_parts_mut(
1252            normalized_volume_guard.as_mut_ptr() as *mut f64,
1253            normalized_volume_guard.len(),
1254        )
1255    };
1256    let normalized_true_range_out: &mut [f64] = unsafe {
1257        core::slice::from_raw_parts_mut(
1258            normalized_true_range_guard.as_mut_ptr() as *mut f64,
1259            normalized_true_range_guard.len(),
1260        )
1261    };
1262    let baseline_out: &mut [f64] = unsafe {
1263        core::slice::from_raw_parts_mut(
1264            baseline_guard.as_mut_ptr() as *mut f64,
1265            baseline_guard.len(),
1266        )
1267    };
1268    let atr_out: &mut [f64] = unsafe {
1269        core::slice::from_raw_parts_mut(atr_guard.as_mut_ptr() as *mut f64, atr_guard.len())
1270    };
1271    let average_volume_out: &mut [f64] = unsafe {
1272        core::slice::from_raw_parts_mut(
1273            average_volume_guard.as_mut_ptr() as *mut f64,
1274            average_volume_guard.len(),
1275        )
1276    };
1277
1278    let do_row = |row: usize,
1279                  normalized_volume_row: &mut [f64],
1280                  normalized_true_range_row: &mut [f64],
1281                  baseline_row: &mut [f64],
1282                  atr_row: &mut [f64],
1283                  average_volume_row: &mut [f64]| {
1284        let combo = &combos[row];
1285        let prepared_row = PreparedNormalizedVolumeTrueRange {
1286            open: prepared.open,
1287            high: prepared.high,
1288            low: prepared.low,
1289            close: prepared.close,
1290            volume: prepared.volume,
1291            len: cols,
1292            style: combo.true_range_style.unwrap_or_default(),
1293            outlier_range: combo.outlier_range.unwrap_or(DEFAULT_OUTLIER_RANGE),
1294            atr_length: combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH),
1295            volume_length: combo.volume_length.unwrap_or(DEFAULT_VOLUME_LENGTH),
1296        };
1297        compute_into_slices(
1298            prepared_row,
1299            normalized_volume_row,
1300            normalized_true_range_row,
1301            baseline_row,
1302            atr_row,
1303            average_volume_row,
1304        )
1305    };
1306
1307    if parallel {
1308        #[cfg(not(target_arch = "wasm32"))]
1309        {
1310            normalized_volume_out
1311                .par_chunks_mut(cols)
1312                .zip(normalized_true_range_out.par_chunks_mut(cols))
1313                .zip(baseline_out.par_chunks_mut(cols))
1314                .zip(atr_out.par_chunks_mut(cols))
1315                .zip(average_volume_out.par_chunks_mut(cols))
1316                .enumerate()
1317                .try_for_each(
1318                    |(
1319                        row,
1320                        (
1321                            (
1322                                ((normalized_volume_row, normalized_true_range_row), baseline_row),
1323                                atr_row,
1324                            ),
1325                            average_volume_row,
1326                        ),
1327                    )| {
1328                        do_row(
1329                            row,
1330                            normalized_volume_row,
1331                            normalized_true_range_row,
1332                            baseline_row,
1333                            atr_row,
1334                            average_volume_row,
1335                        )
1336                    },
1337                )?;
1338        }
1339
1340        #[cfg(target_arch = "wasm32")]
1341        {
1342            for (
1343                row,
1344                (
1345                    (((normalized_volume_row, normalized_true_range_row), baseline_row), atr_row),
1346                    average_volume_row,
1347                ),
1348            ) in normalized_volume_out
1349                .chunks_mut(cols)
1350                .zip(normalized_true_range_out.chunks_mut(cols))
1351                .zip(baseline_out.chunks_mut(cols))
1352                .zip(atr_out.chunks_mut(cols))
1353                .zip(average_volume_out.chunks_mut(cols))
1354                .enumerate()
1355            {
1356                do_row(
1357                    row,
1358                    normalized_volume_row,
1359                    normalized_true_range_row,
1360                    baseline_row,
1361                    atr_row,
1362                    average_volume_row,
1363                )?;
1364            }
1365        }
1366    } else {
1367        for (
1368            row,
1369            (
1370                (((normalized_volume_row, normalized_true_range_row), baseline_row), atr_row),
1371                average_volume_row,
1372            ),
1373        ) in normalized_volume_out
1374            .chunks_mut(cols)
1375            .zip(normalized_true_range_out.chunks_mut(cols))
1376            .zip(baseline_out.chunks_mut(cols))
1377            .zip(atr_out.chunks_mut(cols))
1378            .zip(average_volume_out.chunks_mut(cols))
1379            .enumerate()
1380        {
1381            do_row(
1382                row,
1383                normalized_volume_row,
1384                normalized_true_range_row,
1385                baseline_row,
1386                atr_row,
1387                average_volume_row,
1388            )?;
1389        }
1390    }
1391
1392    let normalized_volume = unsafe {
1393        Vec::from_raw_parts(
1394            normalized_volume_guard.as_mut_ptr() as *mut f64,
1395            normalized_volume_guard.len(),
1396            normalized_volume_guard.capacity(),
1397        )
1398    };
1399    let normalized_true_range = unsafe {
1400        Vec::from_raw_parts(
1401            normalized_true_range_guard.as_mut_ptr() as *mut f64,
1402            normalized_true_range_guard.len(),
1403            normalized_true_range_guard.capacity(),
1404        )
1405    };
1406    let baseline = unsafe {
1407        Vec::from_raw_parts(
1408            baseline_guard.as_mut_ptr() as *mut f64,
1409            baseline_guard.len(),
1410            baseline_guard.capacity(),
1411        )
1412    };
1413    let atr = unsafe {
1414        Vec::from_raw_parts(
1415            atr_guard.as_mut_ptr() as *mut f64,
1416            atr_guard.len(),
1417            atr_guard.capacity(),
1418        )
1419    };
1420    let average_volume = unsafe {
1421        Vec::from_raw_parts(
1422            average_volume_guard.as_mut_ptr() as *mut f64,
1423            average_volume_guard.len(),
1424            average_volume_guard.capacity(),
1425        )
1426    };
1427
1428    Ok(NormalizedVolumeTrueRangeBatchOutput {
1429        normalized_volume,
1430        normalized_true_range,
1431        baseline,
1432        atr,
1433        average_volume,
1434        combos,
1435        rows,
1436        cols,
1437    })
1438}
1439
1440#[cfg(feature = "python")]
1441fn parse_style_py(style: Option<&str>) -> PyResult<Option<NormalizedVolumeTrueRangeStyle>> {
1442    match style {
1443        Some(value) => value
1444            .parse::<NormalizedVolumeTrueRangeStyle>()
1445            .map(Some)
1446            .map_err(PyValueError::new_err),
1447        None => Ok(None),
1448    }
1449}
1450
1451#[cfg(feature = "python")]
1452#[pyfunction(name = "normalized_volume_true_range")]
1453#[pyo3(signature = (open, high, low, close, volume, true_range_style=None, outlier_range=None, atr_length=None, volume_length=None, *, kernel=None))]
1454pub fn normalized_volume_true_range_py<'py>(
1455    py: Python<'py>,
1456    open: PyReadonlyArray1<'py, f64>,
1457    high: PyReadonlyArray1<'py, f64>,
1458    low: PyReadonlyArray1<'py, f64>,
1459    close: PyReadonlyArray1<'py, f64>,
1460    volume: PyReadonlyArray1<'py, f64>,
1461    true_range_style: Option<&str>,
1462    outlier_range: Option<f64>,
1463    atr_length: Option<usize>,
1464    volume_length: Option<usize>,
1465    kernel: Option<&str>,
1466) -> PyResult<(
1467    Bound<'py, PyArray1<f64>>,
1468    Bound<'py, PyArray1<f64>>,
1469    Bound<'py, PyArray1<f64>>,
1470    Bound<'py, PyArray1<f64>>,
1471    Bound<'py, PyArray1<f64>>,
1472)> {
1473    let kernel = validate_kernel(kernel, false)?;
1474    let style = parse_style_py(true_range_style)?;
1475    let input = NormalizedVolumeTrueRangeInput::from_slices(
1476        open.as_slice()?,
1477        high.as_slice()?,
1478        low.as_slice()?,
1479        close.as_slice()?,
1480        volume.as_slice()?,
1481        NormalizedVolumeTrueRangeParams {
1482            true_range_style: style,
1483            outlier_range,
1484            atr_length,
1485            volume_length,
1486        },
1487    );
1488    let out = py
1489        .allow_threads(|| normalized_volume_true_range_with_kernel(&input, kernel))
1490        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1491    Ok((
1492        out.normalized_volume.into_pyarray(py),
1493        out.normalized_true_range.into_pyarray(py),
1494        out.baseline.into_pyarray(py),
1495        out.atr.into_pyarray(py),
1496        out.average_volume.into_pyarray(py),
1497    ))
1498}
1499
1500#[cfg(feature = "python")]
1501#[pyclass(name = "NormalizedVolumeTrueRangeStream")]
1502pub struct NormalizedVolumeTrueRangeStreamPy {
1503    inner: NormalizedVolumeTrueRangeStream,
1504}
1505
1506#[cfg(feature = "python")]
1507#[pymethods]
1508impl NormalizedVolumeTrueRangeStreamPy {
1509    #[new]
1510    #[pyo3(signature = (true_range_style=None, outlier_range=None, atr_length=None, volume_length=None))]
1511    pub fn new(
1512        true_range_style: Option<&str>,
1513        outlier_range: Option<f64>,
1514        atr_length: Option<usize>,
1515        volume_length: Option<usize>,
1516    ) -> PyResult<Self> {
1517        let inner = NormalizedVolumeTrueRangeStream::try_new(NormalizedVolumeTrueRangeParams {
1518            true_range_style: parse_style_py(true_range_style)?,
1519            outlier_range,
1520            atr_length,
1521            volume_length,
1522        })
1523        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1524        Ok(Self { inner })
1525    }
1526
1527    pub fn update(
1528        &mut self,
1529        open: f64,
1530        high: f64,
1531        low: f64,
1532        close: f64,
1533        volume: f64,
1534    ) -> Option<(f64, f64, f64, f64, f64)> {
1535        self.inner.update(open, high, low, close, volume)
1536    }
1537
1538    pub fn reset(&mut self) {
1539        self.inner.reset();
1540    }
1541}
1542
1543#[cfg(feature = "python")]
1544#[pyfunction(name = "normalized_volume_true_range_batch")]
1545#[pyo3(signature = (open, high, low, close, volume, outlier_range_range=None, atr_length_range=None, volume_length_range=None, true_range_style=None, *, kernel=None))]
1546pub fn normalized_volume_true_range_batch_py<'py>(
1547    py: Python<'py>,
1548    open: PyReadonlyArray1<'py, f64>,
1549    high: PyReadonlyArray1<'py, f64>,
1550    low: PyReadonlyArray1<'py, f64>,
1551    close: PyReadonlyArray1<'py, f64>,
1552    volume: PyReadonlyArray1<'py, f64>,
1553    outlier_range_range: Option<(f64, f64, f64)>,
1554    atr_length_range: Option<(usize, usize, usize)>,
1555    volume_length_range: Option<(usize, usize, usize)>,
1556    true_range_style: Option<&str>,
1557    kernel: Option<&str>,
1558) -> PyResult<Bound<'py, PyDict>> {
1559    let kernel = validate_kernel(kernel, true)?;
1560    let style = parse_style_py(true_range_style)?;
1561    let out = normalized_volume_true_range_batch_with_kernel(
1562        open.as_slice()?,
1563        high.as_slice()?,
1564        low.as_slice()?,
1565        close.as_slice()?,
1566        volume.as_slice()?,
1567        &NormalizedVolumeTrueRangeBatchRange {
1568            outlier_range: outlier_range_range.unwrap_or((
1569                DEFAULT_OUTLIER_RANGE,
1570                DEFAULT_OUTLIER_RANGE,
1571                0.0,
1572            )),
1573            atr_length: atr_length_range.unwrap_or((DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0)),
1574            volume_length: volume_length_range.unwrap_or((
1575                DEFAULT_VOLUME_LENGTH,
1576                DEFAULT_VOLUME_LENGTH,
1577                0,
1578            )),
1579            true_range_style: style,
1580        },
1581        kernel,
1582    )
1583    .map_err(|e| PyValueError::new_err(e.to_string()))?;
1584
1585    let dict = PyDict::new(py);
1586    dict.set_item(
1587        "normalized_volume",
1588        out.normalized_volume
1589            .into_pyarray(py)
1590            .reshape((out.rows, out.cols))?,
1591    )?;
1592    dict.set_item(
1593        "normalized_true_range",
1594        out.normalized_true_range
1595            .into_pyarray(py)
1596            .reshape((out.rows, out.cols))?,
1597    )?;
1598    dict.set_item(
1599        "baseline",
1600        out.baseline
1601            .into_pyarray(py)
1602            .reshape((out.rows, out.cols))?,
1603    )?;
1604    dict.set_item(
1605        "atr",
1606        out.atr.into_pyarray(py).reshape((out.rows, out.cols))?,
1607    )?;
1608    dict.set_item(
1609        "average_volume",
1610        out.average_volume
1611            .into_pyarray(py)
1612            .reshape((out.rows, out.cols))?,
1613    )?;
1614    dict.set_item(
1615        "outlier_ranges",
1616        out.combos
1617            .iter()
1618            .map(|combo| combo.outlier_range.unwrap_or(DEFAULT_OUTLIER_RANGE))
1619            .collect::<Vec<_>>()
1620            .into_pyarray(py),
1621    )?;
1622    dict.set_item(
1623        "atr_lengths",
1624        out.combos
1625            .iter()
1626            .map(|combo| combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH))
1627            .collect::<Vec<_>>()
1628            .into_pyarray(py),
1629    )?;
1630    dict.set_item(
1631        "volume_lengths",
1632        out.combos
1633            .iter()
1634            .map(|combo| combo.volume_length.unwrap_or(DEFAULT_VOLUME_LENGTH))
1635            .collect::<Vec<_>>()
1636            .into_pyarray(py),
1637    )?;
1638    dict.set_item(
1639        "true_range_styles",
1640        PyList::new(
1641            py,
1642            out.combos
1643                .iter()
1644                .map(|combo| combo.true_range_style.unwrap_or_default().as_str()),
1645        )?,
1646    )?;
1647    dict.set_item("rows", out.rows)?;
1648    dict.set_item("cols", out.cols)?;
1649    Ok(dict)
1650}
1651
1652#[cfg(feature = "python")]
1653pub fn register_normalized_volume_true_range_module(
1654    m: &Bound<'_, pyo3::types::PyModule>,
1655) -> PyResult<()> {
1656    m.add_function(wrap_pyfunction!(normalized_volume_true_range_py, m)?)?;
1657    m.add_function(wrap_pyfunction!(normalized_volume_true_range_batch_py, m)?)?;
1658    m.add_class::<NormalizedVolumeTrueRangeStreamPy>()?;
1659    Ok(())
1660}
1661
1662#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1663fn parse_style_js(
1664    style: Option<String>,
1665) -> Result<Option<NormalizedVolumeTrueRangeStyle>, JsValue> {
1666    match style {
1667        Some(value) => value
1668            .parse::<NormalizedVolumeTrueRangeStyle>()
1669            .map(Some)
1670            .map_err(|e| JsValue::from_str(&e)),
1671        None => Ok(None),
1672    }
1673}
1674
1675#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1676#[derive(Serialize, Deserialize)]
1677struct NormalizedVolumeTrueRangeJsOutput {
1678    normalized_volume: Vec<f64>,
1679    normalized_true_range: Vec<f64>,
1680    baseline: Vec<f64>,
1681    atr: Vec<f64>,
1682    average_volume: Vec<f64>,
1683}
1684
1685#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1686#[derive(Serialize, Deserialize)]
1687struct NormalizedVolumeTrueRangeStreamJsOutput {
1688    normalized_volume: f64,
1689    normalized_true_range: f64,
1690    baseline: f64,
1691    atr: f64,
1692    average_volume: f64,
1693}
1694
1695#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1696#[derive(Serialize, Deserialize)]
1697pub struct NormalizedVolumeTrueRangeBatchConfig {
1698    pub true_range_style: Option<String>,
1699    pub outlier_range_range: (f64, f64, f64),
1700    pub atr_length_range: (usize, usize, usize),
1701    pub volume_length_range: (usize, usize, usize),
1702}
1703
1704#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1705#[derive(Serialize, Deserialize)]
1706pub struct NormalizedVolumeTrueRangeBatchJsOutput {
1707    pub normalized_volume: Vec<f64>,
1708    pub normalized_true_range: Vec<f64>,
1709    pub baseline: Vec<f64>,
1710    pub atr: Vec<f64>,
1711    pub average_volume: Vec<f64>,
1712    pub combos: Vec<NormalizedVolumeTrueRangeParams>,
1713    pub rows: usize,
1714    pub cols: usize,
1715}
1716
1717#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1718#[wasm_bindgen(js_name = normalized_volume_true_range_js)]
1719pub fn normalized_volume_true_range_js(
1720    open: &[f64],
1721    high: &[f64],
1722    low: &[f64],
1723    close: &[f64],
1724    volume: &[f64],
1725    true_range_style: Option<String>,
1726    outlier_range: Option<f64>,
1727    atr_length: Option<usize>,
1728    volume_length: Option<usize>,
1729) -> Result<JsValue, JsValue> {
1730    let input = NormalizedVolumeTrueRangeInput::from_slices(
1731        open,
1732        high,
1733        low,
1734        close,
1735        volume,
1736        NormalizedVolumeTrueRangeParams {
1737            true_range_style: parse_style_js(true_range_style)?,
1738            outlier_range,
1739            atr_length,
1740            volume_length,
1741        },
1742    );
1743    let out =
1744        normalized_volume_true_range(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1745    serde_wasm_bindgen::to_value(&NormalizedVolumeTrueRangeJsOutput {
1746        normalized_volume: out.normalized_volume,
1747        normalized_true_range: out.normalized_true_range,
1748        baseline: out.baseline,
1749        atr: out.atr,
1750        average_volume: out.average_volume,
1751    })
1752    .map_err(|e| JsValue::from_str(&e.to_string()))
1753}
1754
1755#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1756#[wasm_bindgen(js_name = normalized_volume_true_range_batch)]
1757pub fn normalized_volume_true_range_batch_unified_js(
1758    open: &[f64],
1759    high: &[f64],
1760    low: &[f64],
1761    close: &[f64],
1762    volume: &[f64],
1763    config: JsValue,
1764) -> Result<JsValue, JsValue> {
1765    let config: NormalizedVolumeTrueRangeBatchConfig =
1766        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1767    let out = normalized_volume_true_range_batch_with_kernel(
1768        open,
1769        high,
1770        low,
1771        close,
1772        volume,
1773        &NormalizedVolumeTrueRangeBatchRange {
1774            true_range_style: parse_style_js(config.true_range_style)?,
1775            outlier_range: config.outlier_range_range,
1776            atr_length: config.atr_length_range,
1777            volume_length: config.volume_length_range,
1778        },
1779        Kernel::Auto,
1780    )
1781    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1782    serde_wasm_bindgen::to_value(&NormalizedVolumeTrueRangeBatchJsOutput {
1783        normalized_volume: out.normalized_volume,
1784        normalized_true_range: out.normalized_true_range,
1785        baseline: out.baseline,
1786        atr: out.atr,
1787        average_volume: out.average_volume,
1788        combos: out.combos,
1789        rows: out.rows,
1790        cols: out.cols,
1791    })
1792    .map_err(|e| JsValue::from_str(&e.to_string()))
1793}
1794
1795#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1796#[wasm_bindgen(js_name = normalized_volume_true_range_alloc)]
1797pub fn normalized_volume_true_range_alloc(len: usize) -> *mut f64 {
1798    let mut values = vec![0.0; len];
1799    let ptr = values.as_mut_ptr();
1800    std::mem::forget(values);
1801    ptr
1802}
1803
1804#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1805#[wasm_bindgen(js_name = normalized_volume_true_range_free)]
1806pub fn normalized_volume_true_range_free(ptr: *mut f64, len: usize) {
1807    if ptr.is_null() {
1808        return;
1809    }
1810    unsafe {
1811        drop(Vec::from_raw_parts(ptr, len, len));
1812    }
1813}
1814
1815#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1816#[wasm_bindgen(js_name = normalized_volume_true_range_into)]
1817pub fn normalized_volume_true_range_into(
1818    open_ptr: *const f64,
1819    high_ptr: *const f64,
1820    low_ptr: *const f64,
1821    close_ptr: *const f64,
1822    volume_ptr: *const f64,
1823    normalized_volume_ptr: *mut f64,
1824    normalized_true_range_ptr: *mut f64,
1825    baseline_ptr: *mut f64,
1826    atr_ptr: *mut f64,
1827    average_volume_ptr: *mut f64,
1828    len: usize,
1829    true_range_style: Option<String>,
1830    outlier_range: Option<f64>,
1831    atr_length: Option<usize>,
1832    volume_length: Option<usize>,
1833) -> Result<(), JsValue> {
1834    if open_ptr.is_null()
1835        || high_ptr.is_null()
1836        || low_ptr.is_null()
1837        || close_ptr.is_null()
1838        || volume_ptr.is_null()
1839        || normalized_volume_ptr.is_null()
1840        || normalized_true_range_ptr.is_null()
1841        || baseline_ptr.is_null()
1842        || atr_ptr.is_null()
1843        || average_volume_ptr.is_null()
1844    {
1845        return Err(JsValue::from_str(
1846            "null pointer passed to normalized_volume_true_range_into",
1847        ));
1848    }
1849
1850    unsafe {
1851        let open = std::slice::from_raw_parts(open_ptr, len);
1852        let high = std::slice::from_raw_parts(high_ptr, len);
1853        let low = std::slice::from_raw_parts(low_ptr, len);
1854        let close = std::slice::from_raw_parts(close_ptr, len);
1855        let volume = std::slice::from_raw_parts(volume_ptr, len);
1856        let input = NormalizedVolumeTrueRangeInput::from_slices(
1857            open,
1858            high,
1859            low,
1860            close,
1861            volume,
1862            NormalizedVolumeTrueRangeParams {
1863                true_range_style: parse_style_js(true_range_style)?,
1864                outlier_range,
1865                atr_length,
1866                volume_length,
1867            },
1868        );
1869
1870        let input_alias = [
1871            normalized_volume_ptr as *const f64,
1872            normalized_true_range_ptr as *const f64,
1873            baseline_ptr as *const f64,
1874            atr_ptr as *const f64,
1875            average_volume_ptr as *const f64,
1876        ]
1877        .iter()
1878        .any(|&ptr| {
1879            ptr == open_ptr
1880                || ptr == high_ptr
1881                || ptr == low_ptr
1882                || ptr == close_ptr
1883                || ptr == volume_ptr
1884        });
1885        let output_alias = normalized_volume_ptr == normalized_true_range_ptr
1886            || normalized_volume_ptr == baseline_ptr
1887            || normalized_volume_ptr == atr_ptr
1888            || normalized_volume_ptr == average_volume_ptr
1889            || normalized_true_range_ptr == baseline_ptr
1890            || normalized_true_range_ptr == atr_ptr
1891            || normalized_true_range_ptr == average_volume_ptr
1892            || baseline_ptr == atr_ptr
1893            || baseline_ptr == average_volume_ptr
1894            || atr_ptr == average_volume_ptr;
1895
1896        if input_alias || output_alias {
1897            let mut normalized_volume = vec![0.0; len];
1898            let mut normalized_true_range = vec![0.0; len];
1899            let mut baseline = vec![0.0; len];
1900            let mut atr = vec![0.0; len];
1901            let mut average_volume = vec![0.0; len];
1902            normalized_volume_true_range_into_slice(
1903                &mut normalized_volume,
1904                &mut normalized_true_range,
1905                &mut baseline,
1906                &mut atr,
1907                &mut average_volume,
1908                &input,
1909                Kernel::Auto,
1910            )
1911            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1912            std::slice::from_raw_parts_mut(normalized_volume_ptr, len)
1913                .copy_from_slice(&normalized_volume);
1914            std::slice::from_raw_parts_mut(normalized_true_range_ptr, len)
1915                .copy_from_slice(&normalized_true_range);
1916            std::slice::from_raw_parts_mut(baseline_ptr, len).copy_from_slice(&baseline);
1917            std::slice::from_raw_parts_mut(atr_ptr, len).copy_from_slice(&atr);
1918            std::slice::from_raw_parts_mut(average_volume_ptr, len)
1919                .copy_from_slice(&average_volume);
1920            return Ok(());
1921        }
1922
1923        normalized_volume_true_range_into_slice(
1924            std::slice::from_raw_parts_mut(normalized_volume_ptr, len),
1925            std::slice::from_raw_parts_mut(normalized_true_range_ptr, len),
1926            std::slice::from_raw_parts_mut(baseline_ptr, len),
1927            std::slice::from_raw_parts_mut(atr_ptr, len),
1928            std::slice::from_raw_parts_mut(average_volume_ptr, len),
1929            &input,
1930            Kernel::Auto,
1931        )
1932        .map_err(|e| JsValue::from_str(&e.to_string()))
1933    }
1934}
1935
1936#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1937#[wasm_bindgen(js_name = normalized_volume_true_range_batch_into)]
1938pub fn normalized_volume_true_range_batch_into(
1939    open_ptr: *const f64,
1940    high_ptr: *const f64,
1941    low_ptr: *const f64,
1942    close_ptr: *const f64,
1943    volume_ptr: *const f64,
1944    normalized_volume_ptr: *mut f64,
1945    normalized_true_range_ptr: *mut f64,
1946    baseline_ptr: *mut f64,
1947    atr_ptr: *mut f64,
1948    average_volume_ptr: *mut f64,
1949    len: usize,
1950    outlier_range_start: f64,
1951    outlier_range_end: f64,
1952    outlier_range_step: f64,
1953    atr_length_start: usize,
1954    atr_length_end: usize,
1955    atr_length_step: usize,
1956    volume_length_start: usize,
1957    volume_length_end: usize,
1958    volume_length_step: usize,
1959    true_range_style: Option<String>,
1960) -> Result<usize, JsValue> {
1961    if open_ptr.is_null()
1962        || high_ptr.is_null()
1963        || low_ptr.is_null()
1964        || close_ptr.is_null()
1965        || volume_ptr.is_null()
1966        || normalized_volume_ptr.is_null()
1967        || normalized_true_range_ptr.is_null()
1968        || baseline_ptr.is_null()
1969        || atr_ptr.is_null()
1970        || average_volume_ptr.is_null()
1971    {
1972        return Err(JsValue::from_str(
1973            "null pointer passed to normalized_volume_true_range_batch_into",
1974        ));
1975    }
1976
1977    unsafe {
1978        let open = std::slice::from_raw_parts(open_ptr, len);
1979        let high = std::slice::from_raw_parts(high_ptr, len);
1980        let low = std::slice::from_raw_parts(low_ptr, len);
1981        let close = std::slice::from_raw_parts(close_ptr, len);
1982        let volume = std::slice::from_raw_parts(volume_ptr, len);
1983        let sweep = NormalizedVolumeTrueRangeBatchRange {
1984            true_range_style: parse_style_js(true_range_style)?,
1985            outlier_range: (outlier_range_start, outlier_range_end, outlier_range_step),
1986            atr_length: (atr_length_start, atr_length_end, atr_length_step),
1987            volume_length: (volume_length_start, volume_length_end, volume_length_step),
1988        };
1989        let combos = expand_grid_normalized_volume_true_range(&sweep)
1990            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1991        let rows = combos.len();
1992        let total = rows
1993            .checked_mul(len)
1994            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1995
1996        let input_alias = [
1997            normalized_volume_ptr as *const f64,
1998            normalized_true_range_ptr as *const f64,
1999            baseline_ptr as *const f64,
2000            atr_ptr as *const f64,
2001            average_volume_ptr as *const f64,
2002        ]
2003        .iter()
2004        .any(|&ptr| {
2005            ptr == open_ptr
2006                || ptr == high_ptr
2007                || ptr == low_ptr
2008                || ptr == close_ptr
2009                || ptr == volume_ptr
2010        });
2011        let output_alias = normalized_volume_ptr == normalized_true_range_ptr
2012            || normalized_volume_ptr == baseline_ptr
2013            || normalized_volume_ptr == atr_ptr
2014            || normalized_volume_ptr == average_volume_ptr
2015            || normalized_true_range_ptr == baseline_ptr
2016            || normalized_true_range_ptr == atr_ptr
2017            || normalized_true_range_ptr == average_volume_ptr
2018            || baseline_ptr == atr_ptr
2019            || baseline_ptr == average_volume_ptr
2020            || atr_ptr == average_volume_ptr;
2021
2022        let out = normalized_volume_true_range_batch_with_kernel(
2023            open,
2024            high,
2025            low,
2026            close,
2027            volume,
2028            &sweep,
2029            Kernel::Auto,
2030        )
2031        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2032
2033        if input_alias || output_alias {
2034            std::slice::from_raw_parts_mut(normalized_volume_ptr, total)
2035                .copy_from_slice(&out.normalized_volume);
2036            std::slice::from_raw_parts_mut(normalized_true_range_ptr, total)
2037                .copy_from_slice(&out.normalized_true_range);
2038            std::slice::from_raw_parts_mut(baseline_ptr, total).copy_from_slice(&out.baseline);
2039            std::slice::from_raw_parts_mut(atr_ptr, total).copy_from_slice(&out.atr);
2040            std::slice::from_raw_parts_mut(average_volume_ptr, total)
2041                .copy_from_slice(&out.average_volume);
2042            return Ok(rows);
2043        }
2044
2045        std::slice::from_raw_parts_mut(normalized_volume_ptr, total)
2046            .copy_from_slice(&out.normalized_volume);
2047        std::slice::from_raw_parts_mut(normalized_true_range_ptr, total)
2048            .copy_from_slice(&out.normalized_true_range);
2049        std::slice::from_raw_parts_mut(baseline_ptr, total).copy_from_slice(&out.baseline);
2050        std::slice::from_raw_parts_mut(atr_ptr, total).copy_from_slice(&out.atr);
2051        std::slice::from_raw_parts_mut(average_volume_ptr, total)
2052            .copy_from_slice(&out.average_volume);
2053        Ok(rows)
2054    }
2055}
2056
2057#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2058#[wasm_bindgen]
2059pub struct NormalizedVolumeTrueRangeStreamWasm {
2060    inner: NormalizedVolumeTrueRangeStream,
2061}
2062
2063#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2064#[wasm_bindgen]
2065impl NormalizedVolumeTrueRangeStreamWasm {
2066    #[wasm_bindgen(constructor)]
2067    pub fn new(
2068        true_range_style: Option<String>,
2069        outlier_range: Option<f64>,
2070        atr_length: Option<usize>,
2071        volume_length: Option<usize>,
2072    ) -> Result<NormalizedVolumeTrueRangeStreamWasm, JsValue> {
2073        Ok(Self {
2074            inner: NormalizedVolumeTrueRangeStream::try_new(NormalizedVolumeTrueRangeParams {
2075                true_range_style: parse_style_js(true_range_style)?,
2076                outlier_range,
2077                atr_length,
2078                volume_length,
2079            })
2080            .map_err(|e| JsValue::from_str(&e.to_string()))?,
2081        })
2082    }
2083
2084    pub fn update(
2085        &mut self,
2086        open: f64,
2087        high: f64,
2088        low: f64,
2089        close: f64,
2090        volume: f64,
2091    ) -> Result<JsValue, JsValue> {
2092        match self.inner.update(open, high, low, close, volume) {
2093            Some((normalized_volume, normalized_true_range, baseline, atr, average_volume)) => {
2094                serde_wasm_bindgen::to_value(&NormalizedVolumeTrueRangeStreamJsOutput {
2095                    normalized_volume,
2096                    normalized_true_range,
2097                    baseline,
2098                    atr,
2099                    average_volume,
2100                })
2101                .map_err(|e| JsValue::from_str(&e.to_string()))
2102            }
2103            None => Ok(JsValue::NULL),
2104        }
2105    }
2106
2107    pub fn reset(&mut self) {
2108        self.inner.reset();
2109    }
2110}
2111
2112#[cfg(test)]
2113mod tests {
2114    use super::*;
2115    use crate::utilities::data_loader::read_candles_from_csv;
2116    use std::error::Error;
2117
2118    fn naive_reference(
2119        open: &[f64],
2120        high: &[f64],
2121        low: &[f64],
2122        close: &[f64],
2123        volume: &[f64],
2124        params: &NormalizedVolumeTrueRangeParams,
2125    ) -> NormalizedVolumeTrueRangeOutput {
2126        let len = close.len();
2127        let mut normalized_volume = vec![f64::NAN; len];
2128        let mut normalized_true_range = vec![f64::NAN; len];
2129        let mut baseline = vec![f64::NAN; len];
2130        let mut atr = vec![f64::NAN; len];
2131        let mut average_volume = vec![f64::NAN; len];
2132        let mut stream = NormalizedVolumeTrueRangeStream::try_new(params.clone()).unwrap();
2133        for idx in 0..len {
2134            if let Some((nv, ntr, base, atr_value, avg_vol)) =
2135                stream.update(open[idx], high[idx], low[idx], close[idx], volume[idx])
2136            {
2137                normalized_volume[idx] = nv;
2138                normalized_true_range[idx] = ntr;
2139                baseline[idx] = base;
2140                atr[idx] = atr_value;
2141                average_volume[idx] = avg_vol;
2142            }
2143        }
2144        NormalizedVolumeTrueRangeOutput {
2145            normalized_volume,
2146            normalized_true_range,
2147            baseline,
2148            atr,
2149            average_volume,
2150        }
2151    }
2152
2153    fn assert_close(actual: &[f64], expected: &[f64]) {
2154        assert_eq!(actual.len(), expected.len());
2155        for (lhs, rhs) in actual.iter().zip(expected.iter()) {
2156            assert!((lhs.is_nan() && rhs.is_nan()) || (lhs - rhs).abs() <= 1e-12);
2157        }
2158    }
2159
2160    fn sample_ohlcv() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
2161        let len = 256;
2162        let mut open = Vec::with_capacity(len);
2163        let mut high = Vec::with_capacity(len);
2164        let mut low = Vec::with_capacity(len);
2165        let mut close = Vec::with_capacity(len);
2166        let mut volume = Vec::with_capacity(len);
2167        let mut prev_close = 100.0;
2168        for idx in 0..len {
2169            let x = idx as f64;
2170            let drift = x * 0.015;
2171            let body = (x * 0.11).sin() * 1.6;
2172            let spread = 2.0 + (x * 0.07).cos().abs();
2173            let open_value = prev_close + (x * 0.03).sin() * 0.6;
2174            let close_value = 100.0 + drift + body;
2175            let high_value = open_value.max(close_value) + spread * 0.55;
2176            let low_value = open_value.min(close_value) - spread * 0.45;
2177            let volume_value = 1_000_000.0 + (x * 0.17).sin().abs() * 350_000.0 + x * 800.0;
2178            open.push(open_value);
2179            high.push(high_value);
2180            low.push(low_value);
2181            close.push(close_value);
2182            volume.push(volume_value);
2183            prev_close = close_value;
2184        }
2185        (open, high, low, close, volume)
2186    }
2187
2188    #[test]
2189    fn normalized_volume_true_range_matches_naive_body() -> Result<(), Box<dyn Error>> {
2190        let (open, high, low, close, volume) = sample_ohlcv();
2191        let params = NormalizedVolumeTrueRangeParams::default();
2192        let input = NormalizedVolumeTrueRangeInput::from_slices(
2193            &open,
2194            &high,
2195            &low,
2196            &close,
2197            &volume,
2198            params.clone(),
2199        );
2200        let actual = normalized_volume_true_range(&input)?;
2201        let expected = naive_reference(&open, &high, &low, &close, &volume, &params);
2202        assert_close(&actual.normalized_volume, &expected.normalized_volume);
2203        assert_close(
2204            &actual.normalized_true_range,
2205            &expected.normalized_true_range,
2206        );
2207        assert_close(&actual.baseline, &expected.baseline);
2208        assert_close(&actual.atr, &expected.atr);
2209        assert_close(&actual.average_volume, &expected.average_volume);
2210        Ok(())
2211    }
2212
2213    #[test]
2214    fn normalized_volume_true_range_into_matches_api() -> Result<(), Box<dyn Error>> {
2215        let (open, high, low, close, volume) = sample_ohlcv();
2216        let input = NormalizedVolumeTrueRangeInput::from_slices(
2217            &open,
2218            &high,
2219            &low,
2220            &close,
2221            &volume,
2222            NormalizedVolumeTrueRangeParams {
2223                true_range_style: Some(NormalizedVolumeTrueRangeStyle::Delta),
2224                outlier_range: Some(4.5),
2225                atr_length: Some(10),
2226                volume_length: Some(7),
2227            },
2228        );
2229        let expected = normalized_volume_true_range(&input)?;
2230        let mut normalized_volume = vec![0.0; close.len()];
2231        let mut normalized_true_range = vec![0.0; close.len()];
2232        let mut baseline = vec![0.0; close.len()];
2233        let mut atr = vec![0.0; close.len()];
2234        let mut average_volume = vec![0.0; close.len()];
2235        normalized_volume_true_range_into(
2236            &input,
2237            &mut normalized_volume,
2238            &mut normalized_true_range,
2239            &mut baseline,
2240            &mut atr,
2241            &mut average_volume,
2242        )?;
2243        assert_close(&normalized_volume, &expected.normalized_volume);
2244        assert_close(&normalized_true_range, &expected.normalized_true_range);
2245        assert_close(&baseline, &expected.baseline);
2246        assert_close(&atr, &expected.atr);
2247        assert_close(&average_volume, &expected.average_volume);
2248        Ok(())
2249    }
2250
2251    #[test]
2252    fn normalized_volume_true_range_batch_matches_single() -> Result<(), Box<dyn Error>> {
2253        let (open, high, low, close, volume) = sample_ohlcv();
2254        let batch = normalized_volume_true_range_batch_with_kernel(
2255            &open,
2256            &high,
2257            &low,
2258            &close,
2259            &volume,
2260            &NormalizedVolumeTrueRangeBatchRange {
2261                true_range_style: Some(NormalizedVolumeTrueRangeStyle::Body),
2262                outlier_range: (4.0, 5.0, 1.0),
2263                atr_length: (8, 10, 2),
2264                volume_length: (5, 5, 0),
2265            },
2266            Kernel::ScalarBatch,
2267        )?;
2268        for (row, combo) in batch.combos.iter().enumerate() {
2269            let single =
2270                normalized_volume_true_range(&NormalizedVolumeTrueRangeInput::from_slices(
2271                    &open,
2272                    &high,
2273                    &low,
2274                    &close,
2275                    &volume,
2276                    combo.clone(),
2277                ))?;
2278            let start = row * batch.cols;
2279            let end = start + batch.cols;
2280            assert_close(
2281                &batch.normalized_volume[start..end],
2282                &single.normalized_volume,
2283            );
2284            assert_close(
2285                &batch.normalized_true_range[start..end],
2286                &single.normalized_true_range,
2287            );
2288            assert_close(&batch.baseline[start..end], &single.baseline);
2289            assert_close(&batch.atr[start..end], &single.atr);
2290            assert_close(&batch.average_volume[start..end], &single.average_volume);
2291        }
2292        Ok(())
2293    }
2294
2295    #[test]
2296    fn normalized_volume_true_range_fixture_has_values() -> Result<(), Box<dyn Error>> {
2297        let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
2298        let out = normalized_volume_true_range(
2299            &NormalizedVolumeTrueRangeInput::with_default_candles(&candles),
2300        )?;
2301        assert_eq!(out.normalized_volume.len(), candles.close.len());
2302        assert!(out
2303            .normalized_volume
2304            .iter()
2305            .skip(32)
2306            .any(|value| value.is_finite()));
2307        assert!(out
2308            .normalized_true_range
2309            .iter()
2310            .skip(32)
2311            .any(|value| value.is_finite()));
2312        assert!(out.baseline.iter().skip(32).any(|value| value.is_finite()));
2313        assert!(out.atr.iter().skip(32).any(|value| value.is_finite()));
2314        assert!(out
2315            .average_volume
2316            .iter()
2317            .skip(32)
2318            .any(|value| value.is_finite()));
2319        Ok(())
2320    }
2321}