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}