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