Skip to main content

vector_ta/indicators/
velocity.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, MaybeUninit};
29use thiserror::Error;
30
31const DEFAULT_LENGTH: usize = 21;
32const DEFAULT_SMOOTH_LENGTH: usize = 5;
33const MIN_LENGTH: usize = 2;
34const MAX_LENGTH: usize = 60;
35const MIN_SMOOTH_LENGTH: usize = 1;
36const MAX_SMOOTH_LENGTH: usize = 9;
37
38impl<'a> AsRef<[f64]> for VelocityInput<'a> {
39    #[inline(always)]
40    fn as_ref(&self) -> &[f64] {
41        match &self.data {
42            VelocityData::Slice(slice) => slice,
43            VelocityData::Candles { candles, source } => source_type(candles, source),
44        }
45    }
46}
47
48#[derive(Debug, Clone)]
49pub enum VelocityData<'a> {
50    Candles {
51        candles: &'a Candles,
52        source: &'a str,
53    },
54    Slice(&'a [f64]),
55}
56
57#[derive(Debug, Clone)]
58pub struct VelocityOutput {
59    pub values: Vec<f64>,
60}
61
62#[derive(Debug, Clone)]
63#[cfg_attr(
64    all(target_arch = "wasm32", feature = "wasm"),
65    derive(Serialize, Deserialize)
66)]
67pub struct VelocityParams {
68    pub length: Option<usize>,
69    pub smooth_length: Option<usize>,
70}
71
72impl Default for VelocityParams {
73    fn default() -> Self {
74        Self {
75            length: Some(DEFAULT_LENGTH),
76            smooth_length: Some(DEFAULT_SMOOTH_LENGTH),
77        }
78    }
79}
80
81#[derive(Debug, Clone)]
82pub struct VelocityInput<'a> {
83    pub data: VelocityData<'a>,
84    pub params: VelocityParams,
85}
86
87impl<'a> VelocityInput<'a> {
88    #[inline]
89    pub fn from_candles(candles: &'a Candles, source: &'a str, params: VelocityParams) -> Self {
90        Self {
91            data: VelocityData::Candles { candles, source },
92            params,
93        }
94    }
95
96    #[inline]
97    pub fn from_slice(slice: &'a [f64], params: VelocityParams) -> Self {
98        Self {
99            data: VelocityData::Slice(slice),
100            params,
101        }
102    }
103
104    #[inline]
105    pub fn with_default_candles(candles: &'a Candles) -> Self {
106        Self::from_candles(candles, "hlcc4", VelocityParams::default())
107    }
108
109    #[inline]
110    pub fn get_length(&self) -> usize {
111        self.params.length.unwrap_or(DEFAULT_LENGTH)
112    }
113
114    #[inline]
115    pub fn get_smooth_length(&self) -> usize {
116        self.params.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH)
117    }
118}
119
120#[derive(Copy, Clone, Debug)]
121pub struct VelocityBuilder {
122    length: Option<usize>,
123    smooth_length: Option<usize>,
124    kernel: Kernel,
125}
126
127impl Default for VelocityBuilder {
128    fn default() -> Self {
129        Self {
130            length: None,
131            smooth_length: None,
132            kernel: Kernel::Auto,
133        }
134    }
135}
136
137impl VelocityBuilder {
138    #[inline(always)]
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    #[inline(always)]
144    pub fn length(mut self, length: usize) -> Self {
145        self.length = Some(length);
146        self
147    }
148
149    #[inline(always)]
150    pub fn smooth_length(mut self, smooth_length: usize) -> Self {
151        self.smooth_length = Some(smooth_length);
152        self
153    }
154
155    #[inline(always)]
156    pub fn kernel(mut self, kernel: Kernel) -> Self {
157        self.kernel = kernel;
158        self
159    }
160
161    #[inline(always)]
162    pub fn apply(self, candles: &Candles) -> Result<VelocityOutput, VelocityError> {
163        let input = VelocityInput::from_candles(
164            candles,
165            "hlcc4",
166            VelocityParams {
167                length: self.length,
168                smooth_length: self.smooth_length,
169            },
170        );
171        velocity_with_kernel(&input, self.kernel)
172    }
173
174    #[inline(always)]
175    pub fn apply_slice(self, data: &[f64]) -> Result<VelocityOutput, VelocityError> {
176        let input = VelocityInput::from_slice(
177            data,
178            VelocityParams {
179                length: self.length,
180                smooth_length: self.smooth_length,
181            },
182        );
183        velocity_with_kernel(&input, self.kernel)
184    }
185
186    #[inline(always)]
187    pub fn into_stream(self) -> Result<VelocityStream, VelocityError> {
188        VelocityStream::try_new(VelocityParams {
189            length: self.length,
190            smooth_length: self.smooth_length,
191        })
192    }
193}
194
195#[derive(Debug, Error)]
196pub enum VelocityError {
197    #[error("velocity: input data slice is empty.")]
198    EmptyInputData,
199    #[error("velocity: all values are NaN.")]
200    AllValuesNaN,
201    #[error("velocity: invalid length: {length}. Expected 2..=60.")]
202    InvalidLength { length: usize },
203    #[error("velocity: invalid smoothing length: {smooth_length}. Expected 1..=9.")]
204    InvalidSmoothLength { smooth_length: usize },
205    #[error("velocity: not enough valid data: needed = {needed}, valid = {valid}")]
206    NotEnoughValidData { needed: usize, valid: usize },
207    #[error("velocity: output length mismatch: expected = {expected}, got = {got}")]
208    OutputLengthMismatch { expected: usize, got: usize },
209    #[error("velocity: invalid length range: start={start}, end={end}, step={step}")]
210    InvalidLengthRange {
211        start: usize,
212        end: usize,
213        step: usize,
214    },
215    #[error("velocity: invalid smoothing length range: start={start}, end={end}, step={step}")]
216    InvalidSmoothLengthRange {
217        start: usize,
218        end: usize,
219        step: usize,
220    },
221    #[error("velocity: invalid kernel for batch: {0:?}")]
222    InvalidKernelForBatch(Kernel),
223}
224
225#[derive(Copy, Clone, Debug)]
226struct PreparedVelocity<'a> {
227    data: &'a [f64],
228    first_valid: usize,
229    length: usize,
230    smooth_length: usize,
231}
232
233#[derive(Debug, Clone)]
234struct VelocityCore {
235    length: usize,
236    smooth_length: usize,
237    harmonic_over_length: f64,
238    history: Vec<f64>,
239    history_head: usize,
240    history_count: usize,
241    raw_ring: Vec<f64>,
242    raw_head: usize,
243    raw_count: usize,
244}
245
246impl VelocityCore {
247    #[inline]
248    fn new(length: usize, smooth_length: usize) -> Self {
249        let mut harmonic = 0.0;
250        for lag in 1..=length {
251            harmonic += 1.0 / lag as f64;
252        }
253        Self {
254            length,
255            smooth_length,
256            harmonic_over_length: harmonic / length as f64,
257            history: vec![f64::NAN; length],
258            history_head: 0,
259            history_count: 0,
260            raw_ring: vec![f64::NAN; smooth_length],
261            raw_head: 0,
262            raw_count: 0,
263        }
264    }
265
266    #[inline]
267    fn reset(&mut self) {
268        self.history.fill(f64::NAN);
269        self.history_head = 0;
270        self.history_count = 0;
271        self.raw_ring.fill(f64::NAN);
272        self.raw_head = 0;
273        self.raw_count = 0;
274    }
275
276    #[inline(always)]
277    fn history_value(&self, lag: usize) -> f64 {
278        if lag == 0 || lag > self.history_count {
279            return 0.0;
280        }
281        let idx = (self.history_head + self.length - lag) % self.length;
282        let value = self.history[idx];
283        if value.is_finite() {
284            value
285        } else {
286            0.0
287        }
288    }
289
290    #[inline(always)]
291    fn push_history(&mut self, value: f64) {
292        self.history[self.history_head] = value;
293        self.history_head += 1;
294        if self.history_head == self.length {
295            self.history_head = 0;
296        }
297        if self.history_count < self.length {
298            self.history_count += 1;
299        }
300    }
301
302    #[inline(always)]
303    fn push_raw(&mut self, raw: f64) -> Option<f64> {
304        self.raw_ring[self.raw_head] = raw;
305        self.raw_head += 1;
306        if self.raw_head == self.smooth_length {
307            self.raw_head = 0;
308        }
309        if self.raw_count < self.smooth_length {
310            self.raw_count += 1;
311        }
312        if self.raw_count < self.smooth_length {
313            return None;
314        }
315
316        let mut weighted = 0.0;
317        for offset in 0..self.smooth_length {
318            let idx = (self.raw_head + offset) % self.smooth_length;
319            let value = self.raw_ring[idx];
320            if !value.is_finite() {
321                return Some(f64::NAN);
322            }
323            weighted += (offset + 1) as f64 * value;
324        }
325
326        let denom = (self.smooth_length * (self.smooth_length + 1) / 2) as f64;
327        Some(weighted / denom)
328    }
329
330    #[inline(always)]
331    fn update(&mut self, value: f64) -> Option<f64> {
332        let raw = if value.is_finite() {
333            let mut weighted_past = 0.0;
334            for lag in 1..=self.length {
335                weighted_past += self.history_value(lag) / lag as f64;
336            }
337            value * self.harmonic_over_length - weighted_past / self.length as f64
338        } else {
339            f64::NAN
340        };
341
342        self.push_history(value);
343        self.push_raw(raw)
344    }
345}
346
347#[inline(always)]
348fn normalize_single_kernel(kernel: Kernel) -> Kernel {
349    match kernel {
350        Kernel::Auto => detect_best_kernel(),
351        Kernel::ScalarBatch => Kernel::Scalar,
352        Kernel::Avx2Batch => Kernel::Avx2,
353        Kernel::Avx512Batch => Kernel::Avx512,
354        other => other,
355    }
356}
357
358#[inline(always)]
359fn validate_params(length: usize, smooth_length: usize) -> Result<(), VelocityError> {
360    if !(MIN_LENGTH..=MAX_LENGTH).contains(&length) {
361        return Err(VelocityError::InvalidLength { length });
362    }
363    if !(MIN_SMOOTH_LENGTH..=MAX_SMOOTH_LENGTH).contains(&smooth_length) {
364        return Err(VelocityError::InvalidSmoothLength { smooth_length });
365    }
366    Ok(())
367}
368
369#[inline(always)]
370fn velocity_prepare<'a>(
371    input: &'a VelocityInput<'a>,
372    kernel: Kernel,
373) -> Result<(PreparedVelocity<'a>, Kernel), VelocityError> {
374    let data = input.as_ref();
375    if data.is_empty() {
376        return Err(VelocityError::EmptyInputData);
377    }
378
379    let first_valid = data
380        .iter()
381        .position(|value| !value.is_nan())
382        .ok_or(VelocityError::AllValuesNaN)?;
383    let length = input.get_length();
384    let smooth_length = input.get_smooth_length();
385    validate_params(length, smooth_length)?;
386
387    let valid = data.len() - first_valid;
388    if valid < smooth_length {
389        return Err(VelocityError::NotEnoughValidData {
390            needed: smooth_length,
391            valid,
392        });
393    }
394
395    Ok((
396        PreparedVelocity {
397            data,
398            first_valid,
399            length,
400            smooth_length,
401        },
402        normalize_single_kernel(kernel),
403    ))
404}
405
406#[inline(always)]
407fn compute_velocity_into(prepared: PreparedVelocity<'_>, out: &mut [f64]) {
408    let mut core = VelocityCore::new(prepared.length, prepared.smooth_length);
409    for idx in prepared.first_valid..prepared.data.len() {
410        out[idx] = match core.update(prepared.data[idx]) {
411            Some(value) => value,
412            None => f64::NAN,
413        };
414    }
415}
416
417#[inline]
418pub fn velocity(input: &VelocityInput) -> Result<VelocityOutput, VelocityError> {
419    velocity_with_kernel(input, Kernel::Auto)
420}
421
422pub fn velocity_with_kernel(
423    input: &VelocityInput,
424    kernel: Kernel,
425) -> Result<VelocityOutput, VelocityError> {
426    let (prepared, _) = velocity_prepare(input, kernel)?;
427    let warm = prepared.first_valid + prepared.smooth_length - 1;
428    let mut out = alloc_with_nan_prefix(prepared.data.len(), warm);
429    compute_velocity_into(prepared, &mut out);
430    Ok(VelocityOutput { values: out })
431}
432
433#[inline]
434pub fn velocity_into_slice(
435    out: &mut [f64],
436    input: &VelocityInput,
437    kernel: Kernel,
438) -> Result<(), VelocityError> {
439    let (prepared, _) = velocity_prepare(input, kernel)?;
440    if out.len() != prepared.data.len() {
441        return Err(VelocityError::OutputLengthMismatch {
442            expected: prepared.data.len(),
443            got: out.len(),
444        });
445    }
446
447    let warm = (prepared.first_valid + prepared.smooth_length - 1).min(out.len());
448    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
449    for value in &mut out[..warm] {
450        *value = qnan;
451    }
452    compute_velocity_into(prepared, out);
453    Ok(())
454}
455
456#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
457#[inline]
458pub fn velocity_into(input: &VelocityInput, out: &mut [f64]) -> Result<(), VelocityError> {
459    velocity_into_slice(out, input, Kernel::Auto)
460}
461
462#[derive(Debug, Clone)]
463pub struct VelocityStream {
464    core: VelocityCore,
465    started: bool,
466}
467
468impl VelocityStream {
469    pub fn try_new(params: VelocityParams) -> Result<Self, VelocityError> {
470        let length = params.length.unwrap_or(DEFAULT_LENGTH);
471        let smooth_length = params.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH);
472        validate_params(length, smooth_length)?;
473        Ok(Self {
474            core: VelocityCore::new(length, smooth_length),
475            started: false,
476        })
477    }
478
479    #[inline(always)]
480    pub fn update(&mut self, value: f64) -> Option<f64> {
481        if !self.started {
482            if value.is_nan() {
483                return None;
484            }
485            self.started = true;
486        }
487        self.core.update(value)
488    }
489
490    #[inline]
491    pub fn reset(&mut self) {
492        self.started = false;
493        self.core.reset();
494    }
495}
496
497#[derive(Clone, Debug)]
498pub struct VelocityBatchRange {
499    pub length: (usize, usize, usize),
500    pub smooth_length: (usize, usize, usize),
501}
502
503impl Default for VelocityBatchRange {
504    fn default() -> Self {
505        Self {
506            length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
507            smooth_length: (DEFAULT_SMOOTH_LENGTH, DEFAULT_SMOOTH_LENGTH, 0),
508        }
509    }
510}
511
512#[derive(Clone, Debug, Default)]
513pub struct VelocityBatchBuilder {
514    range: VelocityBatchRange,
515    kernel: Kernel,
516}
517
518impl VelocityBatchBuilder {
519    pub fn new() -> Self {
520        Self::default()
521    }
522
523    pub fn kernel(mut self, kernel: Kernel) -> Self {
524        self.kernel = kernel;
525        self
526    }
527
528    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
529        self.range.length = (start, end, step);
530        self
531    }
532
533    pub fn length_static(mut self, length: usize) -> Self {
534        self.range.length = (length, length, 0);
535        self
536    }
537
538    pub fn smooth_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
539        self.range.smooth_length = (start, end, step);
540        self
541    }
542
543    pub fn smooth_length_static(mut self, smooth_length: usize) -> Self {
544        self.range.smooth_length = (smooth_length, smooth_length, 0);
545        self
546    }
547
548    pub fn apply_slice(self, data: &[f64]) -> Result<VelocityBatchOutput, VelocityError> {
549        velocity_batch_with_kernel(data, &self.range, self.kernel)
550    }
551
552    pub fn apply_candles(self, candles: &Candles) -> Result<VelocityBatchOutput, VelocityError> {
553        self.apply_slice(source_type(candles, "hlcc4"))
554    }
555
556    pub fn apply_candles_source(
557        self,
558        candles: &Candles,
559        source: &str,
560    ) -> Result<VelocityBatchOutput, VelocityError> {
561        self.apply_slice(source_type(candles, source))
562    }
563}
564
565#[derive(Clone, Debug)]
566pub struct VelocityBatchOutput {
567    pub values: Vec<f64>,
568    pub combos: Vec<VelocityParams>,
569    pub rows: usize,
570    pub cols: usize,
571}
572
573impl VelocityBatchOutput {
574    pub fn row_for_params(&self, params: &VelocityParams) -> Option<usize> {
575        let length = params.length.unwrap_or(DEFAULT_LENGTH);
576        let smooth_length = params.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH);
577        self.combos.iter().position(|combo| {
578            combo.length.unwrap_or(DEFAULT_LENGTH) == length
579                && combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH) == smooth_length
580        })
581    }
582
583    pub fn values_for(&self, params: &VelocityParams) -> Option<&[f64]> {
584        self.row_for_params(params).map(|row| {
585            let start = row * self.cols;
586            &self.values[start..start + self.cols]
587        })
588    }
589}
590
591#[inline(always)]
592fn expand_axis(axis: (usize, usize, usize), is_smooth: bool) -> Result<Vec<usize>, VelocityError> {
593    let (start, end, step) = axis;
594    if step == 0 || start == end {
595        return Ok(vec![start]);
596    }
597
598    let mut values = Vec::new();
599    if start < end {
600        let mut current = start;
601        while current <= end {
602            values.push(current);
603            match current.checked_add(step) {
604                Some(next) if next > current => current = next,
605                _ => break,
606            }
607        }
608    } else {
609        let mut current = start;
610        while current >= end {
611            values.push(current);
612            if current < end.saturating_add(step) {
613                break;
614            }
615            current = current.saturating_sub(step);
616        }
617    }
618
619    if values.is_empty() {
620        return Err(if is_smooth {
621            VelocityError::InvalidSmoothLengthRange { start, end, step }
622        } else {
623            VelocityError::InvalidLengthRange { start, end, step }
624        });
625    }
626
627    Ok(values)
628}
629
630#[inline(always)]
631fn expand_grid(range: &VelocityBatchRange) -> Result<Vec<VelocityParams>, VelocityError> {
632    let lengths = expand_axis(range.length, false)?;
633    let smooth_lengths = expand_axis(range.smooth_length, true)?;
634    let mut combos = Vec::with_capacity(lengths.len() * smooth_lengths.len());
635    for &length in &lengths {
636        for &smooth_length in &smooth_lengths {
637            validate_params(length, smooth_length)?;
638            combos.push(VelocityParams {
639                length: Some(length),
640                smooth_length: Some(smooth_length),
641            });
642        }
643    }
644    Ok(combos)
645}
646
647pub fn velocity_batch_with_kernel(
648    data: &[f64],
649    sweep: &VelocityBatchRange,
650    kernel: Kernel,
651) -> Result<VelocityBatchOutput, VelocityError> {
652    let batch_kernel = match kernel {
653        Kernel::Auto => detect_best_batch_kernel(),
654        other if other.is_batch() => other,
655        other => return Err(VelocityError::InvalidKernelForBatch(other)),
656    };
657
658    let scalar_kernel = match batch_kernel {
659        Kernel::ScalarBatch => Kernel::Scalar,
660        Kernel::Avx2Batch => Kernel::Avx2,
661        Kernel::Avx512Batch => Kernel::Avx512,
662        _ => unreachable!(),
663    };
664
665    velocity_batch_inner(
666        data,
667        sweep,
668        scalar_kernel,
669        !matches!(batch_kernel, Kernel::ScalarBatch),
670    )
671}
672
673#[inline(always)]
674pub fn velocity_batch_slice(
675    data: &[f64],
676    sweep: &VelocityBatchRange,
677    kernel: Kernel,
678) -> Result<VelocityBatchOutput, VelocityError> {
679    velocity_batch_inner(data, sweep, kernel, false)
680}
681
682#[inline(always)]
683pub fn velocity_batch_par_slice(
684    data: &[f64],
685    sweep: &VelocityBatchRange,
686    kernel: Kernel,
687) -> Result<VelocityBatchOutput, VelocityError> {
688    velocity_batch_inner(data, sweep, kernel, true)
689}
690
691#[inline(always)]
692fn velocity_batch_inner(
693    data: &[f64],
694    sweep: &VelocityBatchRange,
695    kernel: Kernel,
696    parallel: bool,
697) -> Result<VelocityBatchOutput, VelocityError> {
698    let combos = expand_grid(sweep)?;
699    if data.is_empty() {
700        return Err(VelocityError::EmptyInputData);
701    }
702
703    let first_valid = data
704        .iter()
705        .position(|value| !value.is_nan())
706        .ok_or(VelocityError::AllValuesNaN)?;
707    let valid = data.len() - first_valid;
708    let max_smooth = combos
709        .iter()
710        .map(|combo| combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH))
711        .max()
712        .unwrap_or(DEFAULT_SMOOTH_LENGTH);
713    if valid < max_smooth {
714        return Err(VelocityError::NotEnoughValidData {
715            needed: max_smooth,
716            valid,
717        });
718    }
719
720    let rows = combos.len();
721    let cols = data.len();
722    let mut buf = make_uninit_matrix(rows, cols);
723    let warm_prefixes: Vec<usize> = combos
724        .iter()
725        .map(|combo| first_valid + combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH) - 1)
726        .collect();
727    init_matrix_prefixes(&mut buf, cols, &warm_prefixes);
728
729    let mut guard = ManuallyDrop::new(buf);
730    let out_mu: &mut [MaybeUninit<f64>] =
731        unsafe { std::slice::from_raw_parts_mut(guard.as_mut_ptr(), guard.len()) };
732
733    let _ = normalize_single_kernel(kernel);
734    let do_row = |row: usize, row_mu: &mut [MaybeUninit<f64>]| {
735        let out = unsafe {
736            std::slice::from_raw_parts_mut(row_mu.as_mut_ptr() as *mut f64, row_mu.len())
737        };
738        compute_velocity_into(
739            PreparedVelocity {
740                data,
741                first_valid,
742                length: combos[row].length.unwrap_or(DEFAULT_LENGTH),
743                smooth_length: combos[row].smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
744            },
745            out,
746        );
747    };
748
749    if parallel {
750        #[cfg(not(target_arch = "wasm32"))]
751        out_mu
752            .par_chunks_mut(cols)
753            .enumerate()
754            .for_each(|(row, row_mu)| do_row(row, row_mu));
755        #[cfg(target_arch = "wasm32")]
756        for (row, row_mu) in out_mu.chunks_mut(cols).enumerate() {
757            do_row(row, row_mu);
758        }
759    } else {
760        for (row, row_mu) in out_mu.chunks_mut(cols).enumerate() {
761            do_row(row, row_mu);
762        }
763    }
764
765    let values = unsafe {
766        Vec::from_raw_parts(
767            guard.as_mut_ptr() as *mut f64,
768            guard.len(),
769            guard.capacity(),
770        )
771    };
772
773    Ok(VelocityBatchOutput {
774        values,
775        combos,
776        rows,
777        cols,
778    })
779}
780
781#[inline(always)]
782fn velocity_batch_inner_into(
783    data: &[f64],
784    sweep: &VelocityBatchRange,
785    kernel: Kernel,
786    parallel: bool,
787    out: &mut [f64],
788) -> Result<Vec<VelocityParams>, VelocityError> {
789    let combos = expand_grid(sweep)?;
790    if data.is_empty() {
791        return Err(VelocityError::EmptyInputData);
792    }
793
794    let first_valid = data
795        .iter()
796        .position(|value| !value.is_nan())
797        .ok_or(VelocityError::AllValuesNaN)?;
798    let valid = data.len() - first_valid;
799    let max_smooth = combos
800        .iter()
801        .map(|combo| combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH))
802        .max()
803        .unwrap_or(DEFAULT_SMOOTH_LENGTH);
804    if valid < max_smooth {
805        return Err(VelocityError::NotEnoughValidData {
806            needed: max_smooth,
807            valid,
808        });
809    }
810
811    let rows = combos.len();
812    let cols = data.len();
813    let expected = rows
814        .checked_mul(cols)
815        .ok_or(VelocityError::OutputLengthMismatch {
816            expected: usize::MAX,
817            got: out.len(),
818        })?;
819    if out.len() != expected {
820        return Err(VelocityError::OutputLengthMismatch {
821            expected,
822            got: out.len(),
823        });
824    }
825
826    let _ = normalize_single_kernel(kernel);
827    if parallel {
828        #[cfg(not(target_arch = "wasm32"))]
829        out.par_chunks_mut(cols)
830            .enumerate()
831            .for_each(|(row, row_out)| {
832                compute_velocity_into(
833                    PreparedVelocity {
834                        data,
835                        first_valid,
836                        length: combos[row].length.unwrap_or(DEFAULT_LENGTH),
837                        smooth_length: combos[row].smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
838                    },
839                    row_out,
840                );
841            });
842        #[cfg(target_arch = "wasm32")]
843        for (row, row_out) in out.chunks_mut(cols).enumerate() {
844            compute_velocity_into(
845                PreparedVelocity {
846                    data,
847                    first_valid,
848                    length: combos[row].length.unwrap_or(DEFAULT_LENGTH),
849                    smooth_length: combos[row].smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
850                },
851                row_out,
852            );
853        }
854    } else {
855        for (row, row_out) in out.chunks_mut(cols).enumerate() {
856            compute_velocity_into(
857                PreparedVelocity {
858                    data,
859                    first_valid,
860                    length: combos[row].length.unwrap_or(DEFAULT_LENGTH),
861                    smooth_length: combos[row].smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH),
862                },
863                row_out,
864            );
865        }
866    }
867
868    Ok(combos)
869}
870
871#[cfg(feature = "python")]
872#[pyfunction(name = "velocity")]
873#[pyo3(signature = (data, length=21, smooth_length=5, kernel=None))]
874pub fn velocity_py<'py>(
875    py: Python<'py>,
876    data: PyReadonlyArray1<'py, f64>,
877    length: usize,
878    smooth_length: usize,
879    kernel: Option<&str>,
880) -> PyResult<Bound<'py, PyArray1<f64>>> {
881    let slice = data.as_slice()?;
882    let kernel = validate_kernel(kernel, false)?;
883    let input = VelocityInput::from_slice(
884        slice,
885        VelocityParams {
886            length: Some(length),
887            smooth_length: Some(smooth_length),
888        },
889    );
890    let out = py
891        .allow_threads(|| velocity_with_kernel(&input, kernel).map(|output| output.values))
892        .map_err(|e| PyValueError::new_err(e.to_string()))?;
893    Ok(out.into_pyarray(py))
894}
895
896#[cfg(feature = "python")]
897#[pyfunction(name = "velocity_batch")]
898#[pyo3(signature = (data, length_range, smooth_length_range, kernel=None))]
899pub fn velocity_batch_py<'py>(
900    py: Python<'py>,
901    data: PyReadonlyArray1<'py, f64>,
902    length_range: (usize, usize, usize),
903    smooth_length_range: (usize, usize, usize),
904    kernel: Option<&str>,
905) -> PyResult<Bound<'py, PyDict>> {
906    let slice = data.as_slice()?;
907    let kernel = validate_kernel(kernel, true)?;
908    let sweep = VelocityBatchRange {
909        length: length_range,
910        smooth_length: smooth_length_range,
911    };
912
913    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
914    let rows = combos.len();
915    let cols = slice.len();
916    let total = rows
917        .checked_mul(cols)
918        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
919
920    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
921    let out_slice = unsafe { out_arr.as_slice_mut()? };
922
923    let combos = py
924        .allow_threads(|| {
925            let batch_kernel = match kernel {
926                Kernel::Auto => detect_best_batch_kernel(),
927                other => other,
928            };
929            velocity_batch_inner_into(
930                slice,
931                &sweep,
932                batch_kernel,
933                !matches!(batch_kernel, Kernel::ScalarBatch),
934                out_slice,
935            )
936        })
937        .map_err(|e| PyValueError::new_err(e.to_string()))?;
938
939    let dict = PyDict::new(py);
940    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
941    dict.set_item(
942        "lengths",
943        combos
944            .iter()
945            .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
946            .collect::<Vec<_>>()
947            .into_pyarray(py),
948    )?;
949    dict.set_item(
950        "smooth_lengths",
951        combos
952            .iter()
953            .map(|combo| combo.smooth_length.unwrap_or(DEFAULT_SMOOTH_LENGTH) as u64)
954            .collect::<Vec<_>>()
955            .into_pyarray(py),
956    )?;
957    dict.set_item("rows", rows)?;
958    dict.set_item("cols", cols)?;
959    Ok(dict)
960}
961
962#[cfg(feature = "python")]
963#[pyclass(name = "VelocityStream")]
964pub struct VelocityStreamPy {
965    inner: VelocityStream,
966}
967
968#[cfg(feature = "python")]
969#[pymethods]
970impl VelocityStreamPy {
971    #[new]
972    pub fn new(length: usize, smooth_length: usize) -> PyResult<Self> {
973        let inner = VelocityStream::try_new(VelocityParams {
974            length: Some(length),
975            smooth_length: Some(smooth_length),
976        })
977        .map_err(|e| PyValueError::new_err(e.to_string()))?;
978        Ok(Self { inner })
979    }
980
981    pub fn update(&mut self, value: f64) -> Option<f64> {
982        self.inner.update(value)
983    }
984
985    pub fn reset(&mut self) {
986        self.inner.reset();
987    }
988}
989
990#[cfg(feature = "python")]
991pub fn register_velocity_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
992    m.add_function(wrap_pyfunction!(velocity_py, m)?)?;
993    m.add_function(wrap_pyfunction!(velocity_batch_py, m)?)?;
994    m.add_class::<VelocityStreamPy>()?;
995    Ok(())
996}
997
998#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
999#[derive(Serialize, Deserialize)]
1000pub struct VelocityBatchConfig {
1001    pub length_range: (usize, usize, usize),
1002    pub smooth_length_range: (usize, usize, usize),
1003}
1004
1005#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1006#[derive(Serialize, Deserialize)]
1007pub struct VelocityBatchJsOutput {
1008    pub values: Vec<f64>,
1009    pub combos: Vec<VelocityParams>,
1010    pub rows: usize,
1011    pub cols: usize,
1012}
1013
1014#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1015#[wasm_bindgen]
1016pub fn velocity_js(data: &[f64], length: usize, smooth_length: usize) -> Result<Vec<f64>, JsValue> {
1017    let input = VelocityInput::from_slice(
1018        data,
1019        VelocityParams {
1020            length: Some(length),
1021            smooth_length: Some(smooth_length),
1022        },
1023    );
1024    let mut out = vec![0.0; data.len()];
1025    velocity_into_slice(&mut out, &input, Kernel::Auto)
1026        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1027    Ok(out)
1028}
1029
1030#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1031#[wasm_bindgen(js_name = velocity_batch)]
1032pub fn velocity_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1033    let config: VelocityBatchConfig = serde_wasm_bindgen::from_value(config)
1034        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1035    let sweep = VelocityBatchRange {
1036        length: config.length_range,
1037        smooth_length: config.smooth_length_range,
1038    };
1039    let output = velocity_batch_with_kernel(data, &sweep, Kernel::Auto)
1040        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1041    serde_wasm_bindgen::to_value(&VelocityBatchJsOutput {
1042        values: output.values,
1043        combos: output.combos,
1044        rows: output.rows,
1045        cols: output.cols,
1046    })
1047    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1048}
1049
1050#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1051#[wasm_bindgen]
1052pub fn velocity_alloc(len: usize) -> *mut f64 {
1053    let mut values = Vec::<f64>::with_capacity(len);
1054    let ptr = values.as_mut_ptr();
1055    std::mem::forget(values);
1056    ptr
1057}
1058
1059#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1060#[wasm_bindgen]
1061pub fn velocity_free(ptr: *mut f64, len: usize) {
1062    if !ptr.is_null() {
1063        unsafe {
1064            let _ = Vec::from_raw_parts(ptr, len, len);
1065        }
1066    }
1067}
1068
1069#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1070#[wasm_bindgen]
1071pub fn velocity_into(
1072    in_ptr: *const f64,
1073    out_ptr: *mut f64,
1074    len: usize,
1075    length: usize,
1076    smooth_length: usize,
1077) -> Result<(), JsValue> {
1078    if in_ptr.is_null() || out_ptr.is_null() {
1079        return Err(JsValue::from_str("null pointer passed to velocity_into"));
1080    }
1081
1082    unsafe {
1083        let data = std::slice::from_raw_parts(in_ptr, len);
1084        let input = VelocityInput::from_slice(
1085            data,
1086            VelocityParams {
1087                length: Some(length),
1088                smooth_length: Some(smooth_length),
1089            },
1090        );
1091        if in_ptr == out_ptr {
1092            let mut temp = vec![0.0; len];
1093            velocity_into_slice(&mut temp, &input, Kernel::Auto)
1094                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1095            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1096            out.copy_from_slice(&temp);
1097        } else {
1098            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1099            velocity_into_slice(out, &input, Kernel::Auto)
1100                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1101        }
1102        Ok(())
1103    }
1104}
1105
1106#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1107#[wasm_bindgen]
1108pub fn velocity_batch_into(
1109    in_ptr: *const f64,
1110    out_ptr: *mut f64,
1111    len: usize,
1112    length_start: usize,
1113    length_end: usize,
1114    length_step: usize,
1115    smooth_length_start: usize,
1116    smooth_length_end: usize,
1117    smooth_length_step: usize,
1118) -> Result<usize, JsValue> {
1119    if in_ptr.is_null() || out_ptr.is_null() {
1120        return Err(JsValue::from_str(
1121            "null pointer passed to velocity_batch_into",
1122        ));
1123    }
1124
1125    unsafe {
1126        let data = std::slice::from_raw_parts(in_ptr, len);
1127        let sweep = VelocityBatchRange {
1128            length: (length_start, length_end, length_step),
1129            smooth_length: (smooth_length_start, smooth_length_end, smooth_length_step),
1130        };
1131        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1132        let rows = combos.len();
1133        let cols = len;
1134        let total = rows
1135            .checked_mul(cols)
1136            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1137        let out = std::slice::from_raw_parts_mut(out_ptr, total);
1138        let batch_kernel = detect_best_batch_kernel();
1139        velocity_batch_inner_into(
1140            data,
1141            &sweep,
1142            batch_kernel,
1143            !matches!(batch_kernel, Kernel::ScalarBatch),
1144            out,
1145        )
1146        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1147        Ok(rows)
1148    }
1149}
1150
1151#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1152#[wasm_bindgen]
1153pub struct VelocityStreamWasm {
1154    inner: VelocityStream,
1155}
1156
1157#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1158#[wasm_bindgen]
1159impl VelocityStreamWasm {
1160    #[wasm_bindgen(constructor)]
1161    pub fn new(length: usize, smooth_length: usize) -> Result<VelocityStreamWasm, JsValue> {
1162        Ok(Self {
1163            inner: VelocityStream::try_new(VelocityParams {
1164                length: Some(length),
1165                smooth_length: Some(smooth_length),
1166            })
1167            .map_err(|e| JsValue::from_str(&e.to_string()))?,
1168        })
1169    }
1170
1171    pub fn update(&mut self, value: f64) -> Result<JsValue, JsValue> {
1172        match self.inner.update(value) {
1173            Some(output) => {
1174                serde_wasm_bindgen::to_value(&output).map_err(|e| JsValue::from_str(&e.to_string()))
1175            }
1176            None => Ok(JsValue::NULL),
1177        }
1178    }
1179
1180    pub fn reset(&mut self) {
1181        self.inner.reset();
1182    }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187    use super::*;
1188    use crate::utilities::data_loader::read_candles_from_csv;
1189    use std::error::Error;
1190
1191    fn naive_velocity(data: &[f64], length: usize, smooth_length: usize) -> Vec<f64> {
1192        let mut out = vec![f64::NAN; data.len()];
1193        let Some(first_valid) = data.iter().position(|value| !value.is_nan()) else {
1194            return out;
1195        };
1196
1197        let denom = (smooth_length * (smooth_length + 1) / 2) as f64;
1198        let mut raw = vec![f64::NAN; data.len()];
1199        for idx in first_valid..data.len() {
1200            let value = data[idx];
1201            if !value.is_finite() {
1202                continue;
1203            }
1204            let mut acc = 0.0;
1205            for lag in 1..=length {
1206                let hist = if idx >= lag {
1207                    let prev = data[idx - lag];
1208                    if prev.is_finite() {
1209                        prev
1210                    } else {
1211                        0.0
1212                    }
1213                } else {
1214                    0.0
1215                };
1216                acc += (value - hist) / lag as f64;
1217            }
1218            raw[idx] = acc / length as f64;
1219        }
1220
1221        for idx in (first_valid + smooth_length - 1)..data.len() {
1222            let mut weighted = 0.0;
1223            let mut valid = true;
1224            for offset in 0..smooth_length {
1225                let raw_value = raw[idx - smooth_length + 1 + offset];
1226                if !raw_value.is_finite() {
1227                    valid = false;
1228                    break;
1229                }
1230                weighted += (offset + 1) as f64 * raw_value;
1231            }
1232            if valid {
1233                out[idx] = weighted / denom;
1234            }
1235        }
1236        out
1237    }
1238
1239    fn sample_data() -> Vec<f64> {
1240        (0..256)
1241            .map(|idx| {
1242                let x = idx as f64;
1243                100.0 + (x * 0.07).sin() * 3.0 + (x * 0.033).cos() * 1.75 + x * 0.015
1244            })
1245            .collect()
1246    }
1247
1248    #[test]
1249    fn velocity_matches_naive_reference() -> Result<(), Box<dyn Error>> {
1250        let data = vec![f64::NAN, f64::NAN, 10.0, 11.0, 12.0, 13.0, 12.0, 14.0];
1251        let input = VelocityInput::from_slice(
1252            &data,
1253            VelocityParams {
1254                length: Some(3),
1255                smooth_length: Some(2),
1256            },
1257        );
1258        let output = velocity(&input)?;
1259        let expected = naive_velocity(&data, 3, 2);
1260        for (actual, expected) in output.values.iter().zip(expected.iter()) {
1261            assert!((actual.is_nan() && expected.is_nan()) || (actual - expected).abs() <= 1e-12);
1262        }
1263        Ok(())
1264    }
1265
1266    #[test]
1267    fn velocity_into_matches_api() -> Result<(), Box<dyn Error>> {
1268        let data = sample_data();
1269        let input = VelocityInput::from_slice(&data, VelocityParams::default());
1270        let baseline = velocity(&input)?.values;
1271        let mut out = vec![0.0; data.len()];
1272        velocity_into(&input, &mut out)?;
1273        for (actual, expected) in out.iter().zip(baseline.iter()) {
1274            assert!((actual.is_nan() && expected.is_nan()) || (actual - expected).abs() <= 1e-12);
1275        }
1276        Ok(())
1277    }
1278
1279    #[test]
1280    fn velocity_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1281        let data = sample_data();
1282        let batch = velocity(&VelocityInput::from_slice(&data, VelocityParams::default()))?;
1283        let mut stream = VelocityStream::try_new(VelocityParams::default())?;
1284        let mut values = vec![f64::NAN; data.len()];
1285        for (idx, value) in data.iter().copied().enumerate() {
1286            if let Some(output) = stream.update(value) {
1287                values[idx] = output;
1288            }
1289        }
1290        for (actual, expected) in values.iter().zip(batch.values.iter()) {
1291            assert!((actual.is_nan() && expected.is_nan()) || (actual - expected).abs() <= 1e-12);
1292        }
1293        Ok(())
1294    }
1295
1296    #[test]
1297    fn velocity_batch_matches_single() -> Result<(), Box<dyn Error>> {
1298        let data = sample_data();
1299        let batch = velocity_batch_with_kernel(
1300            &data,
1301            &VelocityBatchRange {
1302                length: (10, 12, 2),
1303                smooth_length: (3, 5, 2),
1304            },
1305            Kernel::ScalarBatch,
1306        )?;
1307
1308        for (row, combo) in batch.combos.iter().enumerate() {
1309            let single = velocity(&VelocityInput::from_slice(&data, combo.clone()))?;
1310            let start = row * batch.cols;
1311            let row_values = &batch.values[start..start + batch.cols];
1312            for (actual, expected) in row_values.iter().zip(single.values.iter()) {
1313                assert!(
1314                    (actual.is_nan() && expected.is_nan()) || (actual - expected).abs() <= 1e-12
1315                );
1316            }
1317        }
1318        Ok(())
1319    }
1320
1321    #[test]
1322    fn velocity_fixture_has_values() -> Result<(), Box<dyn Error>> {
1323        let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
1324        let out = velocity(&VelocityInput::with_default_candles(&candles))?;
1325        assert_eq!(out.values.len(), candles.close.len());
1326        assert!(out.values.iter().skip(64).any(|value| value.is_finite()));
1327        Ok(())
1328    }
1329}