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}