Skip to main content

vector_ta/indicators/
velocity_acceleration_indicator.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[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::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::convert::AsRef;
28use std::mem::ManuallyDrop;
29use thiserror::Error;
30
31const DEFAULT_LENGTH: usize = 21;
32const DEFAULT_SMOOTH_LENGTH: usize = 5;
33const DEFAULT_SOURCE: &str = "hlcc4";
34
35impl<'a> AsRef<[f64]> for VelocityAccelerationIndicatorInput<'a> {
36    #[inline(always)]
37    fn as_ref(&self) -> &[f64] {
38        match &self.data {
39            VelocityAccelerationIndicatorData::Slice(slice) => slice,
40            VelocityAccelerationIndicatorData::Candles { candles, source } => {
41                source_type(candles, source)
42            }
43        }
44    }
45}
46
47#[derive(Debug, Clone)]
48pub enum VelocityAccelerationIndicatorData<'a> {
49    Candles {
50        candles: &'a Candles,
51        source: &'a str,
52    },
53    Slice(&'a [f64]),
54}
55
56#[derive(Debug, Clone)]
57pub struct VelocityAccelerationIndicatorOutput {
58    pub values: Vec<f64>,
59}
60
61#[derive(Debug, Clone, PartialEq)]
62#[cfg_attr(
63    all(target_arch = "wasm32", feature = "wasm"),
64    derive(Serialize, Deserialize)
65)]
66pub struct VelocityAccelerationIndicatorParams {
67    pub length: Option<usize>,
68    pub smooth_length: Option<usize>,
69}
70
71impl Default for VelocityAccelerationIndicatorParams {
72    fn default() -> Self {
73        Self {
74            length: Some(DEFAULT_LENGTH),
75            smooth_length: Some(DEFAULT_SMOOTH_LENGTH),
76        }
77    }
78}
79
80#[derive(Debug, Clone)]
81pub struct VelocityAccelerationIndicatorInput<'a> {
82    pub data: VelocityAccelerationIndicatorData<'a>,
83    pub params: VelocityAccelerationIndicatorParams,
84}
85
86impl<'a> VelocityAccelerationIndicatorInput<'a> {
87    #[inline]
88    pub fn from_candles(
89        candles: &'a Candles,
90        source: &'a str,
91        params: VelocityAccelerationIndicatorParams,
92    ) -> Self {
93        Self {
94            data: VelocityAccelerationIndicatorData::Candles { candles, source },
95            params,
96        }
97    }
98
99    #[inline]
100    pub fn from_slice(slice: &'a [f64], params: VelocityAccelerationIndicatorParams) -> Self {
101        Self {
102            data: VelocityAccelerationIndicatorData::Slice(slice),
103            params,
104        }
105    }
106
107    #[inline]
108    pub fn with_default_candles(candles: &'a Candles) -> Self {
109        Self::from_candles(
110            candles,
111            DEFAULT_SOURCE,
112            VelocityAccelerationIndicatorParams::default(),
113        )
114    }
115
116    #[inline]
117    pub fn get_length(&self) -> usize {
118        self.params.length.unwrap_or(DEFAULT_LENGTH)
119    }
120
121    #[inline]
122    pub fn get_smooth_length(&self) -> usize {
123        self.params.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH)
124    }
125}
126
127#[derive(Copy, Clone, Debug)]
128pub struct VelocityAccelerationIndicatorBuilder {
129    length: Option<usize>,
130    smooth_length: Option<usize>,
131    kernel: Kernel,
132}
133
134impl Default for VelocityAccelerationIndicatorBuilder {
135    fn default() -> Self {
136        Self {
137            length: None,
138            smooth_length: None,
139            kernel: Kernel::Auto,
140        }
141    }
142}
143
144impl VelocityAccelerationIndicatorBuilder {
145    #[inline]
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    #[inline]
151    pub fn length(mut self, length: usize) -> Self {
152        self.length = Some(length);
153        self
154    }
155
156    #[inline]
157    pub fn smooth_length(mut self, smooth_length: usize) -> Self {
158        self.smooth_length = Some(smooth_length);
159        self
160    }
161
162    #[inline]
163    pub fn kernel(mut self, kernel: Kernel) -> Self {
164        self.kernel = kernel;
165        self
166    }
167
168    #[inline]
169    pub fn apply(
170        self,
171        candles: &Candles,
172        source: &str,
173    ) -> Result<VelocityAccelerationIndicatorOutput, VelocityAccelerationIndicatorError> {
174        let input = VelocityAccelerationIndicatorInput::from_candles(
175            candles,
176            source,
177            VelocityAccelerationIndicatorParams {
178                length: self.length,
179                smooth_length: self.smooth_length,
180            },
181        );
182        velocity_acceleration_indicator_with_kernel(&input, self.kernel)
183    }
184
185    #[inline]
186    pub fn apply_slice(
187        self,
188        data: &[f64],
189    ) -> Result<VelocityAccelerationIndicatorOutput, VelocityAccelerationIndicatorError> {
190        let input = VelocityAccelerationIndicatorInput::from_slice(
191            data,
192            VelocityAccelerationIndicatorParams {
193                length: self.length,
194                smooth_length: self.smooth_length,
195            },
196        );
197        velocity_acceleration_indicator_with_kernel(&input, self.kernel)
198    }
199
200    #[inline]
201    pub fn into_stream(
202        self,
203    ) -> Result<VelocityAccelerationIndicatorStream, VelocityAccelerationIndicatorError> {
204        VelocityAccelerationIndicatorStream::try_new(VelocityAccelerationIndicatorParams {
205            length: self.length,
206            smooth_length: self.smooth_length,
207        })
208    }
209}
210
211#[derive(Debug, Error)]
212pub enum VelocityAccelerationIndicatorError {
213    #[error("velocity_acceleration_indicator: Input data slice is empty.")]
214    EmptyInputData,
215    #[error("velocity_acceleration_indicator: All values are NaN.")]
216    AllValuesNaN,
217    #[error("velocity_acceleration_indicator: Invalid length: {length}")]
218    InvalidLength { length: usize },
219    #[error("velocity_acceleration_indicator: Invalid smooth_length: {smooth_length}")]
220    InvalidSmoothLength { smooth_length: usize },
221    #[error(
222        "velocity_acceleration_indicator: Not enough valid data: needed = {needed}, valid = {valid}"
223    )]
224    NotEnoughValidData { needed: usize, valid: usize },
225    #[error(
226        "velocity_acceleration_indicator: Output length mismatch: expected = {expected}, got = {got}"
227    )]
228    OutputLengthMismatch { expected: usize, got: usize },
229    #[error(
230        "velocity_acceleration_indicator: Invalid range: start={start}, end={end}, step={step}"
231    )]
232    InvalidRange {
233        start: String,
234        end: String,
235        step: String,
236    },
237    #[error("velocity_acceleration_indicator: Invalid kernel for batch: {0:?}")]
238    InvalidKernelForBatch(Kernel),
239}
240
241#[derive(Debug, Clone)]
242struct LagHistory {
243    values: Vec<f64>,
244    next: usize,
245    count: usize,
246}
247
248impl LagHistory {
249    #[inline]
250    fn new(size: usize) -> Self {
251        Self {
252            values: vec![0.0; size],
253            next: 0,
254            count: 0,
255        }
256    }
257
258    #[inline]
259    fn reset(&mut self) {
260        self.next = 0;
261        self.count = 0;
262    }
263
264    #[inline]
265    fn weighted_past_sum(&self) -> f64 {
266        let len = self.values.len();
267        let upto = self.count.min(len);
268        let mut sum = 0.0;
269        for lag in 1..=upto {
270            let idx = if self.next >= lag {
271                self.next - lag
272            } else {
273                len + self.next - lag
274            };
275            sum += self.values[idx] / lag as f64;
276        }
277        sum
278    }
279
280    #[inline]
281    fn push(&mut self, value: f64) {
282        if self.values.is_empty() {
283            return;
284        }
285        self.values[self.next] = value;
286        self.next += 1;
287        if self.next == self.values.len() {
288            self.next = 0;
289        }
290        if self.count < self.values.len() {
291            self.count += 1;
292        }
293    }
294}
295
296#[derive(Debug, Clone)]
297struct WmaState {
298    values: Vec<f64>,
299    next: usize,
300    count: usize,
301    sum: f64,
302    weighted_sum: f64,
303    denominator: f64,
304}
305
306impl WmaState {
307    #[inline]
308    fn new(length: usize) -> Self {
309        let length = length.max(1);
310        Self {
311            values: vec![0.0; length],
312            next: 0,
313            count: 0,
314            sum: 0.0,
315            weighted_sum: 0.0,
316            denominator: (length * (length + 1) / 2) as f64,
317        }
318    }
319
320    #[inline]
321    fn reset(&mut self) {
322        self.next = 0;
323        self.count = 0;
324        self.sum = 0.0;
325        self.weighted_sum = 0.0;
326    }
327
328    #[inline]
329    fn len(&self) -> usize {
330        self.values.len()
331    }
332
333    #[inline]
334    fn update(&mut self, value: f64) -> Option<f64> {
335        let len = self.len();
336        if len == 1 {
337            self.values[0] = value;
338            self.count = 1;
339            self.sum = value;
340            self.weighted_sum = value;
341            return Some(value);
342        }
343
344        if self.count < len {
345            self.values[self.next] = value;
346            self.count += 1;
347            self.next += 1;
348            if self.next == len {
349                self.next = 0;
350            }
351            self.sum += value;
352            self.weighted_sum += self.count as f64 * value;
353            if self.count < len {
354                None
355            } else {
356                Some(self.weighted_sum / self.denominator)
357            }
358        } else {
359            let old = self.values[self.next];
360            let prev_sum = self.sum;
361            self.values[self.next] = value;
362            self.next += 1;
363            if self.next == len {
364                self.next = 0;
365            }
366            self.sum = prev_sum - old + value;
367            self.weighted_sum = self.weighted_sum - prev_sum + len as f64 * value;
368            Some(self.weighted_sum / self.denominator)
369        }
370    }
371}
372
373#[derive(Debug, Clone)]
374pub struct VelocityAccelerationIndicatorStream {
375    harmonic_sum: f64,
376    inv_length: f64,
377    source_history: LagHistory,
378    wma: WmaState,
379    acceleration_history: LagHistory,
380}
381
382impl VelocityAccelerationIndicatorStream {
383    #[inline]
384    pub fn try_new(
385        params: VelocityAccelerationIndicatorParams,
386    ) -> Result<Self, VelocityAccelerationIndicatorError> {
387        let length = params.length.unwrap_or(DEFAULT_LENGTH);
388        if length < 2 {
389            return Err(VelocityAccelerationIndicatorError::InvalidLength { length });
390        }
391        let smooth_length = params.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH);
392        if smooth_length == 0 {
393            return Err(VelocityAccelerationIndicatorError::InvalidSmoothLength { smooth_length });
394        }
395
396        let mut harmonic_sum = 0.0;
397        for i in 1..=length {
398            harmonic_sum += 1.0 / i as f64;
399        }
400
401        Ok(Self {
402            harmonic_sum,
403            inv_length: 1.0 / length as f64,
404            source_history: LagHistory::new(length),
405            wma: WmaState::new(smooth_length),
406            acceleration_history: LagHistory::new(length),
407        })
408    }
409
410    #[inline]
411    pub fn reset(&mut self) {
412        self.source_history.reset();
413        self.wma.reset();
414        self.acceleration_history.reset();
415    }
416
417    #[inline]
418    pub fn get_warmup_period(&self) -> usize {
419        self.wma.len().saturating_sub(1)
420    }
421
422    #[inline]
423    pub fn update(&mut self, value: f64) -> Option<f64> {
424        if !value.is_finite() {
425            self.reset();
426            return None;
427        }
428
429        let velocity = velocity_value(
430            value,
431            &self.source_history,
432            self.harmonic_sum,
433            self.inv_length,
434        );
435        self.source_history.push(value);
436
437        let velocity_avg = self.wma.update(velocity)?;
438        let acceleration = velocity_value(
439            velocity_avg,
440            &self.acceleration_history,
441            self.harmonic_sum,
442            self.inv_length,
443        );
444        self.acceleration_history.push(velocity_avg);
445        Some(acceleration)
446    }
447}
448
449#[inline(always)]
450fn velocity_value(current: f64, history: &LagHistory, harmonic_sum: f64, inv_length: f64) -> f64 {
451    (current * harmonic_sum - history.weighted_past_sum()) * inv_length
452}
453
454#[inline(always)]
455fn first_valid_value(data: &[f64]) -> usize {
456    let mut i = 0usize;
457    while i < data.len() {
458        if data[i].is_finite() {
459            return i;
460        }
461        i += 1;
462    }
463    data.len()
464}
465
466#[inline(always)]
467fn max_consecutive_valid_values(data: &[f64]) -> usize {
468    let mut best = 0usize;
469    let mut run = 0usize;
470    for &value in data {
471        if value.is_finite() {
472            run += 1;
473            if run > best {
474                best = run;
475            }
476        } else {
477            run = 0;
478        }
479    }
480    best
481}
482
483#[inline(always)]
484fn resolve_params(
485    params: &VelocityAccelerationIndicatorParams,
486) -> Result<(usize, usize), VelocityAccelerationIndicatorError> {
487    let length = params.length.unwrap_or(DEFAULT_LENGTH);
488    if length < 2 {
489        return Err(VelocityAccelerationIndicatorError::InvalidLength { length });
490    }
491    let smooth_length = params.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH);
492    if smooth_length == 0 {
493        return Err(VelocityAccelerationIndicatorError::InvalidSmoothLength { smooth_length });
494    }
495    Ok((length, smooth_length))
496}
497
498#[inline(always)]
499fn velocity_acceleration_indicator_prepare<'a>(
500    input: &'a VelocityAccelerationIndicatorInput,
501    kernel: Kernel,
502) -> Result<(&'a [f64], usize, usize, usize, Kernel), VelocityAccelerationIndicatorError> {
503    let data = input.as_ref();
504    if data.is_empty() {
505        return Err(VelocityAccelerationIndicatorError::EmptyInputData);
506    }
507
508    let first = first_valid_value(data);
509    if first >= data.len() {
510        return Err(VelocityAccelerationIndicatorError::AllValuesNaN);
511    }
512
513    let (length, smooth_length) = resolve_params(&input.params)?;
514    let valid = max_consecutive_valid_values(data);
515    if valid < smooth_length {
516        return Err(VelocityAccelerationIndicatorError::NotEnoughValidData {
517            needed: smooth_length,
518            valid,
519        });
520    }
521
522    let chosen = match kernel {
523        Kernel::Auto => detect_best_kernel(),
524        other => other.to_non_batch(),
525    };
526    Ok((data, length, smooth_length, first, chosen))
527}
528
529#[inline(always)]
530fn compute_row(data: &[f64], length: usize, smooth_length: usize, out: &mut [f64]) {
531    let mut stream =
532        VelocityAccelerationIndicatorStream::try_new(VelocityAccelerationIndicatorParams {
533            length: Some(length),
534            smooth_length: Some(smooth_length),
535        })
536        .unwrap();
537    for (slot, &value) in out.iter_mut().zip(data.iter()) {
538        *slot = stream.update(value).unwrap_or(f64::NAN);
539    }
540}
541
542#[inline]
543pub fn velocity_acceleration_indicator(
544    input: &VelocityAccelerationIndicatorInput,
545) -> Result<VelocityAccelerationIndicatorOutput, VelocityAccelerationIndicatorError> {
546    velocity_acceleration_indicator_with_kernel(input, Kernel::Auto)
547}
548
549#[inline]
550pub fn velocity_acceleration_indicator_with_kernel(
551    input: &VelocityAccelerationIndicatorInput,
552    kernel: Kernel,
553) -> Result<VelocityAccelerationIndicatorOutput, VelocityAccelerationIndicatorError> {
554    let (data, length, smooth_length, first, _chosen) =
555        velocity_acceleration_indicator_prepare(input, kernel)?;
556    let mut values = alloc_with_nan_prefix(
557        data.len(),
558        first.saturating_add(smooth_length.saturating_sub(1)),
559    );
560    compute_row(data, length, smooth_length, &mut values);
561    Ok(VelocityAccelerationIndicatorOutput { values })
562}
563
564#[inline]
565pub fn velocity_acceleration_indicator_into_slice(
566    out: &mut [f64],
567    input: &VelocityAccelerationIndicatorInput,
568    kernel: Kernel,
569) -> Result<(), VelocityAccelerationIndicatorError> {
570    let (data, length, smooth_length, _first, _chosen) =
571        velocity_acceleration_indicator_prepare(input, kernel)?;
572    if out.len() != data.len() {
573        return Err(VelocityAccelerationIndicatorError::OutputLengthMismatch {
574            expected: data.len(),
575            got: out.len(),
576        });
577    }
578    compute_row(data, length, smooth_length, out);
579    Ok(())
580}
581
582#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
583#[inline]
584pub fn velocity_acceleration_indicator_into(
585    input: &VelocityAccelerationIndicatorInput,
586    out: &mut [f64],
587) -> Result<(), VelocityAccelerationIndicatorError> {
588    velocity_acceleration_indicator_into_slice(out, input, Kernel::Auto)
589}
590
591#[derive(Clone, Debug)]
592pub struct VelocityAccelerationIndicatorBatchRange {
593    pub length: (usize, usize, usize),
594    pub smooth_length: (usize, usize, usize),
595}
596
597impl Default for VelocityAccelerationIndicatorBatchRange {
598    fn default() -> Self {
599        Self {
600            length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
601            smooth_length: (DEFAULT_SMOOTH_LENGTH, DEFAULT_SMOOTH_LENGTH, 0),
602        }
603    }
604}
605
606#[derive(Clone, Debug, Default)]
607pub struct VelocityAccelerationIndicatorBatchBuilder {
608    range: VelocityAccelerationIndicatorBatchRange,
609    kernel: Kernel,
610}
611
612impl VelocityAccelerationIndicatorBatchBuilder {
613    #[inline]
614    pub fn new() -> Self {
615        Self::default()
616    }
617
618    #[inline]
619    pub fn kernel(mut self, kernel: Kernel) -> Self {
620        self.kernel = kernel;
621        self
622    }
623
624    #[inline]
625    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
626        self.range.length = (start, end, step);
627        self
628    }
629
630    #[inline]
631    pub fn length_static(mut self, length: usize) -> Self {
632        self.range.length = (length, length, 0);
633        self
634    }
635
636    #[inline]
637    pub fn smooth_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
638        self.range.smooth_length = (start, end, step);
639        self
640    }
641
642    #[inline]
643    pub fn smooth_length_static(mut self, smooth_length: usize) -> Self {
644        self.range.smooth_length = (smooth_length, smooth_length, 0);
645        self
646    }
647
648    #[inline]
649    pub fn apply_slice(
650        self,
651        data: &[f64],
652    ) -> Result<VelocityAccelerationIndicatorBatchOutput, VelocityAccelerationIndicatorError> {
653        velocity_acceleration_indicator_batch_with_kernel(data, &self.range, self.kernel)
654    }
655
656    #[inline]
657    pub fn apply_candles(
658        self,
659        candles: &Candles,
660        source: &str,
661    ) -> Result<VelocityAccelerationIndicatorBatchOutput, VelocityAccelerationIndicatorError> {
662        self.apply_slice(source_type(candles, source))
663    }
664}
665
666#[derive(Clone, Debug)]
667pub struct VelocityAccelerationIndicatorBatchOutput {
668    pub values: Vec<f64>,
669    pub combos: Vec<VelocityAccelerationIndicatorParams>,
670    pub rows: usize,
671    pub cols: usize,
672}
673
674impl VelocityAccelerationIndicatorBatchOutput {
675    #[inline]
676    pub fn row_for_params(&self, params: &VelocityAccelerationIndicatorParams) -> Option<usize> {
677        self.combos.iter().position(|combo| {
678            combo.length.unwrap_or(DEFAULT_LENGTH) == params.length.unwrap_or(DEFAULT_LENGTH)
679                && combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH)
680                    == params.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH)
681        })
682    }
683
684    #[inline]
685    pub fn values_for(&self, params: &VelocityAccelerationIndicatorParams) -> Option<&[f64]> {
686        self.row_for_params(params).and_then(|row| {
687            row.checked_mul(self.cols)
688                .and_then(|start| self.values.get(start..start + self.cols))
689        })
690    }
691}
692
693#[inline(always)]
694fn expand_axis_usize(
695    (start, end, step): (usize, usize, usize),
696) -> Result<Vec<usize>, VelocityAccelerationIndicatorError> {
697    if step == 0 || start == end {
698        return Ok(vec![start]);
699    }
700
701    let mut out = Vec::new();
702    if start < end {
703        let mut x = start;
704        while x <= end {
705            out.push(x);
706            let next = x.saturating_add(step);
707            if next == x {
708                break;
709            }
710            x = next;
711        }
712    } else {
713        let mut x = start;
714        loop {
715            out.push(x);
716            if x == end {
717                break;
718            }
719            let next = x.saturating_sub(step);
720            if next == x || next < end {
721                break;
722            }
723            x = next;
724        }
725    }
726
727    if out.is_empty() {
728        return Err(VelocityAccelerationIndicatorError::InvalidRange {
729            start: start.to_string(),
730            end: end.to_string(),
731            step: step.to_string(),
732        });
733    }
734    Ok(out)
735}
736
737#[inline(always)]
738fn expand_grid_velocity_acceleration_indicator(
739    sweep: &VelocityAccelerationIndicatorBatchRange,
740) -> Result<Vec<VelocityAccelerationIndicatorParams>, VelocityAccelerationIndicatorError> {
741    let lengths = expand_axis_usize(sweep.length)?;
742    let smooth_lengths = expand_axis_usize(sweep.smooth_length)?;
743
744    let mut combos = Vec::with_capacity(lengths.len() * smooth_lengths.len());
745    for length in lengths {
746        for smooth_length in smooth_lengths.iter().copied() {
747            let combo = VelocityAccelerationIndicatorParams {
748                length: Some(length),
749                smooth_length: Some(smooth_length),
750            };
751            let _ = resolve_params(&combo)?;
752            combos.push(combo);
753        }
754    }
755    Ok(combos)
756}
757
758#[inline]
759pub fn velocity_acceleration_indicator_batch_with_kernel(
760    data: &[f64],
761    sweep: &VelocityAccelerationIndicatorBatchRange,
762    kernel: Kernel,
763) -> Result<VelocityAccelerationIndicatorBatchOutput, VelocityAccelerationIndicatorError> {
764    let batch_kernel = match kernel {
765        Kernel::Auto => detect_best_batch_kernel(),
766        other if other.is_batch() => other,
767        other => {
768            return Err(VelocityAccelerationIndicatorError::InvalidKernelForBatch(
769                other,
770            ))
771        }
772    };
773    velocity_acceleration_indicator_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
774}
775
776#[inline]
777pub fn velocity_acceleration_indicator_batch_slice(
778    data: &[f64],
779    sweep: &VelocityAccelerationIndicatorBatchRange,
780    kernel: Kernel,
781) -> Result<VelocityAccelerationIndicatorBatchOutput, VelocityAccelerationIndicatorError> {
782    velocity_acceleration_indicator_batch_inner(data, sweep, kernel, false)
783}
784
785#[inline]
786pub fn velocity_acceleration_indicator_batch_par_slice(
787    data: &[f64],
788    sweep: &VelocityAccelerationIndicatorBatchRange,
789    kernel: Kernel,
790) -> Result<VelocityAccelerationIndicatorBatchOutput, VelocityAccelerationIndicatorError> {
791    velocity_acceleration_indicator_batch_inner(data, sweep, kernel, true)
792}
793
794#[inline(always)]
795pub fn velocity_acceleration_indicator_batch_inner(
796    data: &[f64],
797    sweep: &VelocityAccelerationIndicatorBatchRange,
798    _kernel: Kernel,
799    parallel: bool,
800) -> Result<VelocityAccelerationIndicatorBatchOutput, VelocityAccelerationIndicatorError> {
801    let combos = expand_grid_velocity_acceleration_indicator(sweep)?;
802    let rows = combos.len();
803    let cols = data.len();
804    if cols == 0 {
805        return Err(VelocityAccelerationIndicatorError::EmptyInputData);
806    }
807
808    let first = first_valid_value(data);
809    if first >= cols {
810        return Err(VelocityAccelerationIndicatorError::AllValuesNaN);
811    }
812
813    let valid = max_consecutive_valid_values(data);
814    let max_smooth_length = combos
815        .iter()
816        .map(|combo| combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH))
817        .max()
818        .unwrap_or(0);
819    if valid < max_smooth_length {
820        return Err(VelocityAccelerationIndicatorError::NotEnoughValidData {
821            needed: max_smooth_length,
822            valid,
823        });
824    }
825
826    let mut buf_mu = make_uninit_matrix(rows, cols);
827    let warmups: Vec<usize> = combos
828        .iter()
829        .map(|combo| {
830            first.saturating_add(
831                combo
832                    .smooth_length
833                    .unwrap_or(DEFAULT_SMOOTH_LENGTH)
834                    .saturating_sub(1),
835            )
836        })
837        .collect();
838    init_matrix_prefixes(&mut buf_mu, cols, &warmups);
839
840    let mut guard = ManuallyDrop::new(buf_mu);
841    let out: &mut [f64] =
842        unsafe { std::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
843
844    if parallel {
845        #[cfg(not(target_arch = "wasm32"))]
846        out.par_chunks_mut(cols)
847            .enumerate()
848            .for_each(|(row, out_row)| {
849                let combo = &combos[row];
850                compute_row(
851                    data,
852                    combo.length.unwrap_or(DEFAULT_LENGTH),
853                    combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
854                    out_row,
855                );
856            });
857
858        #[cfg(target_arch = "wasm32")]
859        for (row, out_row) in out.chunks_mut(cols).enumerate() {
860            let combo = &combos[row];
861            compute_row(
862                data,
863                combo.length.unwrap_or(DEFAULT_LENGTH),
864                combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
865                out_row,
866            );
867        }
868    } else {
869        for (row, out_row) in out.chunks_mut(cols).enumerate() {
870            let combo = &combos[row];
871            compute_row(
872                data,
873                combo.length.unwrap_or(DEFAULT_LENGTH),
874                combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
875                out_row,
876            );
877        }
878    }
879
880    let values = unsafe {
881        Vec::from_raw_parts(
882            guard.as_mut_ptr() as *mut f64,
883            guard.len(),
884            guard.capacity(),
885        )
886    };
887
888    Ok(VelocityAccelerationIndicatorBatchOutput {
889        values,
890        combos,
891        rows,
892        cols,
893    })
894}
895
896#[inline(always)]
897pub fn velocity_acceleration_indicator_batch_inner_into(
898    data: &[f64],
899    sweep: &VelocityAccelerationIndicatorBatchRange,
900    _kernel: Kernel,
901    parallel: bool,
902    out: &mut [f64],
903) -> Result<Vec<VelocityAccelerationIndicatorParams>, VelocityAccelerationIndicatorError> {
904    let combos = expand_grid_velocity_acceleration_indicator(sweep)?;
905    let rows = combos.len();
906    let cols = data.len();
907    if cols == 0 {
908        return Err(VelocityAccelerationIndicatorError::EmptyInputData);
909    }
910
911    let total = rows.checked_mul(cols).ok_or_else(|| {
912        VelocityAccelerationIndicatorError::OutputLengthMismatch {
913            expected: usize::MAX,
914            got: out.len(),
915        }
916    })?;
917    if out.len() != total {
918        return Err(VelocityAccelerationIndicatorError::OutputLengthMismatch {
919            expected: total,
920            got: out.len(),
921        });
922    }
923
924    let first = first_valid_value(data);
925    if first >= cols {
926        return Err(VelocityAccelerationIndicatorError::AllValuesNaN);
927    }
928
929    let valid = max_consecutive_valid_values(data);
930    let max_smooth_length = combos
931        .iter()
932        .map(|combo| combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH))
933        .max()
934        .unwrap_or(0);
935    if valid < max_smooth_length {
936        return Err(VelocityAccelerationIndicatorError::NotEnoughValidData {
937            needed: max_smooth_length,
938            valid,
939        });
940    }
941
942    let warmups: Vec<usize> = combos
943        .iter()
944        .map(|combo| {
945            first.saturating_add(
946                combo
947                    .smooth_length
948                    .unwrap_or(DEFAULT_SMOOTH_LENGTH)
949                    .saturating_sub(1),
950            )
951        })
952        .collect();
953
954    for (row, out_row) in out.chunks_mut(cols).enumerate() {
955        let warmup = warmups[row].min(cols);
956        out_row[..warmup].fill(f64::NAN);
957    }
958
959    if parallel {
960        #[cfg(not(target_arch = "wasm32"))]
961        out.par_chunks_mut(cols)
962            .enumerate()
963            .for_each(|(row, out_row)| {
964                let combo = &combos[row];
965                compute_row(
966                    data,
967                    combo.length.unwrap_or(DEFAULT_LENGTH),
968                    combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
969                    out_row,
970                );
971            });
972
973        #[cfg(target_arch = "wasm32")]
974        for (row, out_row) in out.chunks_mut(cols).enumerate() {
975            let combo = &combos[row];
976            compute_row(
977                data,
978                combo.length.unwrap_or(DEFAULT_LENGTH),
979                combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
980                out_row,
981            );
982        }
983    } else {
984        for (row, out_row) in out.chunks_mut(cols).enumerate() {
985            let combo = &combos[row];
986            compute_row(
987                data,
988                combo.length.unwrap_or(DEFAULT_LENGTH),
989                combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
990                out_row,
991            );
992        }
993    }
994
995    Ok(combos)
996}
997
998#[cfg(feature = "python")]
999#[pyfunction(name = "velocity_acceleration_indicator")]
1000#[pyo3(signature = (data, length=DEFAULT_LENGTH, smooth_length=DEFAULT_SMOOTH_LENGTH, kernel=None))]
1001pub fn velocity_acceleration_indicator_py<'py>(
1002    py: Python<'py>,
1003    data: PyReadonlyArray1<'py, f64>,
1004    length: usize,
1005    smooth_length: usize,
1006    kernel: Option<&str>,
1007) -> PyResult<Bound<'py, PyArray1<f64>>> {
1008    let slice = data.as_slice()?;
1009    let kernel = validate_kernel(kernel, false)?;
1010    let input = VelocityAccelerationIndicatorInput::from_slice(
1011        slice,
1012        VelocityAccelerationIndicatorParams {
1013            length: Some(length),
1014            smooth_length: Some(smooth_length),
1015        },
1016    );
1017    let output = py
1018        .allow_threads(|| velocity_acceleration_indicator_with_kernel(&input, kernel))
1019        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1020    Ok(output.values.into_pyarray(py))
1021}
1022
1023#[cfg(feature = "python")]
1024#[pyclass(name = "VelocityAccelerationIndicatorStream")]
1025pub struct VelocityAccelerationIndicatorStreamPy {
1026    inner: VelocityAccelerationIndicatorStream,
1027}
1028
1029#[cfg(feature = "python")]
1030#[pymethods]
1031impl VelocityAccelerationIndicatorStreamPy {
1032    #[new]
1033    #[pyo3(signature = (length=DEFAULT_LENGTH, smooth_length=DEFAULT_SMOOTH_LENGTH))]
1034    fn new(length: usize, smooth_length: usize) -> PyResult<Self> {
1035        let inner =
1036            VelocityAccelerationIndicatorStream::try_new(VelocityAccelerationIndicatorParams {
1037                length: Some(length),
1038                smooth_length: Some(smooth_length),
1039            })
1040            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1041        Ok(Self { inner })
1042    }
1043
1044    fn update(&mut self, value: f64) -> Option<f64> {
1045        self.inner.update(value)
1046    }
1047
1048    #[getter]
1049    fn warmup_period(&self) -> usize {
1050        self.inner.get_warmup_period()
1051    }
1052}
1053
1054#[cfg(feature = "python")]
1055#[pyfunction(name = "velocity_acceleration_indicator_batch")]
1056#[pyo3(signature = (data, length_range=(DEFAULT_LENGTH, DEFAULT_LENGTH, 0), smooth_length_range=(DEFAULT_SMOOTH_LENGTH, DEFAULT_SMOOTH_LENGTH, 0), kernel=None))]
1057pub fn velocity_acceleration_indicator_batch_py<'py>(
1058    py: Python<'py>,
1059    data: PyReadonlyArray1<'py, f64>,
1060    length_range: (usize, usize, usize),
1061    smooth_length_range: (usize, usize, usize),
1062    kernel: Option<&str>,
1063) -> PyResult<Bound<'py, PyDict>> {
1064    let slice = data.as_slice()?;
1065    let kernel = validate_kernel(kernel, true)?;
1066    let sweep = VelocityAccelerationIndicatorBatchRange {
1067        length: length_range,
1068        smooth_length: smooth_length_range,
1069    };
1070    let combos = expand_grid_velocity_acceleration_indicator(&sweep)
1071        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1072    let rows = combos.len();
1073    let cols = slice.len();
1074    let total = rows
1075        .checked_mul(cols)
1076        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1077
1078    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1079    let out_slice = unsafe { out_arr.as_slice_mut()? };
1080
1081    let combos = py
1082        .allow_threads(|| {
1083            let batch = match kernel {
1084                Kernel::Auto => detect_best_batch_kernel(),
1085                other => other,
1086            };
1087            velocity_acceleration_indicator_batch_inner_into(
1088                slice,
1089                &sweep,
1090                batch.to_non_batch(),
1091                true,
1092                out_slice,
1093            )
1094        })
1095        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1096
1097    let dict = PyDict::new(py);
1098    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1099    dict.set_item(
1100        "lengths",
1101        combos
1102            .iter()
1103            .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
1104            .collect::<Vec<_>>()
1105            .into_pyarray(py),
1106    )?;
1107    dict.set_item(
1108        "smooth_lengths",
1109        combos
1110            .iter()
1111            .map(|combo| combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH) as u64)
1112            .collect::<Vec<_>>()
1113            .into_pyarray(py),
1114    )?;
1115    dict.set_item("rows", rows)?;
1116    dict.set_item("cols", cols)?;
1117    Ok(dict)
1118}
1119
1120#[cfg(feature = "python")]
1121pub fn register_velocity_acceleration_indicator_module(
1122    module: &Bound<'_, pyo3::types::PyModule>,
1123) -> PyResult<()> {
1124    module.add_function(wrap_pyfunction!(
1125        velocity_acceleration_indicator_py,
1126        module
1127    )?)?;
1128    module.add_function(wrap_pyfunction!(
1129        velocity_acceleration_indicator_batch_py,
1130        module
1131    )?)?;
1132    module.add_class::<VelocityAccelerationIndicatorStreamPy>()?;
1133    Ok(())
1134}
1135
1136#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1137#[wasm_bindgen(js_name = "velocity_acceleration_indicator_js")]
1138pub fn velocity_acceleration_indicator_js(
1139    data: &[f64],
1140    length: usize,
1141    smooth_length: usize,
1142) -> Result<Vec<f64>, JsValue> {
1143    let input = VelocityAccelerationIndicatorInput::from_slice(
1144        data,
1145        VelocityAccelerationIndicatorParams {
1146            length: Some(length),
1147            smooth_length: Some(smooth_length),
1148        },
1149    );
1150    let mut output = vec![0.0; data.len()];
1151    velocity_acceleration_indicator_into_slice(&mut output, &input, Kernel::Auto)
1152        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1153    Ok(output)
1154}
1155
1156#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1157#[wasm_bindgen]
1158pub fn velocity_acceleration_indicator_alloc(len: usize) -> *mut f64 {
1159    let mut vec = Vec::<f64>::with_capacity(len);
1160    let ptr = vec.as_mut_ptr();
1161    std::mem::forget(vec);
1162    ptr
1163}
1164
1165#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1166#[wasm_bindgen]
1167pub fn velocity_acceleration_indicator_free(ptr: *mut f64, len: usize) {
1168    if !ptr.is_null() {
1169        unsafe {
1170            let _ = Vec::from_raw_parts(ptr, len, len);
1171        }
1172    }
1173}
1174
1175#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1176#[wasm_bindgen]
1177pub fn velocity_acceleration_indicator_into(
1178    in_ptr: *const f64,
1179    out_ptr: *mut f64,
1180    len: usize,
1181    length: usize,
1182    smooth_length: usize,
1183) -> Result<(), JsValue> {
1184    if in_ptr.is_null() || out_ptr.is_null() {
1185        return Err(JsValue::from_str("Null pointer provided"));
1186    }
1187    unsafe {
1188        let data = std::slice::from_raw_parts(in_ptr, len);
1189        let input = VelocityAccelerationIndicatorInput::from_slice(
1190            data,
1191            VelocityAccelerationIndicatorParams {
1192                length: Some(length),
1193                smooth_length: Some(smooth_length),
1194            },
1195        );
1196        if in_ptr == out_ptr {
1197            let mut tmp = vec![0.0; len];
1198            velocity_acceleration_indicator_into_slice(&mut tmp, &input, Kernel::Auto)
1199                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1200            std::slice::from_raw_parts_mut(out_ptr, len).copy_from_slice(&tmp);
1201        } else {
1202            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1203            velocity_acceleration_indicator_into_slice(out, &input, Kernel::Auto)
1204                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1205        }
1206    }
1207    Ok(())
1208}
1209
1210#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1211#[derive(Serialize, Deserialize)]
1212pub struct VelocityAccelerationIndicatorBatchConfig {
1213    pub length_range: (usize, usize, usize),
1214    pub smooth_length_range: Option<(usize, usize, usize)>,
1215}
1216
1217#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1218#[derive(Serialize, Deserialize)]
1219pub struct VelocityAccelerationIndicatorBatchJsOutput {
1220    pub values: Vec<f64>,
1221    pub combos: Vec<VelocityAccelerationIndicatorParams>,
1222    pub rows: usize,
1223    pub cols: usize,
1224}
1225
1226#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1227#[wasm_bindgen(js_name = "velocity_acceleration_indicator_batch_js")]
1228pub fn velocity_acceleration_indicator_batch_js(
1229    data: &[f64],
1230    config: JsValue,
1231) -> Result<JsValue, JsValue> {
1232    let config: VelocityAccelerationIndicatorBatchConfig =
1233        serde_wasm_bindgen::from_value(config)
1234            .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1235    let sweep = VelocityAccelerationIndicatorBatchRange {
1236        length: config.length_range,
1237        smooth_length: config.smooth_length_range.unwrap_or((
1238            DEFAULT_SMOOTH_LENGTH,
1239            DEFAULT_SMOOTH_LENGTH,
1240            0,
1241        )),
1242    };
1243    let output = velocity_acceleration_indicator_batch_with_kernel(data, &sweep, Kernel::Auto)
1244        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1245    serde_wasm_bindgen::to_value(&VelocityAccelerationIndicatorBatchJsOutput {
1246        values: output.values,
1247        combos: output.combos,
1248        rows: output.rows,
1249        cols: output.cols,
1250    })
1251    .map_err(|e| JsValue::from_str(&e.to_string()))
1252}
1253
1254#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1255#[wasm_bindgen]
1256pub fn velocity_acceleration_indicator_batch_into(
1257    in_ptr: *const f64,
1258    out_ptr: *mut f64,
1259    len: usize,
1260    length_start: usize,
1261    length_end: usize,
1262    length_step: usize,
1263    smooth_length_start: usize,
1264    smooth_length_end: usize,
1265    smooth_length_step: usize,
1266) -> Result<usize, JsValue> {
1267    if in_ptr.is_null() || out_ptr.is_null() {
1268        return Err(JsValue::from_str("Null pointer provided"));
1269    }
1270    unsafe {
1271        let data = std::slice::from_raw_parts(in_ptr, len);
1272        let sweep = VelocityAccelerationIndicatorBatchRange {
1273            length: (length_start, length_end, length_step),
1274            smooth_length: (smooth_length_start, smooth_length_end, smooth_length_step),
1275        };
1276        let combos = expand_grid_velocity_acceleration_indicator(&sweep)
1277            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1278        let rows = combos.len();
1279        let total = rows
1280            .checked_mul(len)
1281            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1282        let out = std::slice::from_raw_parts_mut(out_ptr, total);
1283        velocity_acceleration_indicator_batch_inner_into(data, &sweep, Kernel::Auto, false, out)
1284            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1285        Ok(rows)
1286    }
1287}
1288
1289#[cfg(test)]
1290mod tests {
1291    use super::*;
1292
1293    fn sample_data(length: usize) -> Vec<f64> {
1294        (0..length)
1295            .map(|i| {
1296                let x = i as f64;
1297                100.0 + x * 0.05 + (x * 0.13).sin() * 2.5 + (x * 0.04).cos() * 0.8
1298            })
1299            .collect()
1300    }
1301
1302    fn sample_candles(length: usize) -> Candles {
1303        let mut open = Vec::with_capacity(length);
1304        let mut high = Vec::with_capacity(length);
1305        let mut low = Vec::with_capacity(length);
1306        let mut close = Vec::with_capacity(length);
1307        for i in 0..length {
1308            let x = i as f64;
1309            let o = 100.0 + x * 0.04 + (x * 0.09).sin();
1310            let c = o + (x * 0.11).cos() * 0.9;
1311            let h = o.max(c) + 0.6 + (x * 0.03).sin().abs() * 0.2;
1312            let l = o.min(c) - 0.6 - (x * 0.05).cos().abs() * 0.2;
1313            open.push(o);
1314            high.push(h);
1315            low.push(l);
1316            close.push(c);
1317        }
1318        Candles::new(
1319            (0..length as i64).collect(),
1320            open,
1321            high,
1322            low,
1323            close,
1324            vec![1_000.0; length],
1325        )
1326    }
1327
1328    fn assert_series_eq(actual: &[f64], expected: &[f64]) {
1329        assert_eq!(actual.len(), expected.len());
1330        for (&a, &e) in actual.iter().zip(expected.iter()) {
1331            assert!(
1332                (a.is_nan() && e.is_nan()) || (a - e).abs() <= 1e-12,
1333                "expected {e:?}, got {a:?}"
1334            );
1335        }
1336    }
1337
1338    #[test]
1339    fn velocity_acceleration_indicator_output_contract() {
1340        let data = sample_data(256);
1341        let input = VelocityAccelerationIndicatorInput::from_slice(
1342            &data,
1343            VelocityAccelerationIndicatorParams::default(),
1344        );
1345        let out = velocity_acceleration_indicator(&input).unwrap();
1346        assert_eq!(out.values.len(), data.len());
1347        assert_eq!(
1348            out.values.iter().position(|v| v.is_finite()).unwrap(),
1349            DEFAULT_SMOOTH_LENGTH - 1
1350        );
1351        assert!(out.values.last().unwrap().is_finite());
1352    }
1353
1354    #[test]
1355    fn velocity_acceleration_indicator_length_can_exceed_data_len() {
1356        let data = sample_data(8);
1357        let input = VelocityAccelerationIndicatorInput::from_slice(
1358            &data,
1359            VelocityAccelerationIndicatorParams {
1360                length: Some(21),
1361                smooth_length: Some(1),
1362            },
1363        );
1364        let out = velocity_acceleration_indicator(&input).unwrap();
1365        assert!(out.values.iter().all(|v| v.is_finite()));
1366    }
1367
1368    #[test]
1369    fn velocity_acceleration_indicator_rejects_invalid_parameters() {
1370        let data = sample_data(32);
1371        let err = velocity_acceleration_indicator(&VelocityAccelerationIndicatorInput::from_slice(
1372            &data,
1373            VelocityAccelerationIndicatorParams {
1374                length: Some(1),
1375                smooth_length: Some(5),
1376            },
1377        ))
1378        .unwrap_err();
1379        assert!(matches!(
1380            err,
1381            VelocityAccelerationIndicatorError::InvalidLength { .. }
1382        ));
1383
1384        let err = velocity_acceleration_indicator(&VelocityAccelerationIndicatorInput::from_slice(
1385            &data,
1386            VelocityAccelerationIndicatorParams {
1387                length: Some(21),
1388                smooth_length: Some(0),
1389            },
1390        ))
1391        .unwrap_err();
1392        assert!(matches!(
1393            err,
1394            VelocityAccelerationIndicatorError::InvalidSmoothLength { .. }
1395        ));
1396    }
1397
1398    #[test]
1399    fn velocity_acceleration_indicator_builder_supports_candles() {
1400        let candles = sample_candles(160);
1401        let output = VelocityAccelerationIndicatorBuilder::new()
1402            .length(21)
1403            .smooth_length(5)
1404            .apply(&candles, "hlcc4")
1405            .unwrap();
1406        assert_eq!(output.values.len(), candles.close.len());
1407        assert!(output.values.last().unwrap().is_finite());
1408    }
1409
1410    #[test]
1411    fn velocity_acceleration_indicator_stream_matches_batch_with_reset() {
1412        let mut data = sample_data(180);
1413        data[90] = f64::NAN;
1414
1415        let input = VelocityAccelerationIndicatorInput::from_slice(
1416            &data,
1417            VelocityAccelerationIndicatorParams::default(),
1418        );
1419        let batch = velocity_acceleration_indicator(&input).unwrap();
1420        let mut stream = VelocityAccelerationIndicatorStream::try_new(
1421            VelocityAccelerationIndicatorParams::default(),
1422        )
1423        .unwrap();
1424        let mut streamed = Vec::with_capacity(data.len());
1425        for &value in &data {
1426            streamed.push(stream.update(value).unwrap_or(f64::NAN));
1427        }
1428        assert_series_eq(&batch.values, &streamed);
1429    }
1430
1431    #[test]
1432    fn velocity_acceleration_indicator_into_matches_api() {
1433        let data = sample_data(192);
1434        let input = VelocityAccelerationIndicatorInput::from_slice(
1435            &data,
1436            VelocityAccelerationIndicatorParams::default(),
1437        );
1438        let direct = velocity_acceleration_indicator(&input).unwrap();
1439        let mut out = vec![0.0; data.len()];
1440        velocity_acceleration_indicator_into(&input, &mut out).unwrap();
1441        assert_series_eq(&direct.values, &out);
1442    }
1443
1444    #[test]
1445    fn velocity_acceleration_indicator_batch_single_param_matches_single() {
1446        let data = sample_data(192);
1447        let batch = velocity_acceleration_indicator_batch_with_kernel(
1448            &data,
1449            &VelocityAccelerationIndicatorBatchRange::default(),
1450            Kernel::ScalarBatch,
1451        )
1452        .unwrap();
1453        let single =
1454            velocity_acceleration_indicator(&VelocityAccelerationIndicatorInput::from_slice(
1455                &data,
1456                VelocityAccelerationIndicatorParams::default(),
1457            ))
1458            .unwrap();
1459        assert_eq!(batch.rows, 1);
1460        assert_eq!(batch.cols, data.len());
1461        assert_series_eq(&batch.values[..data.len()], &single.values);
1462    }
1463
1464    #[test]
1465    fn velocity_acceleration_indicator_batch_metadata() {
1466        let data = sample_data(128);
1467        let batch = velocity_acceleration_indicator_batch_with_kernel(
1468            &data,
1469            &VelocityAccelerationIndicatorBatchRange {
1470                length: (21, 23, 2),
1471                smooth_length: (4, 5, 1),
1472            },
1473            Kernel::ScalarBatch,
1474        )
1475        .unwrap();
1476        assert_eq!(batch.rows, 4);
1477        assert_eq!(batch.cols, data.len());
1478        assert_eq!(batch.combos.len(), 4);
1479    }
1480}