1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::cuda_available;
3#[cfg(all(feature = "python", feature = "cuda"))]
4use crate::cuda::moving_averages::tilson_wrapper::DeviceArrayF32Tilson;
5#[cfg(all(feature = "python", feature = "cuda"))]
6use crate::cuda::moving_averages::{CudaTilson, CudaTilsonError};
7use crate::utilities::data_loader::{source_type, Candles};
8use crate::utilities::enums::Kernel;
9use crate::utilities::helpers::{
10 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
11 make_uninit_matrix,
12};
13#[cfg(feature = "python")]
14use crate::utilities::kernel_validation::validate_kernel;
15#[cfg(all(feature = "python", feature = "cuda"))]
16use cust::memory::DeviceBuffer;
17
18#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
19use core::arch::x86_64::*;
20#[cfg(not(target_arch = "wasm32"))]
21use rayon::prelude::*;
22use std::convert::AsRef;
23use std::error::Error;
24use std::mem::MaybeUninit;
25use thiserror::Error;
26
27#[cfg(feature = "python")]
28use numpy::{IntoPyArray, PyArray1};
29#[cfg(feature = "python")]
30use pyo3::exceptions::PyValueError;
31#[cfg(feature = "python")]
32use pyo3::prelude::*;
33#[cfg(feature = "python")]
34use pyo3::types::{PyDict, PyList};
35#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
36use serde::{Deserialize, Serialize};
37#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
38use wasm_bindgen::prelude::*;
39
40impl<'a> AsRef<[f64]> for TilsonInput<'a> {
41 #[inline(always)]
42 fn as_ref(&self) -> &[f64] {
43 match &self.data {
44 TilsonData::Slice(slice) => slice,
45 TilsonData::Candles { candles, source } => source_type(candles, source),
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
51pub enum TilsonData<'a> {
52 Candles {
53 candles: &'a Candles,
54 source: &'a str,
55 },
56 Slice(&'a [f64]),
57}
58
59#[derive(Debug, Clone)]
60pub struct TilsonOutput {
61 pub values: Vec<f64>,
62}
63
64#[derive(Debug, Clone)]
65#[cfg_attr(
66 all(target_arch = "wasm32", feature = "wasm"),
67 derive(Serialize, Deserialize)
68)]
69pub struct TilsonParams {
70 pub period: Option<usize>,
71 pub volume_factor: Option<f64>,
72}
73
74impl Default for TilsonParams {
75 fn default() -> Self {
76 Self {
77 period: Some(5),
78 volume_factor: Some(0.0),
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
84pub struct TilsonInput<'a> {
85 pub data: TilsonData<'a>,
86 pub params: TilsonParams,
87}
88
89impl<'a> TilsonInput<'a> {
90 #[inline]
91 pub fn from_candles(c: &'a Candles, s: &'a str, p: TilsonParams) -> Self {
92 Self {
93 data: TilsonData::Candles {
94 candles: c,
95 source: s,
96 },
97 params: p,
98 }
99 }
100 #[inline]
101 pub fn from_slice(sl: &'a [f64], p: TilsonParams) -> Self {
102 Self {
103 data: TilsonData::Slice(sl),
104 params: p,
105 }
106 }
107 #[inline]
108 pub fn with_default_candles(c: &'a Candles) -> Self {
109 Self::from_candles(c, "close", TilsonParams::default())
110 }
111 #[inline]
112 pub fn get_period(&self) -> usize {
113 self.params.period.unwrap_or(5)
114 }
115 #[inline]
116 pub fn get_volume_factor(&self) -> f64 {
117 self.params.volume_factor.unwrap_or(0.0)
118 }
119}
120
121#[derive(Copy, Clone, Debug)]
122pub struct TilsonBuilder {
123 period: Option<usize>,
124 volume_factor: Option<f64>,
125 kernel: Kernel,
126}
127
128impl Default for TilsonBuilder {
129 fn default() -> Self {
130 Self {
131 period: None,
132 volume_factor: None,
133 kernel: Kernel::Auto,
134 }
135 }
136}
137
138impl TilsonBuilder {
139 #[inline(always)]
140 pub fn new() -> Self {
141 Self::default()
142 }
143 #[inline(always)]
144 pub fn period(mut self, n: usize) -> Self {
145 self.period = Some(n);
146 self
147 }
148 #[inline(always)]
149 pub fn volume_factor(mut self, v: f64) -> Self {
150 self.volume_factor = Some(v);
151 self
152 }
153 #[inline(always)]
154 pub fn kernel(mut self, k: Kernel) -> Self {
155 self.kernel = k;
156 self
157 }
158
159 #[inline(always)]
160 pub fn apply(self, c: &Candles) -> Result<TilsonOutput, TilsonError> {
161 let p = TilsonParams {
162 period: self.period,
163 volume_factor: self.volume_factor,
164 };
165 let i = TilsonInput::from_candles(c, "close", p);
166 tilson_with_kernel(&i, self.kernel)
167 }
168
169 #[inline(always)]
170 pub fn apply_slice(self, d: &[f64]) -> Result<TilsonOutput, TilsonError> {
171 let p = TilsonParams {
172 period: self.period,
173 volume_factor: self.volume_factor,
174 };
175 let i = TilsonInput::from_slice(d, p);
176 tilson_with_kernel(&i, self.kernel)
177 }
178
179 #[inline(always)]
180 pub fn into_stream(self) -> Result<TilsonStream, TilsonError> {
181 let p = TilsonParams {
182 period: self.period,
183 volume_factor: self.volume_factor,
184 };
185 TilsonStream::try_new(p)
186 }
187}
188
189#[derive(Debug, Error)]
190pub enum TilsonError {
191 #[error("tilson: Input data slice is empty.")]
192 EmptyInputData,
193
194 #[error("tilson: All values are NaN.")]
195 AllValuesNaN,
196
197 #[error("tilson: Invalid period: period = {period}, data length = {data_len}")]
198 InvalidPeriod { period: usize, data_len: usize },
199
200 #[error("tilson: Not enough valid data: needed = {needed}, valid = {valid}")]
201 NotEnoughValidData { needed: usize, valid: usize },
202
203 #[error("tilson: Invalid volume factor: {v_factor}")]
204 InvalidVolumeFactor { v_factor: f64 },
205
206 #[error("tilson: Output length mismatch: expected = {expected}, got = {got}")]
207 OutputLengthMismatch { expected: usize, got: usize },
208
209 #[error("tilson: Invalid kernel for batch operation: {0:?}")]
210 InvalidKernelForBatch(Kernel),
211
212 #[error("tilson: Invalid integer range expansion: start={start}, end={end}, step={step}")]
213 InvalidRangeUsize {
214 start: usize,
215 end: usize,
216 step: usize,
217 },
218
219 #[error("tilson: Invalid float range expansion: start={start}, end={end}, step={step}")]
220 InvalidRangeF64 { start: f64, end: f64, step: f64 },
221
222 #[error("tilson: invalid input: {0}")]
223 InvalidInput(&'static str),
224}
225
226#[inline]
227pub fn tilson(input: &TilsonInput) -> Result<TilsonOutput, TilsonError> {
228 tilson_with_kernel(input, Kernel::Auto)
229}
230
231pub fn tilson_with_kernel(
232 input: &TilsonInput,
233 kernel: Kernel,
234) -> Result<TilsonOutput, TilsonError> {
235 let (data, period, v_factor, first, len, chosen) = tilson_prepare(input, kernel)?;
236 let lookback_total = 6 * (period - 1);
237 let warm = first + lookback_total;
238
239 let mut out = alloc_with_nan_prefix(len, warm);
240 tilson_compute_into(data, period, v_factor, first, chosen, &mut out)?;
241 Ok(TilsonOutput { values: out })
242}
243
244#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
245pub fn tilson_into(input: &TilsonInput, out: &mut [f64]) -> Result<(), TilsonError> {
246 let (data, period, v_factor, first, len, chosen) = tilson_prepare(input, Kernel::Auto)?;
247
248 if out.len() != len {
249 return Err(TilsonError::OutputLengthMismatch {
250 expected: len,
251 got: out.len(),
252 });
253 }
254
255 let warm = (first + 6 * (period - 1)).min(len);
256 for i in 0..warm {
257 out[i] = f64::from_bits(0x7ff8_0000_0000_0000);
258 }
259
260 tilson_compute_into(data, period, v_factor, first, chosen, out)?;
261 Ok(())
262}
263
264#[inline]
265pub fn tilson_into_slice(
266 dst: &mut [f64],
267 input: &TilsonInput,
268 kern: Kernel,
269) -> Result<(), TilsonError> {
270 let (data, period, v_factor, first, _len, chosen) = tilson_prepare(input, kern)?;
271 if dst.len() != data.len() {
272 return Err(TilsonError::OutputLengthMismatch {
273 expected: data.len(),
274 got: dst.len(),
275 });
276 }
277 tilson_compute_into(data, period, v_factor, first, chosen, dst)?;
278 let warm = first + 6 * (period - 1);
279 let warm_end = warm.min(dst.len());
280 for v in &mut dst[..warm_end] {
281 *v = f64::NAN;
282 }
283 Ok(())
284}
285
286#[inline]
287pub fn tilson_scalar(
288 data: &[f64],
289 period: usize,
290 v_factor: f64,
291 first_valid: usize,
292 out: &mut [f64],
293) -> Result<(), TilsonError> {
294 let len = data.len();
295 let lookback_total = 6 * (period.saturating_sub(1));
296 debug_assert_eq!(len, out.len());
297
298 if len == 0 {
299 return Err(TilsonError::EmptyInputData);
300 }
301 if period == 0 || len.saturating_sub(first_valid) < period {
302 return Err(TilsonError::InvalidPeriod {
303 period,
304 data_len: len,
305 });
306 }
307 if v_factor.is_nan() || v_factor.is_infinite() {
308 return Err(TilsonError::InvalidVolumeFactor { v_factor });
309 }
310 if lookback_total + first_valid >= len {
311 return Err(TilsonError::NotEnoughValidData {
312 needed: lookback_total + 1,
313 valid: len - first_valid,
314 });
315 }
316
317 let k = 2.0 / (period as f64 + 1.0);
318 let omk = 1.0 - k;
319 let inv_p = 1.0 / (period as f64);
320
321 let t = v_factor * v_factor;
322 let c1 = -(t * v_factor);
323 let c2 = 3.0 * (t - c1);
324 let c3 = -6.0 * t - 3.0 * (v_factor - c1);
325 let c4 = 1.0 + 3.0 * v_factor - c1 + 3.0 * t;
326
327 let dp = unsafe { data.as_ptr().add(first_valid) };
328 let outp = out.as_mut_ptr();
329
330 let (mut e1, mut e2, mut e3, mut e4, mut e5, mut e6);
331
332 let mut today = 0usize;
333
334 let mut sum = 0.0;
335 unsafe {
336 let mut i = 0usize;
337 while i + 4 <= period {
338 let base = dp.add(today + i);
339 sum += *base + *base.add(1) + *base.add(2) + *base.add(3);
340 i += 4;
341 }
342 while i < period {
343 sum += *dp.add(today + i);
344 i += 1;
345 }
346 }
347 e1 = sum * inv_p;
348 today += period;
349
350 let mut acc = e1;
351 unsafe {
352 let mut j = 1usize;
353 while j < period {
354 let x = *dp.add(today);
355 e1 = k * x + omk * e1;
356 acc += e1;
357 today += 1;
358 j += 1;
359 }
360 }
361 e2 = acc * inv_p;
362
363 acc = e2;
364 unsafe {
365 let mut j = 1usize;
366 while j < period {
367 let x = *dp.add(today);
368 e1 = k * x + omk * e1;
369 e2 = k * e1 + omk * e2;
370 acc += e2;
371 today += 1;
372 j += 1;
373 }
374 }
375 e3 = acc * inv_p;
376
377 acc = e3;
378 unsafe {
379 let mut j = 1usize;
380 while j < period {
381 let x = *dp.add(today);
382 e1 = k * x + omk * e1;
383 e2 = k * e1 + omk * e2;
384 e3 = k * e2 + omk * e3;
385 acc += e3;
386 today += 1;
387 j += 1;
388 }
389 }
390 e4 = acc * inv_p;
391
392 acc = e4;
393 unsafe {
394 let mut j = 1usize;
395 while j < period {
396 let x = *dp.add(today);
397 e1 = k * x + omk * e1;
398 e2 = k * e1 + omk * e2;
399 e3 = k * e2 + omk * e3;
400 e4 = k * e3 + omk * e4;
401 acc += e4;
402 today += 1;
403 j += 1;
404 }
405 }
406 e5 = acc * inv_p;
407
408 acc = e5;
409 unsafe {
410 let mut j = 1usize;
411 while j < period {
412 let x = *dp.add(today);
413 e1 = k * x + omk * e1;
414 e2 = k * e1 + omk * e2;
415 e3 = k * e2 + omk * e3;
416 e4 = k * e3 + omk * e4;
417 e5 = k * e4 + omk * e5;
418 acc += e5;
419 today += 1;
420 j += 1;
421 }
422 }
423 e6 = acc * inv_p;
424
425 let start_idx = first_valid + lookback_total;
426
427 unsafe {
428 *outp.add(start_idx) = c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3;
429
430 let mut dp_cur = dp.add(today);
431 let dp_end = dp.add(len - first_valid);
432 let mut out_cur = outp.add(start_idx + 1);
433 while dp_cur < dp_end {
434 let x = *dp_cur;
435 e1 = k * x + omk * e1;
436 e2 = k * e1 + omk * e2;
437 e3 = k * e2 + omk * e3;
438 e4 = k * e3 + omk * e4;
439 e5 = k * e4 + omk * e5;
440 e6 = k * e5 + omk * e6;
441
442 *out_cur = c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3;
443
444 dp_cur = dp_cur.add(1);
445 out_cur = out_cur.add(1);
446 }
447 }
448
449 Ok(())
450}
451
452#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
453#[inline]
454unsafe fn tilson_simd128(
455 data: &[f64],
456 period: usize,
457 v_factor: f64,
458 first_valid: usize,
459 out: &mut [f64],
460) -> Result<(), TilsonError> {
461 use core::arch::wasm32::*;
462
463 tilson_scalar(data, period, v_factor, first_valid, out)
464}
465
466#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
467#[target_feature(enable = "avx512f")]
468#[inline]
469pub unsafe fn tilson_avx512(
470 data: &[f64],
471 period: usize,
472 v_factor: f64,
473 first_valid: usize,
474 out: &mut [f64],
475) -> Result<(), TilsonError> {
476 use core::arch::x86_64::*;
477
478 #[inline(always)]
479 unsafe fn dot4_avx512(
480 e3: f64,
481 e4: f64,
482 e5: f64,
483 e6: f64,
484 c4: f64,
485 c3: f64,
486 c2: f64,
487 c1: f64,
488 ) -> f64 {
489 let ve = _mm512_setr_pd(e3, e4, e5, e6, 0.0, 0.0, 0.0, 0.0);
490 let vc = _mm512_setr_pd(c4, c3, c2, c1, 0.0, 0.0, 0.0, 0.0);
491 let prod = _mm512_mul_pd(ve, vc);
492 _mm512_reduce_add_pd(prod)
493 }
494
495 let len = data.len();
496 let lookback_total = 6 * (period.saturating_sub(1));
497 if len == 0 {
498 return Err(TilsonError::EmptyInputData);
499 }
500 if period == 0 || len.saturating_sub(first_valid) < period {
501 return Err(TilsonError::InvalidPeriod {
502 period,
503 data_len: len,
504 });
505 }
506 if v_factor.is_nan() || v_factor.is_infinite() {
507 return Err(TilsonError::InvalidVolumeFactor { v_factor });
508 }
509 if lookback_total + first_valid >= len {
510 return Err(TilsonError::NotEnoughValidData {
511 needed: lookback_total + 1,
512 valid: len - first_valid,
513 });
514 }
515
516 let k = 2.0 / (period as f64 + 1.0);
517 let omk = 1.0 - k;
518 let inv_p = 1.0 / (period as f64);
519
520 let t = v_factor * v_factor;
521 let c1 = -(t * v_factor);
522 let c2 = 3.0 * (t - c1);
523 let c3 = -6.0 * t - 3.0 * (v_factor - c1);
524 let c4 = 1.0 + 3.0 * v_factor - c1 + 3.0 * t;
525
526 let dp = data.as_ptr().add(first_valid);
527 let outp = out.as_mut_ptr();
528
529 let mut today = 0usize;
530 let (mut e1, mut e2, mut e3, mut e4, mut e5, mut e6);
531
532 let mut sum = 0.0;
533 {
534 let mut i = 0usize;
535 while i + 4 <= period {
536 let base = dp.add(today + i);
537 sum += *base + *base.add(1) + *base.add(2) + *base.add(3);
538 i += 4;
539 }
540 while i < period {
541 sum += *dp.add(today + i);
542 i += 1;
543 }
544 }
545 e1 = sum * inv_p;
546 today += period;
547
548 let mut acc = e1;
549 {
550 let mut j = 1usize;
551 while j < period {
552 let x = *dp.add(today);
553 e1 = k * x + omk * e1;
554 acc += e1;
555 today += 1;
556 j += 1;
557 }
558 }
559 e2 = acc * inv_p;
560
561 acc = e2;
562 {
563 let mut j = 1usize;
564 while j < period {
565 let x = *dp.add(today);
566 e1 = k * x + omk * e1;
567 e2 = k * e1 + omk * e2;
568 acc += e2;
569 today += 1;
570 j += 1;
571 }
572 }
573 e3 = acc * inv_p;
574
575 acc = e3;
576 {
577 let mut j = 1usize;
578 while j < period {
579 let x = *dp.add(today);
580 e1 = k * x + omk * e1;
581 e2 = k * e1 + omk * e2;
582 e3 = k * e2 + omk * e3;
583 acc += e3;
584 today += 1;
585 j += 1;
586 }
587 }
588 e4 = acc * inv_p;
589
590 acc = e4;
591 {
592 let mut j = 1usize;
593 while j < period {
594 let x = *dp.add(today);
595 e1 = k * x + omk * e1;
596 e2 = k * e1 + omk * e2;
597 e3 = k * e2 + omk * e3;
598 e4 = k * e3 + omk * e4;
599 acc += e4;
600 today += 1;
601 j += 1;
602 }
603 }
604 e5 = acc * inv_p;
605
606 acc = e5;
607 {
608 let mut j = 1usize;
609 while j < period {
610 let x = *dp.add(today);
611 e1 = k * x + omk * e1;
612 e2 = k * e1 + omk * e2;
613 e3 = k * e2 + omk * e3;
614 e4 = k * e3 + omk * e4;
615 e5 = k * e4 + omk * e5;
616 acc += e5;
617 today += 1;
618 j += 1;
619 }
620 }
621 e6 = acc * inv_p;
622
623 let start_idx = first_valid + lookback_total;
624 let end_idx = len - 1;
625
626 *outp.add(start_idx) = dot4_avx512(e3, e4, e5, e6, c4, c3, c2, c1);
627
628 let mut idx = start_idx + 1;
629 while (first_valid + today) <= end_idx {
630 let x = *dp.add(today);
631 e1 = k * x + omk * e1;
632 e2 = k * e1 + omk * e2;
633 e3 = k * e2 + omk * e3;
634 e4 = k * e3 + omk * e4;
635 e5 = k * e4 + omk * e5;
636 e6 = k * e5 + omk * e6;
637
638 *outp.add(idx) = dot4_avx512(e3, e4, e5, e6, c4, c3, c2, c1);
639
640 today += 1;
641 idx += 1;
642 }
643
644 Ok(())
645}
646
647#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
648#[target_feature(enable = "avx2")]
649#[inline]
650pub unsafe fn tilson_avx2(
651 data: &[f64],
652 period: usize,
653 v_factor: f64,
654 first_valid: usize,
655 out: &mut [f64],
656) -> Result<(), TilsonError> {
657 use core::arch::x86_64::*;
658
659 #[inline(always)]
660 unsafe fn dot4_avx2(
661 e3: f64,
662 e4: f64,
663 e5: f64,
664 e6: f64,
665 c4: f64,
666 c3: f64,
667 c2: f64,
668 c1: f64,
669 ) -> f64 {
670 let ve = _mm256_setr_pd(e3, e4, e5, e6);
671 let vc = _mm256_setr_pd(c4, c3, c2, c1);
672 let prod = _mm256_mul_pd(ve, vc);
673 let lo = _mm256_castpd256_pd128(prod);
674 let hi = _mm256_extractf128_pd(prod, 1);
675 let s0 = _mm_hadd_pd(lo, lo);
676 let s1 = _mm_hadd_pd(hi, hi);
677 let sum = _mm_add_sd(s0, s1);
678 _mm_cvtsd_f64(sum)
679 }
680
681 let len = data.len();
682 let lookback_total = 6 * (period.saturating_sub(1));
683 if len == 0 {
684 return Err(TilsonError::EmptyInputData);
685 }
686 if period == 0 || len.saturating_sub(first_valid) < period {
687 return Err(TilsonError::InvalidPeriod {
688 period,
689 data_len: len,
690 });
691 }
692 if v_factor.is_nan() || v_factor.is_infinite() {
693 return Err(TilsonError::InvalidVolumeFactor { v_factor });
694 }
695 if lookback_total + first_valid >= len {
696 return Err(TilsonError::NotEnoughValidData {
697 needed: lookback_total + 1,
698 valid: len - first_valid,
699 });
700 }
701
702 let k = 2.0 / (period as f64 + 1.0);
703 let omk = 1.0 - k;
704 let inv_p = 1.0 / (period as f64);
705
706 let t = v_factor * v_factor;
707 let c1 = -(t * v_factor);
708 let c2 = 3.0 * (t - c1);
709 let c3 = -6.0 * t - 3.0 * (v_factor - c1);
710 let c4 = 1.0 + 3.0 * v_factor - c1 + 3.0 * t;
711
712 let dp = data.as_ptr().add(first_valid);
713 let outp = out.as_mut_ptr();
714
715 let mut today = 0usize;
716 let (mut e1, mut e2, mut e3, mut e4, mut e5, mut e6);
717
718 let mut sum = 0.0;
719 {
720 let mut i = 0usize;
721 while i + 4 <= period {
722 let base = dp.add(today + i);
723 sum += *base + *base.add(1) + *base.add(2) + *base.add(3);
724 i += 4;
725 }
726 while i < period {
727 sum += *dp.add(today + i);
728 i += 1;
729 }
730 }
731 e1 = sum * inv_p;
732 today += period;
733
734 let mut acc = e1;
735 {
736 let mut j = 1usize;
737 while j < period {
738 let x = *dp.add(today);
739 e1 = k * x + omk * e1;
740 acc += e1;
741 today += 1;
742 j += 1;
743 }
744 }
745 e2 = acc * inv_p;
746
747 acc = e2;
748 {
749 let mut j = 1usize;
750 while j < period {
751 let x = *dp.add(today);
752 e1 = k * x + omk * e1;
753 e2 = k * e1 + omk * e2;
754 acc += e2;
755 today += 1;
756 j += 1;
757 }
758 }
759 e3 = acc * inv_p;
760
761 acc = e3;
762 {
763 let mut j = 1usize;
764 while j < period {
765 let x = *dp.add(today);
766 e1 = k * x + omk * e1;
767 e2 = k * e1 + omk * e2;
768 e3 = k * e2 + omk * e3;
769 acc += e3;
770 today += 1;
771 j += 1;
772 }
773 }
774 e4 = acc * inv_p;
775
776 acc = e4;
777 {
778 let mut j = 1usize;
779 while j < period {
780 let x = *dp.add(today);
781 e1 = k * x + omk * e1;
782 e2 = k * e1 + omk * e2;
783 e3 = k * e2 + omk * e3;
784 e4 = k * e3 + omk * e4;
785 acc += e4;
786 today += 1;
787 j += 1;
788 }
789 }
790 e5 = acc * inv_p;
791
792 acc = e5;
793 {
794 let mut j = 1usize;
795 while j < period {
796 let x = *dp.add(today);
797 e1 = k * x + omk * e1;
798 e2 = k * e1 + omk * e2;
799 e3 = k * e2 + omk * e3;
800 e4 = k * e3 + omk * e4;
801 e5 = k * e4 + omk * e5;
802 acc += e5;
803 today += 1;
804 j += 1;
805 }
806 }
807 e6 = acc * inv_p;
808
809 let start_idx = first_valid + lookback_total;
810 let end_idx = len - 1;
811
812 *outp.add(start_idx) = dot4_avx2(e3, e4, e5, e6, c4, c3, c2, c1);
813
814 let mut idx = start_idx + 1;
815 while (first_valid + today) <= end_idx {
816 let x = *dp.add(today);
817 e1 = k * x + omk * e1;
818 e2 = k * e1 + omk * e2;
819 e3 = k * e2 + omk * e3;
820 e4 = k * e3 + omk * e4;
821 e5 = k * e4 + omk * e5;
822 e6 = k * e5 + omk * e6;
823
824 *outp.add(idx) = dot4_avx2(e3, e4, e5, e6, c4, c3, c2, c1);
825
826 today += 1;
827 idx += 1;
828 }
829
830 Ok(())
831}
832
833#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
834#[inline]
835pub unsafe fn tilson_avx512_short(
836 data: &[f64],
837 period: usize,
838 v_factor: f64,
839 first_valid: usize,
840 out: &mut [f64],
841) -> Result<(), TilsonError> {
842 tilson_avx512(data, period, v_factor, first_valid, out)
843}
844
845#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
846#[inline]
847pub unsafe fn tilson_avx512_long(
848 data: &[f64],
849 period: usize,
850 v_factor: f64,
851 first_valid: usize,
852 out: &mut [f64],
853) -> Result<(), TilsonError> {
854 tilson_avx512(data, period, v_factor, first_valid, out)
855}
856
857#[inline]
858pub fn tilson_batch_with_kernel(
859 data: &[f64],
860 sweep: &TilsonBatchRange,
861 k: Kernel,
862) -> Result<TilsonBatchOutput, TilsonError> {
863 let kernel = match k {
864 Kernel::Auto => detect_best_batch_kernel(),
865 other if other.is_batch() => other,
866 _ => return Err(TilsonError::InvalidKernelForBatch(k)),
867 };
868
869 let simd = match kernel {
870 Kernel::Avx512Batch => Kernel::Avx512,
871 Kernel::Avx2Batch => Kernel::Avx2,
872 Kernel::ScalarBatch => Kernel::Scalar,
873 _ => unreachable!(),
874 };
875 tilson_batch_par_slice(data, sweep, simd)
876}
877
878#[derive(Clone, Debug)]
879pub struct TilsonBatchRange {
880 pub period: (usize, usize, usize),
881 pub volume_factor: (f64, f64, f64),
882}
883
884impl Default for TilsonBatchRange {
885 fn default() -> Self {
886 Self {
887 period: (5, 254, 1),
888 volume_factor: (0.0, 0.0, 0.0),
889 }
890 }
891}
892
893#[derive(Clone, Debug, Default)]
894pub struct TilsonBatchBuilder {
895 range: TilsonBatchRange,
896 kernel: Kernel,
897}
898
899impl TilsonBatchBuilder {
900 pub fn new() -> Self {
901 Self::default()
902 }
903 pub fn kernel(mut self, k: Kernel) -> Self {
904 self.kernel = k;
905 self
906 }
907
908 #[inline]
909 pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
910 self.range.period = (start, end, step);
911 self
912 }
913 #[inline]
914 pub fn period_static(mut self, p: usize) -> Self {
915 self.range.period = (p, p, 0);
916 self
917 }
918
919 #[inline]
920 pub fn volume_factor_range(mut self, start: f64, end: f64, step: f64) -> Self {
921 self.range.volume_factor = (start, end, step);
922 self
923 }
924 #[inline]
925 pub fn volume_factor_static(mut self, v: f64) -> Self {
926 self.range.volume_factor = (v, v, 0.0);
927 self
928 }
929
930 pub fn apply_slice(self, data: &[f64]) -> Result<TilsonBatchOutput, TilsonError> {
931 tilson_batch_with_kernel(data, &self.range, self.kernel)
932 }
933 pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<TilsonBatchOutput, TilsonError> {
934 TilsonBatchBuilder::new().kernel(k).apply_slice(data)
935 }
936
937 pub fn apply_candles(self, c: &Candles, src: &str) -> Result<TilsonBatchOutput, TilsonError> {
938 let slice = source_type(c, src);
939 self.apply_slice(slice)
940 }
941
942 pub fn with_default_candles(c: &Candles) -> Result<TilsonBatchOutput, TilsonError> {
943 TilsonBatchBuilder::new()
944 .kernel(Kernel::Auto)
945 .apply_candles(c, "close")
946 }
947}
948
949#[derive(Clone, Debug)]
950pub struct TilsonBatchOutput {
951 pub values: Vec<f64>,
952 pub combos: Vec<TilsonParams>,
953 pub rows: usize,
954 pub cols: usize,
955}
956impl TilsonBatchOutput {
957 pub fn row_for_params(&self, p: &TilsonParams) -> Option<usize> {
958 self.combos.iter().position(|c| {
959 c.period.unwrap_or(5) == p.period.unwrap_or(5)
960 && (c.volume_factor.unwrap_or(0.0) - p.volume_factor.unwrap_or(0.0)).abs() < 1e-12
961 })
962 }
963 pub fn values_for(&self, p: &TilsonParams) -> Option<&[f64]> {
964 self.row_for_params(p).map(|row| {
965 let start = row * self.cols;
966 &self.values[start..start + self.cols]
967 })
968 }
969}
970
971#[inline(always)]
972fn expand_grid(r: &TilsonBatchRange) -> Result<Vec<TilsonParams>, TilsonError> {
973 fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, TilsonError> {
974 if step == 0 || start == end {
975 return Ok(vec![start]);
976 }
977 if start < end {
978 return Ok((start..=end).step_by(step).collect());
979 }
980
981 let mut v = Vec::new();
982 let mut cur = start;
983 loop {
984 v.push(cur);
985 match cur.checked_sub(step) {
986 Some(next) if next >= end => {
987 cur = next;
988 }
989 _ => break,
990 }
991 }
992 if v.is_empty() {
993 Err(TilsonError::InvalidRangeUsize { start, end, step })
994 } else {
995 Ok(v)
996 }
997 }
998 fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, TilsonError> {
999 if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
1000 return Ok(vec![start]);
1001 }
1002 let mut v = Vec::new();
1003 let mut x = start;
1004 if step > 0.0 {
1005 while x <= end + 1e-12 {
1006 v.push(x);
1007 x += step;
1008 }
1009 } else {
1010 while x >= end - 1e-12 {
1011 v.push(x);
1012 x += step;
1013 }
1014 }
1015 if v.is_empty() {
1016 Err(TilsonError::InvalidRangeF64 { start, end, step })
1017 } else {
1018 Ok(v)
1019 }
1020 }
1021
1022 let periods = axis_usize(r.period)?;
1023 let v_factors = axis_f64(r.volume_factor)?;
1024
1025 let mut out = Vec::with_capacity(periods.len().saturating_mul(v_factors.len()));
1026 for &p in &periods {
1027 for &v in &v_factors {
1028 out.push(TilsonParams {
1029 period: Some(p),
1030 volume_factor: Some(v),
1031 });
1032 }
1033 }
1034 if out.is_empty() {
1035 return Err(TilsonError::InvalidInput("empty parameter sweep"));
1036 }
1037 Ok(out)
1038}
1039
1040#[inline(always)]
1041pub fn tilson_batch_slice(
1042 data: &[f64],
1043 sweep: &TilsonBatchRange,
1044 kern: Kernel,
1045) -> Result<TilsonBatchOutput, TilsonError> {
1046 tilson_batch_inner(data, sweep, kern, false)
1047}
1048
1049#[inline(always)]
1050pub fn tilson_batch_par_slice(
1051 data: &[f64],
1052 sweep: &TilsonBatchRange,
1053 kern: Kernel,
1054) -> Result<TilsonBatchOutput, TilsonError> {
1055 tilson_batch_inner(data, sweep, kern, true)
1056}
1057
1058#[inline(always)]
1059fn tilson_batch_inner(
1060 data: &[f64],
1061 sweep: &TilsonBatchRange,
1062 kern: Kernel,
1063 parallel: bool,
1064) -> Result<TilsonBatchOutput, TilsonError> {
1065 let combos = expand_grid(sweep)?;
1066
1067 if data.is_empty() {
1068 return Err(TilsonError::EmptyInputData);
1069 }
1070
1071 let first = data
1072 .iter()
1073 .position(|x| !x.is_nan())
1074 .ok_or(TilsonError::AllValuesNaN)?;
1075 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
1076 if data.len() - first < 6 * (max_p - 1) + 1 {
1077 return Err(TilsonError::NotEnoughValidData {
1078 needed: 6 * (max_p - 1) + 1,
1079 valid: data.len() - first,
1080 });
1081 }
1082 let rows = combos.len();
1083 let cols = data.len();
1084 let warm: Vec<usize> = combos
1085 .iter()
1086 .map(|c| first + 6 * (c.period.unwrap() - 1))
1087 .collect();
1088
1089 let mut raw = make_uninit_matrix(rows, cols);
1090 unsafe { init_matrix_prefixes(&mut raw, cols, &warm) };
1091
1092 let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
1093 let period = combos[row].period.unwrap();
1094 let v_factor = combos[row].volume_factor.unwrap();
1095
1096 let out_row =
1097 core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
1098 tilson_row_scalar(data, first, period, v_factor, out_row);
1099 };
1100
1101 if parallel {
1102 #[cfg(not(target_arch = "wasm32"))]
1103 {
1104 raw.par_chunks_mut(cols)
1105 .enumerate()
1106 .for_each(|(row, slice)| do_row(row, slice));
1107 }
1108
1109 #[cfg(target_arch = "wasm32")]
1110 {
1111 for (row, slice) in raw.chunks_mut(cols).enumerate() {
1112 do_row(row, slice);
1113 }
1114 }
1115 } else {
1116 for (row, slice) in raw.chunks_mut(cols).enumerate() {
1117 do_row(row, slice);
1118 }
1119 }
1120
1121 let mut guard = core::mem::ManuallyDrop::new(raw);
1122 let total_cells = rows
1123 .checked_mul(cols)
1124 .ok_or(TilsonError::InvalidInput("rows*cols overflow"))?;
1125 let values = unsafe {
1126 Vec::from_raw_parts(
1127 guard.as_mut_ptr() as *mut f64,
1128 total_cells,
1129 guard.capacity(),
1130 )
1131 };
1132
1133 Ok(TilsonBatchOutput {
1134 values,
1135 combos,
1136 rows,
1137 cols,
1138 })
1139}
1140
1141#[inline(always)]
1142pub unsafe fn tilson_row_scalar(
1143 data: &[f64],
1144 first: usize,
1145 period: usize,
1146 v_factor: f64,
1147 out: &mut [f64],
1148) {
1149 let _ = tilson_scalar(data, period, v_factor, first, out);
1150}
1151
1152#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1153#[inline(always)]
1154pub unsafe fn tilson_row_avx2(
1155 data: &[f64],
1156 first: usize,
1157 period: usize,
1158 v_factor: f64,
1159 out: &mut [f64],
1160) {
1161 tilson_row_scalar(data, first, period, v_factor, out);
1162}
1163
1164#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1165#[inline(always)]
1166pub unsafe fn tilson_row_avx512(
1167 data: &[f64],
1168 first: usize,
1169 period: usize,
1170 v_factor: f64,
1171 out: &mut [f64],
1172) {
1173 tilson_row_scalar(data, first, period, v_factor, out);
1174}
1175
1176#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1177#[inline(always)]
1178pub unsafe fn tilson_row_avx512_short(
1179 data: &[f64],
1180 first: usize,
1181 period: usize,
1182 v_factor: f64,
1183 out: &mut [f64],
1184) {
1185 tilson_row_scalar(data, first, period, v_factor, out);
1186}
1187
1188#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1189#[inline(always)]
1190pub unsafe fn tilson_row_avx512_long(
1191 data: &[f64],
1192 first: usize,
1193 period: usize,
1194 v_factor: f64,
1195 out: &mut [f64],
1196) {
1197 tilson_row_scalar(data, first, period, v_factor, out);
1198}
1199
1200#[derive(Debug, Clone)]
1201pub struct TilsonStream {
1202 period: usize,
1203 v_factor: f64,
1204
1205 e1: f64,
1206 e2: f64,
1207 e3: f64,
1208 e4: f64,
1209 e5: f64,
1210 e6: f64,
1211
1212 k: f64,
1213 one_minus_k: f64,
1214 inv_p: f64,
1215 c1: f64,
1216 c2: f64,
1217 c3: f64,
1218 c4: f64,
1219
1220 phase: u8,
1221 in_phase_count: usize,
1222 sum_e1: f64,
1223 acc: f64,
1224
1225 lookback_total: usize,
1226 values_seen: usize,
1227}
1228
1229impl TilsonStream {
1230 pub fn try_new(params: TilsonParams) -> Result<Self, TilsonError> {
1231 let period = params.period.unwrap_or(5);
1232 let v_factor = params.volume_factor.unwrap_or(0.0);
1233
1234 if period == 0 {
1235 return Err(TilsonError::InvalidPeriod {
1236 period,
1237 data_len: 0,
1238 });
1239 }
1240 if v_factor.is_nan() || v_factor.is_infinite() {
1241 return Err(TilsonError::InvalidVolumeFactor { v_factor });
1242 }
1243
1244 let k = 2.0 / (period as f64 + 1.0);
1245 let one_minus_k = 1.0 - k;
1246 let inv_p = 1.0 / (period as f64);
1247
1248 let t = v_factor * v_factor;
1249 let c1 = -(t * v_factor);
1250 let c2 = 3.0 * (t - c1);
1251 let c3 = -6.0 * t - 3.0 * (v_factor - c1);
1252 let c4 = 1.0 + 3.0 * v_factor - c1 + 3.0 * t;
1253
1254 Ok(Self {
1255 period,
1256 v_factor,
1257 e1: 0.0,
1258 e2: 0.0,
1259 e3: 0.0,
1260 e4: 0.0,
1261 e5: 0.0,
1262 e6: 0.0,
1263 k,
1264 one_minus_k,
1265 inv_p,
1266 c1,
1267 c2,
1268 c3,
1269 c4,
1270 phase: 0,
1271 in_phase_count: 0,
1272 sum_e1: 0.0,
1273 acc: 0.0,
1274 lookback_total: 6 * (period - 1),
1275 values_seen: 0,
1276 })
1277 }
1278
1279 #[inline(always)]
1280 pub fn update(&mut self, value: f64) -> Option<f64> {
1281 self.values_seen = self.values_seen.wrapping_add(1);
1282
1283 match self.phase {
1284 0 => {
1285 self.sum_e1 += value;
1286 self.in_phase_count += 1;
1287 if self.in_phase_count == self.period {
1288 self.e1 = self.sum_e1 * self.inv_p;
1289 self.in_phase_count = 0;
1290
1291 if self.period == 1 {
1292 self.e2 = self.e1;
1293 self.e3 = self.e2;
1294 self.e4 = self.e3;
1295 self.e5 = self.e4;
1296 self.e6 = self.e5;
1297 self.phase = 6;
1298 return Some(self.combine_exact());
1299 }
1300
1301 self.acc = self.e1;
1302 self.phase = 1;
1303 }
1304 None
1305 }
1306
1307 1 => {
1308 self.cascade_update_upto(value, 1);
1309 self.acc += self.e1;
1310 self.in_phase_count += 1;
1311 if self.in_phase_count == self.period - 1 {
1312 self.e2 = self.acc * self.inv_p;
1313 self.acc = self.e2;
1314 self.in_phase_count = 0;
1315 self.phase = 2;
1316 }
1317 None
1318 }
1319
1320 2 => {
1321 self.cascade_update_upto(value, 2);
1322 self.acc += self.e2;
1323 self.in_phase_count += 1;
1324 if self.in_phase_count == self.period - 1 {
1325 self.e3 = self.acc * self.inv_p;
1326 self.acc = self.e3;
1327 self.in_phase_count = 0;
1328 self.phase = 3;
1329 }
1330 None
1331 }
1332
1333 3 => {
1334 self.cascade_update_upto(value, 3);
1335 self.acc += self.e3;
1336 self.in_phase_count += 1;
1337 if self.in_phase_count == self.period - 1 {
1338 self.e4 = self.acc * self.inv_p;
1339 self.acc = self.e4;
1340 self.in_phase_count = 0;
1341 self.phase = 4;
1342 }
1343 None
1344 }
1345
1346 4 => {
1347 self.cascade_update_upto(value, 4);
1348 self.acc += self.e4;
1349 self.in_phase_count += 1;
1350 if self.in_phase_count == self.period - 1 {
1351 self.e5 = self.acc * self.inv_p;
1352 self.acc = self.e5;
1353 self.in_phase_count = 0;
1354 self.phase = 5;
1355 }
1356 None
1357 }
1358
1359 5 => {
1360 self.cascade_update_upto(value, 5);
1361 self.acc += self.e5;
1362 self.in_phase_count += 1;
1363 if self.in_phase_count == self.period - 1 {
1364 self.e6 = self.acc * self.inv_p;
1365 self.phase = 6;
1366 self.in_phase_count = 0;
1367
1368 return Some(self.combine_exact());
1369 }
1370 None
1371 }
1372
1373 _ => {
1374 self.cascade_update_upto(value, 6);
1375 Some(self.combine_exact())
1376 }
1377 }
1378 }
1379
1380 #[inline(always)]
1381 fn cascade_update_upto(&mut self, x: f64, upto: u8) {
1382 let k = self.k;
1383 let omk = self.one_minus_k;
1384
1385 self.e1 = k * x + omk * self.e1;
1386 if upto == 1 {
1387 return;
1388 }
1389
1390 self.e2 = k * self.e1 + omk * self.e2;
1391 if upto == 2 {
1392 return;
1393 }
1394
1395 self.e3 = k * self.e2 + omk * self.e3;
1396 if upto == 3 {
1397 return;
1398 }
1399
1400 self.e4 = k * self.e3 + omk * self.e4;
1401 if upto == 4 {
1402 return;
1403 }
1404
1405 self.e5 = k * self.e4 + omk * self.e5;
1406 if upto == 5 {
1407 return;
1408 }
1409
1410 self.e6 = k * self.e5 + omk * self.e6;
1411 }
1412
1413 #[inline(always)]
1414 fn combine_exact(&self) -> f64 {
1415 let s0 = self.c1 * self.e6 + self.c2 * self.e5;
1416 let s1 = s0 + self.c3 * self.e4;
1417 s1 + self.c4 * self.e3
1418 }
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423 use super::*;
1424 use crate::skip_if_unsupported;
1425 use crate::utilities::data_loader::read_candles_from_csv;
1426
1427 fn check_tilson_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1428 skip_if_unsupported!(kernel, test_name);
1429 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1430 let candles = read_candles_from_csv(file_path)?;
1431
1432 let default_params = TilsonParams {
1433 period: None,
1434 volume_factor: None,
1435 };
1436 let input = TilsonInput::from_candles(&candles, "close", default_params);
1437 let output = tilson_with_kernel(&input, kernel)?;
1438 assert_eq!(output.values.len(), candles.close.len());
1439 Ok(())
1440 }
1441
1442 fn check_tilson_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1443 skip_if_unsupported!(kernel, test_name);
1444 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1445 let candles = read_candles_from_csv(file_path)?;
1446
1447 let input = TilsonInput::from_candles(&candles, "close", TilsonParams::default());
1448 let result = tilson_with_kernel(&input, kernel)?;
1449 let expected_last_five = [
1450 59304.716332473254,
1451 59283.56868015526,
1452 59261.16173577631,
1453 59240.25895948583,
1454 59203.544843167765,
1455 ];
1456 let start = result.values.len().saturating_sub(5);
1457 for (i, &val) in result.values[start..].iter().enumerate() {
1458 let diff = (val - expected_last_five[i]).abs();
1459 assert!(
1460 diff < 1e-8,
1461 "[{}] TILSON {:?} mismatch at idx {}: got {}, expected {}",
1462 test_name,
1463 kernel,
1464 i,
1465 val,
1466 expected_last_five[i]
1467 );
1468 }
1469 Ok(())
1470 }
1471
1472 fn check_tilson_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1473 skip_if_unsupported!(kernel, test_name);
1474 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1475 let candles = read_candles_from_csv(file_path)?;
1476
1477 let input = TilsonInput::with_default_candles(&candles);
1478 match input.data {
1479 TilsonData::Candles { source, .. } => assert_eq!(source, "close"),
1480 _ => panic!("Expected TilsonData::Candles"),
1481 }
1482 let output = tilson_with_kernel(&input, kernel)?;
1483 assert_eq!(output.values.len(), candles.close.len());
1484 Ok(())
1485 }
1486
1487 fn check_tilson_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1488 skip_if_unsupported!(kernel, test_name);
1489 let input_data = [10.0, 20.0, 30.0];
1490 let params = TilsonParams {
1491 period: Some(0),
1492 volume_factor: None,
1493 };
1494 let input = TilsonInput::from_slice(&input_data, params);
1495 let res = tilson_with_kernel(&input, kernel);
1496 assert!(
1497 res.is_err(),
1498 "[{}] TILSON should fail with zero period",
1499 test_name
1500 );
1501 Ok(())
1502 }
1503
1504 fn check_tilson_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1505 skip_if_unsupported!(kernel, test_name);
1506 let input_data: [f64; 0] = [];
1507 let params = TilsonParams {
1508 period: Some(5),
1509 volume_factor: Some(0.0),
1510 };
1511 let input = TilsonInput::from_slice(&input_data, params);
1512 let res = tilson_with_kernel(&input, kernel);
1513 assert!(
1514 res.is_err(),
1515 "[{}] TILSON should fail with empty input",
1516 test_name
1517 );
1518 if let Err(e) = res {
1519 assert!(
1520 matches!(e, TilsonError::EmptyInputData),
1521 "[{}] Expected EmptyInputData error",
1522 test_name
1523 );
1524 }
1525 Ok(())
1526 }
1527
1528 fn check_tilson_period_exceeds_length(
1529 test_name: &str,
1530 kernel: Kernel,
1531 ) -> Result<(), Box<dyn Error>> {
1532 skip_if_unsupported!(kernel, test_name);
1533 let data_small = [10.0, 20.0, 30.0];
1534 let params = TilsonParams {
1535 period: Some(10),
1536 volume_factor: None,
1537 };
1538 let input = TilsonInput::from_slice(&data_small, params);
1539 let res = tilson_with_kernel(&input, kernel);
1540 assert!(
1541 res.is_err(),
1542 "[{}] TILSON should fail with period exceeding length",
1543 test_name
1544 );
1545 Ok(())
1546 }
1547
1548 fn check_tilson_very_small_dataset(
1549 test_name: &str,
1550 kernel: Kernel,
1551 ) -> Result<(), Box<dyn Error>> {
1552 skip_if_unsupported!(kernel, test_name);
1553 let single_point = [42.0];
1554 let params = TilsonParams {
1555 period: Some(9),
1556 volume_factor: None,
1557 };
1558 let input = TilsonInput::from_slice(&single_point, params);
1559 let res = tilson_with_kernel(&input, kernel);
1560 assert!(
1561 res.is_err(),
1562 "[{}] TILSON should fail with insufficient data",
1563 test_name
1564 );
1565 Ok(())
1566 }
1567
1568 fn check_tilson_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1569 skip_if_unsupported!(kernel, test_name);
1570 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1571 let candles = read_candles_from_csv(file_path)?;
1572
1573 let first_params = TilsonParams {
1574 period: Some(5),
1575 volume_factor: None,
1576 };
1577 let first_input = TilsonInput::from_candles(&candles, "close", first_params);
1578 let first_result = tilson_with_kernel(&first_input, kernel)?;
1579
1580 let second_params = TilsonParams {
1581 period: Some(3),
1582 volume_factor: Some(0.7),
1583 };
1584 let second_input = TilsonInput::from_slice(&first_result.values, second_params);
1585 let second_result = tilson_with_kernel(&second_input, kernel)?;
1586
1587 assert_eq!(second_result.values.len(), first_result.values.len());
1588 for i in 240..second_result.values.len() {
1589 assert!(second_result.values[i].is_finite());
1590 }
1591 Ok(())
1592 }
1593
1594 fn check_tilson_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1595 skip_if_unsupported!(kernel, test_name);
1596 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1597 let candles = read_candles_from_csv(file_path)?;
1598
1599 let input = TilsonInput::from_candles(
1600 &candles,
1601 "close",
1602 TilsonParams {
1603 period: Some(5),
1604 volume_factor: Some(0.0),
1605 },
1606 );
1607 let res = tilson_with_kernel(&input, kernel)?;
1608 assert_eq!(res.values.len(), candles.close.len());
1609 if res.values.len() > 240 {
1610 for (i, &val) in res.values[240..].iter().enumerate() {
1611 assert!(
1612 !val.is_nan(),
1613 "[{}] Found unexpected NaN at out-index {}",
1614 test_name,
1615 240 + i
1616 );
1617 }
1618 }
1619 Ok(())
1620 }
1621
1622 fn check_tilson_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1623 skip_if_unsupported!(kernel, test_name);
1624
1625 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1626 let candles = read_candles_from_csv(file_path)?;
1627
1628 let period = 5;
1629 let v_factor = 0.0;
1630
1631 let input = TilsonInput::from_candles(
1632 &candles,
1633 "close",
1634 TilsonParams {
1635 period: Some(period),
1636 volume_factor: Some(v_factor),
1637 },
1638 );
1639 let batch_output = tilson_with_kernel(&input, kernel)?.values;
1640
1641 let mut stream = TilsonStream::try_new(TilsonParams {
1642 period: Some(period),
1643 volume_factor: Some(v_factor),
1644 })?;
1645
1646 let mut stream_values = Vec::with_capacity(candles.close.len());
1647 for &price in &candles.close {
1648 match stream.update(price) {
1649 Some(val) => stream_values.push(val),
1650 None => stream_values.push(f64::NAN),
1651 }
1652 }
1653 assert_eq!(batch_output.len(), stream_values.len());
1654 for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
1655 if b.is_nan() && s.is_nan() {
1656 continue;
1657 }
1658 let diff = (b - s).abs();
1659 assert!(
1660 diff < 1e-9,
1661 "[{}] TILSON streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
1662 test_name,
1663 i,
1664 b,
1665 s,
1666 diff
1667 );
1668 }
1669 Ok(())
1670 }
1671
1672 macro_rules! generate_all_tilson_tests {
1673 ($($test_fn:ident),*) => {
1674 paste::paste! {
1675 $(
1676 #[test]
1677 fn [<$test_fn _scalar_f64>]() {
1678 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1679 }
1680 )*
1681 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1682 $(
1683 #[test]
1684 fn [<$test_fn _avx2_f64>]() {
1685 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1686 }
1687 #[test]
1688 fn [<$test_fn _avx512_f64>]() {
1689 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1690 }
1691 )*
1692 }
1693 }
1694 }
1695
1696 #[cfg(debug_assertions)]
1697 fn check_tilson_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1698 skip_if_unsupported!(kernel, test_name);
1699
1700 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1701 let candles = read_candles_from_csv(file_path)?;
1702
1703 let test_periods = vec![3, 5, 8, 10, 15, 20, 30, 50];
1704 let test_v_factors = vec![0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0];
1705
1706 for &period in &test_periods {
1707 for &v_factor in &test_v_factors {
1708 let params = TilsonParams {
1709 period: Some(period),
1710 volume_factor: Some(v_factor),
1711 };
1712 let input = TilsonInput::from_candles(&candles, "close", params);
1713
1714 if candles.close.len() < 6 * (period - 1) + 1 {
1715 continue;
1716 }
1717
1718 let output = match tilson_with_kernel(&input, kernel) {
1719 Ok(o) => o,
1720 Err(_) => continue,
1721 };
1722
1723 for (i, &val) in output.values.iter().enumerate() {
1724 if val.is_nan() {
1725 continue;
1726 }
1727
1728 let bits = val.to_bits();
1729
1730 if bits == 0x11111111_11111111 {
1731 panic!(
1732 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} with period {} and v_factor {}",
1733 test_name, val, bits, i, period, v_factor
1734 );
1735 }
1736
1737 if bits == 0x22222222_22222222 {
1738 panic!(
1739 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} with period {} and v_factor {}",
1740 test_name, val, bits, i, period, v_factor
1741 );
1742 }
1743
1744 if bits == 0x33333333_33333333 {
1745 panic!(
1746 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} with period {} and v_factor {}",
1747 test_name, val, bits, i, period, v_factor
1748 );
1749 }
1750 }
1751 }
1752 }
1753
1754 Ok(())
1755 }
1756
1757 #[cfg(not(debug_assertions))]
1758 fn check_tilson_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1759 Ok(())
1760 }
1761
1762 #[cfg(feature = "proptest")]
1763 #[allow(clippy::float_cmp)]
1764 fn check_tilson_property(
1765 test_name: &str,
1766 kernel: Kernel,
1767 ) -> Result<(), Box<dyn std::error::Error>> {
1768 use proptest::prelude::*;
1769 skip_if_unsupported!(kernel, test_name);
1770
1771 let strat = (1usize..=30).prop_flat_map(|period| {
1772 let min_len = (6 * period.saturating_sub(1) + 1).max(period);
1773 (
1774 prop::collection::vec(
1775 (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
1776 min_len..400,
1777 ),
1778 Just(period),
1779 0.0f64..=1.0f64,
1780 )
1781 });
1782
1783 proptest::test_runner::TestRunner::default()
1784 .run(&strat, |(data, period, volume_factor)| {
1785 let params = TilsonParams {
1786 period: Some(period),
1787 volume_factor: Some(volume_factor),
1788 };
1789 let input = TilsonInput::from_slice(&data, params);
1790
1791 let TilsonOutput { values: out } = tilson_with_kernel(&input, kernel).unwrap();
1792
1793 let TilsonOutput { values: ref_out } =
1794 tilson_with_kernel(&input, Kernel::Scalar).unwrap();
1795
1796 prop_assert_eq!(out.len(), data.len(), "Output length mismatch");
1797
1798 let warmup_end = 6 * (period - 1);
1799
1800 for i in 0..warmup_end.min(out.len()) {
1801 prop_assert!(
1802 out[i].is_nan(),
1803 "Expected NaN during warmup at index {}, got {}",
1804 i,
1805 out[i]
1806 );
1807 }
1808
1809 let is_constant_data = data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-12);
1810
1811 for i in warmup_end..data.len() {
1812 let y = out[i];
1813 let r = ref_out[i];
1814
1815 if y.is_finite() && r.is_finite() {
1816 let y_bits = y.to_bits();
1817 let r_bits = r.to_bits();
1818 let ulp_diff = y_bits.abs_diff(r_bits);
1819
1820 prop_assert!(
1821 (y - r).abs() <= 1e-9 || ulp_diff <= 8,
1822 "SIMD mismatch at idx {}: {} vs {} (ULP={})",
1823 i,
1824 y,
1825 r,
1826 ulp_diff
1827 );
1828 } else {
1829 prop_assert_eq!(
1830 y.to_bits(),
1831 r.to_bits(),
1832 "Non-finite value mismatch at index {}",
1833 i
1834 );
1835 }
1836
1837 if is_constant_data && i >= warmup_end + period {
1838 let const_val = data[0];
1839 prop_assert!(
1840 (y - const_val).abs() <= 1e-9,
1841 "Constant data property failed at idx {}: expected {}, got {}",
1842 i,
1843 const_val,
1844 y
1845 );
1846 }
1847 }
1848
1849 if period == 1 {
1850 for i in 0..data.len() {
1851 if out[i].is_finite() && data[i].is_finite() {
1852 let tol = (data[i].abs() * 1e-10).max(1e-9);
1853 prop_assert!(
1854 (out[i] - data[i]).abs() <= tol,
1855 "Period=1 property failed at idx {}: expected {}, got {}, diff={}",
1856 i,
1857 data[i],
1858 out[i],
1859 (out[i] - data[i]).abs()
1860 );
1861 }
1862 }
1863 }
1864
1865 if volume_factor == 0.0 && warmup_end < data.len() {
1866 for i in warmup_end..data.len() {
1867 prop_assert!(
1868 out[i].is_finite(),
1869 "With volume_factor=0, output should be finite at idx {}",
1870 i
1871 );
1872 }
1873 }
1874
1875 Ok(())
1876 })
1877 .unwrap();
1878
1879 Ok(())
1880 }
1881
1882 #[cfg(not(feature = "proptest"))]
1883 fn check_tilson_property(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1884 skip_if_unsupported!(kernel, test_name);
1885 Ok(())
1886 }
1887
1888 generate_all_tilson_tests!(
1889 check_tilson_partial_params,
1890 check_tilson_accuracy,
1891 check_tilson_default_candles,
1892 check_tilson_zero_period,
1893 check_tilson_empty_input,
1894 check_tilson_period_exceeds_length,
1895 check_tilson_very_small_dataset,
1896 check_tilson_reinput,
1897 check_tilson_nan_handling,
1898 check_tilson_streaming,
1899 check_tilson_no_poison,
1900 check_tilson_property
1901 );
1902
1903 #[test]
1904 fn test_volume_factor_validation() {
1905 let data = vec![
1906 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
1907 ];
1908
1909 let params1 = TilsonParams {
1910 period: Some(3),
1911 volume_factor: Some(1.5),
1912 };
1913 let input1 = TilsonInput::from_slice(&data, params1);
1914 assert!(
1915 tilson(&input1).is_ok(),
1916 "volume_factor=1.5 should be accepted"
1917 );
1918
1919 let params2 = TilsonParams {
1920 period: Some(3),
1921 volume_factor: Some(-0.5),
1922 };
1923 let input2 = TilsonInput::from_slice(&data, params2);
1924 assert!(
1925 tilson(&input2).is_ok(),
1926 "volume_factor=-0.5 should be accepted"
1927 );
1928
1929 let params3 = TilsonParams {
1930 period: Some(3),
1931 volume_factor: Some(f64::NAN),
1932 };
1933 let input3 = TilsonInput::from_slice(&data, params3);
1934 assert!(
1935 tilson(&input3).is_err(),
1936 "volume_factor=NaN should be rejected"
1937 );
1938
1939 let params4 = TilsonParams {
1940 period: Some(3),
1941 volume_factor: Some(f64::INFINITY),
1942 };
1943 let input4 = TilsonInput::from_slice(&data, params4);
1944 assert!(
1945 tilson(&input4).is_err(),
1946 "volume_factor=INFINITY should be rejected"
1947 );
1948 }
1949
1950 fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1951 skip_if_unsupported!(kernel, test);
1952
1953 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1954 let c = read_candles_from_csv(file)?;
1955
1956 let output = TilsonBatchBuilder::new()
1957 .kernel(kernel)
1958 .apply_candles(&c, "close")?;
1959
1960 let def = TilsonParams::default();
1961 let row = output.values_for(&def).expect("default row missing");
1962
1963 assert_eq!(row.len(), c.close.len());
1964
1965 let expected = [
1966 59304.716332473254,
1967 59283.56868015526,
1968 59261.16173577631,
1969 59240.25895948583,
1970 59203.544843167765,
1971 ];
1972 let start = row.len() - 5;
1973 for (i, &v) in row[start..].iter().enumerate() {
1974 assert!(
1975 (v - expected[i]).abs() < 1e-8,
1976 "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1977 );
1978 }
1979 Ok(())
1980 }
1981
1982 macro_rules! gen_batch_tests {
1983 ($fn_name:ident) => {
1984 paste::paste! {
1985 #[test] fn [<$fn_name _scalar>]() {
1986 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1987 }
1988 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1989 #[test] fn [<$fn_name _avx2>]() {
1990 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1991 }
1992 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1993 #[test] fn [<$fn_name _avx512>]() {
1994 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1995 }
1996 #[test] fn [<$fn_name _auto_detect>]() {
1997 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1998 }
1999 }
2000 };
2001 }
2002
2003 #[cfg(debug_assertions)]
2004 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2005 skip_if_unsupported!(kernel, test);
2006
2007 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2008 let c = read_candles_from_csv(file)?;
2009
2010 let test_configs = vec![
2011 (3, 10, 2, 0.0, 0.5, 0.25),
2012 (5, 20, 5, 0.0, 1.0, 0.2),
2013 (10, 50, 10, 0.3, 0.7, 0.2),
2014 (20, 40, 10, 0.0, 1.0, 0.5),
2015 (5, 5, 1, 0.0, 1.0, 0.1),
2016 (15, 15, 1, 0.5, 0.5, 0.1),
2017 ];
2018
2019 for (p_start, p_end, p_step, v_start, v_end, v_step) in test_configs {
2020 let output = TilsonBatchBuilder::new()
2021 .kernel(kernel)
2022 .period_range(p_start, p_end, p_step)
2023 .volume_factor_range(v_start, v_end, v_step)
2024 .apply_candles(&c, "close")?;
2025
2026 for (idx, &val) in output.values.iter().enumerate() {
2027 if val.is_nan() {
2028 continue;
2029 }
2030
2031 let bits = val.to_bits();
2032 let row = idx / output.cols;
2033 let col = idx % output.cols;
2034 let params = output.combos.get(row);
2035 let period = params.map(|p| p.period.unwrap_or(0)).unwrap_or(0);
2036 let v_factor = params
2037 .map(|p| p.volume_factor.unwrap_or(0.0))
2038 .unwrap_or(0.0);
2039
2040 if bits == 0x11111111_11111111 {
2041 panic!(
2042 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (period {}, v_factor {}, flat index {})",
2043 test, val, bits, row, col, period, v_factor, idx
2044 );
2045 }
2046
2047 if bits == 0x22222222_22222222 {
2048 panic!(
2049 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (period {}, v_factor {}, flat index {})",
2050 test, val, bits, row, col, period, v_factor, idx
2051 );
2052 }
2053
2054 if bits == 0x33333333_33333333 {
2055 panic!(
2056 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (period {}, v_factor {}, flat index {})",
2057 test, val, bits, row, col, period, v_factor, idx
2058 );
2059 }
2060 }
2061 }
2062
2063 Ok(())
2064 }
2065
2066 #[cfg(not(debug_assertions))]
2067 fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2068 Ok(())
2069 }
2070
2071 gen_batch_tests!(check_batch_default_row);
2072 gen_batch_tests!(check_batch_no_poison);
2073
2074 #[test]
2075 fn test_tilson_into_matches_api() -> Result<(), Box<dyn Error>> {
2076 let mut data = Vec::with_capacity(256);
2077 data.extend_from_slice(&[f64::NAN, f64::NAN, f64::NAN, f64::NAN]);
2078 for i in 0..252u32 {
2079 data.push((i as f64).sin() * 100.0 + (i as f64) * 0.1);
2080 }
2081
2082 let input = TilsonInput::from_slice(&data, TilsonParams::default());
2083
2084 let baseline = tilson(&input)?.values;
2085
2086 let mut out = vec![0.0; data.len()];
2087
2088 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2089 {
2090 tilson_into(&input, &mut out)?;
2091
2092 assert_eq!(baseline.len(), out.len());
2093 for (a, b) in baseline.iter().zip(out.iter()) {
2094 let equal = (a.is_nan() && b.is_nan()) || (a == b);
2095 assert!(equal, "Mismatch: baseline={} into={}", a, b);
2096 }
2097 }
2098
2099 Ok(())
2100 }
2101}
2102
2103#[inline]
2104fn tilson_prepare<'a>(
2105 input: &'a TilsonInput,
2106 kernel: Kernel,
2107) -> Result<(&'a [f64], usize, f64, usize, usize, Kernel), TilsonError> {
2108 let data: &[f64] = input.as_ref();
2109
2110 if data.is_empty() {
2111 return Err(TilsonError::EmptyInputData);
2112 }
2113
2114 let first = data
2115 .iter()
2116 .position(|x| !x.is_nan())
2117 .ok_or(TilsonError::AllValuesNaN)?;
2118 let len = data.len();
2119 let period = input.get_period();
2120 let v_factor = input.get_volume_factor();
2121
2122 if period == 0 || period > len {
2123 return Err(TilsonError::InvalidPeriod {
2124 period,
2125 data_len: len,
2126 });
2127 }
2128 if (len - first) < period {
2129 return Err(TilsonError::NotEnoughValidData {
2130 needed: period,
2131 valid: len - first,
2132 });
2133 }
2134 if v_factor.is_nan() || v_factor.is_infinite() {
2135 return Err(TilsonError::InvalidVolumeFactor { v_factor });
2136 }
2137
2138 let lookback_total = 6 * (period - 1);
2139 if (len - first) < lookback_total + 1 {
2140 return Err(TilsonError::NotEnoughValidData {
2141 needed: lookback_total + 1,
2142 valid: len - first,
2143 });
2144 }
2145
2146 let chosen = match kernel {
2147 Kernel::Auto => Kernel::Scalar,
2148 other => other,
2149 };
2150
2151 Ok((data, period, v_factor, first, len, chosen))
2152}
2153
2154#[inline]
2155fn tilson_compute_into(
2156 data: &[f64],
2157 period: usize,
2158 v_factor: f64,
2159 first: usize,
2160 chosen: Kernel,
2161 out: &mut [f64],
2162) -> Result<(), TilsonError> {
2163 unsafe {
2164 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
2165 {
2166 if matches!(chosen, Kernel::Scalar | Kernel::ScalarBatch) {
2167 tilson_simd128(data, period, v_factor, first, out)?;
2168 return Ok(());
2169 }
2170 }
2171
2172 match chosen {
2173 Kernel::Scalar | Kernel::ScalarBatch => {
2174 tilson_scalar(data, period, v_factor, first, out)?
2175 }
2176
2177 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2178 Kernel::Avx2 | Kernel::Avx2Batch => tilson_scalar(data, period, v_factor, first, out)?,
2179 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2180 Kernel::Avx512 | Kernel::Avx512Batch => {
2181 tilson_scalar(data, period, v_factor, first, out)?
2182 }
2183 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
2184 Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
2185 tilson_scalar(data, period, v_factor, first, out)?
2186 }
2187 Kernel::Auto => unreachable!(),
2188 }
2189 }
2190 Ok(())
2191}
2192
2193#[inline(always)]
2194fn tilson_batch_inner_into(
2195 data: &[f64],
2196 sweep: &TilsonBatchRange,
2197 kern: Kernel,
2198 parallel: bool,
2199 out: &mut [f64],
2200) -> Result<Vec<TilsonParams>, TilsonError> {
2201 let combos = expand_grid(sweep)?;
2202
2203 if data.is_empty() {
2204 return Err(TilsonError::EmptyInputData);
2205 }
2206
2207 let first = data
2208 .iter()
2209 .position(|x| !x.is_nan())
2210 .ok_or(TilsonError::AllValuesNaN)?;
2211 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
2212
2213 if data.len() - first < 6 * (max_p - 1) + 1 {
2214 return Err(TilsonError::NotEnoughValidData {
2215 needed: 6 * (max_p - 1) + 1,
2216 valid: data.len() - first,
2217 });
2218 }
2219
2220 let rows = combos.len();
2221 let cols = data.len();
2222
2223 let expected = rows
2224 .checked_mul(cols)
2225 .ok_or(TilsonError::InvalidInput("rows*cols overflow"))?;
2226 if out.len() != expected {
2227 return Err(TilsonError::OutputLengthMismatch {
2228 expected,
2229 got: out.len(),
2230 });
2231 }
2232
2233 let warm: Vec<usize> = combos
2234 .iter()
2235 .map(|c| (6 * (c.period.unwrap() - 1) + first).min(cols))
2236 .collect();
2237
2238 let out_uninit = unsafe {
2239 std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
2240 };
2241
2242 unsafe { init_matrix_prefixes(out_uninit, cols, &warm) };
2243
2244 let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
2245 let period = combos[row].period.unwrap();
2246 let v_factor = combos[row].volume_factor.unwrap();
2247
2248 let out_row =
2249 core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
2250
2251 match kern {
2252 Kernel::Scalar | Kernel::ScalarBatch => {
2253 tilson_row_scalar(data, first, period, v_factor, out_row)
2254 }
2255 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2256 Kernel::Avx2 | Kernel::Avx2Batch => {
2257 tilson_row_avx2(data, first, period, v_factor, out_row)
2258 }
2259 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2260 Kernel::Avx512 | Kernel::Avx512Batch => {
2261 tilson_row_avx512(data, first, period, v_factor, out_row)
2262 }
2263 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
2264 Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch => {
2265 tilson_row_scalar(data, first, period, v_factor, out_row)
2266 }
2267 _ => unreachable!(),
2268 }
2269 };
2270
2271 if parallel {
2272 #[cfg(not(target_arch = "wasm32"))]
2273 {
2274 out_uninit
2275 .par_chunks_mut(cols)
2276 .enumerate()
2277 .for_each(|(row, slice)| do_row(row, slice));
2278 }
2279 #[cfg(target_arch = "wasm32")]
2280 {
2281 for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
2282 do_row(row, slice);
2283 }
2284 }
2285 } else {
2286 for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
2287 do_row(row, slice);
2288 }
2289 }
2290
2291 Ok(combos)
2292}
2293
2294#[cfg(feature = "python")]
2295#[pyfunction(name = "tilson")]
2296#[pyo3(signature = (data, period, volume_factor=None, kernel=None))]
2297
2298pub fn tilson_py<'py>(
2299 py: Python<'py>,
2300 data: numpy::PyReadonlyArray1<'py, f64>,
2301 period: usize,
2302 volume_factor: Option<f64>,
2303 kernel: Option<&str>,
2304) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
2305 use numpy::{IntoPyArray, PyArrayMethods};
2306
2307 let slice_in = data.as_slice()?;
2308 let kern = validate_kernel(kernel, false)?;
2309
2310 let params = TilsonParams {
2311 period: Some(period),
2312 volume_factor: volume_factor.or(Some(0.0)),
2313 };
2314 let tilson_in = TilsonInput::from_slice(slice_in, params);
2315
2316 let result_vec: Vec<f64> = py
2317 .allow_threads(|| tilson_with_kernel(&tilson_in, kern).map(|o| o.values))
2318 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2319
2320 Ok(result_vec.into_pyarray(py))
2321}
2322
2323#[cfg(feature = "python")]
2324#[pyclass(name = "TilsonStream")]
2325pub struct TilsonStreamPy {
2326 stream: TilsonStream,
2327}
2328
2329#[cfg(feature = "python")]
2330#[pymethods]
2331impl TilsonStreamPy {
2332 #[new]
2333 fn new(period: usize, volume_factor: Option<f64>) -> PyResult<Self> {
2334 let params = TilsonParams {
2335 period: Some(period),
2336 volume_factor: volume_factor.or(Some(0.0)),
2337 };
2338 let stream =
2339 TilsonStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2340 Ok(TilsonStreamPy { stream })
2341 }
2342
2343 fn update(&mut self, value: f64) -> Option<f64> {
2344 self.stream.update(value)
2345 }
2346}
2347
2348#[cfg(feature = "python")]
2349#[pyfunction(name = "tilson_batch")]
2350#[pyo3(signature = (data, period_range, volume_factor_range=None, kernel=None))]
2351
2352pub fn tilson_batch_py<'py>(
2353 py: Python<'py>,
2354 data: numpy::PyReadonlyArray1<'py, f64>,
2355 period_range: (usize, usize, usize),
2356 volume_factor_range: Option<(f64, f64, f64)>,
2357 kernel: Option<&str>,
2358) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
2359 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2360 use pyo3::types::PyDict;
2361
2362 let slice_in = data.as_slice()?;
2363 let kern = validate_kernel(kernel, true)?;
2364
2365 let sweep = TilsonBatchRange {
2366 period: period_range,
2367 volume_factor: volume_factor_range.unwrap_or((0.0, 0.0, 0.0)),
2368 };
2369
2370 let combos_dim = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2371 let rows = combos_dim.len();
2372 let cols = slice_in.len();
2373 let total = rows
2374 .checked_mul(cols)
2375 .ok_or_else(|| PyValueError::new_err("dimensions too large to allocate"))?;
2376
2377 let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2378 let slice_out = unsafe { out_arr.as_slice_mut()? };
2379
2380 let combos = py
2381 .allow_threads(|| {
2382 let kernel = match kern {
2383 Kernel::Auto => detect_best_batch_kernel(),
2384 k => k,
2385 };
2386
2387 let simd = match kernel {
2388 Kernel::Avx512Batch => Kernel::Avx512,
2389 Kernel::Avx2Batch => Kernel::Avx2,
2390 Kernel::ScalarBatch => Kernel::Scalar,
2391 _ => unreachable!(),
2392 };
2393
2394 tilson_batch_inner_into(slice_in, &sweep, simd, true, slice_out)
2395 })
2396 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2397
2398 let dict = PyDict::new(py);
2399 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2400 dict.set_item(
2401 "periods",
2402 combos
2403 .iter()
2404 .map(|p| p.period.unwrap() as u64)
2405 .collect::<Vec<_>>()
2406 .into_pyarray(py),
2407 )?;
2408 dict.set_item(
2409 "volume_factors",
2410 combos
2411 .iter()
2412 .map(|p| p.volume_factor.unwrap())
2413 .collect::<Vec<_>>()
2414 .into_pyarray(py),
2415 )?;
2416 Ok(dict)
2417}
2418
2419#[cfg(all(feature = "python", feature = "cuda"))]
2420#[pyfunction(name = "tilson_cuda_batch_dev")]
2421#[pyo3(signature = (data_f32, period_range, volume_factor_range=None, device_id=0))]
2422pub fn tilson_cuda_batch_dev_py(
2423 py: Python<'_>,
2424 data_f32: numpy::PyReadonlyArray1<'_, f32>,
2425 period_range: (usize, usize, usize),
2426 volume_factor_range: Option<(f64, f64, f64)>,
2427 device_id: usize,
2428) -> PyResult<DeviceArrayF32TilsonPy> {
2429 if !cuda_available() {
2430 return Err(PyValueError::new_err("CUDA not available"));
2431 }
2432
2433 let slice_in = data_f32.as_slice()?;
2434 let sweep = TilsonBatchRange {
2435 period: period_range,
2436 volume_factor: volume_factor_range.unwrap_or((0.0, 0.0, 0.0)),
2437 };
2438
2439 let inner = py.allow_threads(|| {
2440 let cuda = CudaTilson::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2441 cuda.tilson_batch_dev(slice_in, &sweep)
2442 .map_err(|e| PyValueError::new_err(e.to_string()))
2443 })?;
2444
2445 Ok(DeviceArrayF32TilsonPy { inner })
2446}
2447
2448#[cfg(all(feature = "python", feature = "cuda"))]
2449#[pyfunction(name = "tilson_cuda_many_series_one_param_dev")]
2450#[pyo3(signature = (data_tm_f32, period, volume_factor, device_id=0))]
2451pub fn tilson_cuda_many_series_one_param_dev_py(
2452 py: Python<'_>,
2453 data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
2454 period: usize,
2455 volume_factor: f64,
2456 device_id: usize,
2457) -> PyResult<DeviceArrayF32TilsonPy> {
2458 if !cuda_available() {
2459 return Err(PyValueError::new_err("CUDA not available"));
2460 }
2461
2462 use numpy::PyUntypedArrayMethods;
2463
2464 let flat = data_tm_f32.as_slice()?;
2465 let rows = data_tm_f32.shape()[0];
2466 let cols = data_tm_f32.shape()[1];
2467 let params = TilsonParams {
2468 period: Some(period),
2469 volume_factor: Some(volume_factor),
2470 };
2471
2472 let inner = py.allow_threads(|| {
2473 let cuda = CudaTilson::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2474 cuda.tilson_many_series_one_param_time_major_dev(flat, cols, rows, ¶ms)
2475 .map_err(|e| PyValueError::new_err(e.to_string()))
2476 })?;
2477
2478 Ok(DeviceArrayF32TilsonPy { inner })
2479}
2480
2481#[cfg(all(feature = "python", feature = "cuda"))]
2482#[pyclass(module = "ta_indicators.cuda", unsendable)]
2483pub struct DeviceArrayF32TilsonPy {
2484 pub(crate) inner: DeviceArrayF32Tilson,
2485}
2486
2487#[cfg(all(feature = "python", feature = "cuda"))]
2488#[pymethods]
2489impl DeviceArrayF32TilsonPy {
2490 #[getter]
2491 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
2492 let d = PyDict::new(py);
2493 d.set_item("shape", (self.inner.rows, self.inner.cols))?;
2494 d.set_item("typestr", "<f4")?;
2495 d.set_item(
2496 "strides",
2497 (
2498 self.inner.cols * std::mem::size_of::<f32>(),
2499 std::mem::size_of::<f32>(),
2500 ),
2501 )?;
2502 d.set_item("data", (self.inner.device_ptr() as usize, false))?;
2503
2504 d.set_item("version", 3)?;
2505 Ok(d)
2506 }
2507
2508 fn __dlpack_device__(&self) -> (i32, i32) {
2509 (2, self.inner.device_id as i32)
2510 }
2511
2512 #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
2513 fn __dlpack__<'py>(
2514 &mut self,
2515 py: Python<'py>,
2516 stream: Option<pyo3::PyObject>,
2517 max_version: Option<pyo3::PyObject>,
2518 dl_device: Option<pyo3::PyObject>,
2519 copy: Option<pyo3::PyObject>,
2520 ) -> PyResult<PyObject> {
2521 use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
2522
2523 let (kdl, alloc_dev) = self.__dlpack_device__();
2524 if let Some(dev_obj) = dl_device.as_ref() {
2525 if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2526 if dev_ty != kdl || dev_id != alloc_dev {
2527 let wants_copy = copy
2528 .as_ref()
2529 .and_then(|c| c.extract::<bool>(py).ok())
2530 .unwrap_or(false);
2531 if wants_copy {
2532 return Err(PyValueError::new_err(
2533 "device copy not implemented for __dlpack__",
2534 ));
2535 } else {
2536 return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2537 }
2538 }
2539 }
2540 }
2541 let _ = stream;
2542
2543 let dummy =
2544 DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
2545 let ctx = self.inner.ctx.clone();
2546 let device_id = self.inner.device_id;
2547 let inner = std::mem::replace(
2548 &mut self.inner,
2549 DeviceArrayF32Tilson {
2550 buf: dummy,
2551 rows: 0,
2552 cols: 0,
2553 ctx,
2554 device_id,
2555 },
2556 );
2557
2558 let rows = inner.rows;
2559 let cols = inner.cols;
2560 let buf = inner.buf;
2561
2562 let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2563
2564 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2565 }
2566}
2567
2568#[cfg(feature = "python")]
2569#[pyfunction(name = "tilson_into")]
2570#[pyo3(signature = (data, period, volume_factor=None, kernel=None))]
2571pub fn tilson_into_py<'py>(
2572 py: Python<'py>,
2573 data: numpy::PyReadonlyArray1<'py, f64>,
2574 period: usize,
2575 volume_factor: Option<f64>,
2576 kernel: Option<&str>,
2577) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
2578 use numpy::{PyArray1, PyArrayMethods};
2579 let slice_in = data.as_slice()?;
2580 let out = unsafe { PyArray1::<f64>::new(py, [slice_in.len()], false) };
2581 let slice_out = unsafe { out.as_slice_mut()? };
2582
2583 let kern = validate_kernel(kernel, false)?;
2584 let params = TilsonParams {
2585 period: Some(period),
2586 volume_factor: Some(volume_factor.unwrap_or(0.0)),
2587 };
2588 let input = TilsonInput::from_slice(slice_in, params);
2589
2590 py.allow_threads(|| tilson_into_slice(slice_out, &input, kern))
2591 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2592 Ok(out)
2593}
2594
2595#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2596#[derive(Serialize, Deserialize)]
2597pub struct TilsonBatchConfig {
2598 pub period_range: (usize, usize, usize),
2599 pub volume_factor_range: (f64, f64, f64),
2600}
2601
2602#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2603#[derive(Serialize, Deserialize)]
2604pub struct TilsonBatchJsOutput {
2605 pub values: Vec<f64>,
2606 pub combos: Vec<TilsonParams>,
2607 pub rows: usize,
2608 pub cols: usize,
2609}
2610
2611#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2612#[wasm_bindgen(js_name = tilson_js)]
2613
2614pub fn tilson_js(data: &[f64], period: usize, volume_factor: f64) -> Result<Vec<f64>, JsValue> {
2615 let params = TilsonParams {
2616 period: Some(period),
2617 volume_factor: Some(volume_factor),
2618 };
2619 let input = TilsonInput::from_slice(data, params);
2620 let mut out = vec![0.0; data.len()];
2621 tilson_into_slice(&mut out, &input, Kernel::Auto)
2622 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2623 Ok(out)
2624}
2625
2626#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2627#[wasm_bindgen(js_name = tilson_batch_js)]
2628
2629pub fn tilson_batch_js(
2630 data: &[f64],
2631 period_start: usize,
2632 period_end: usize,
2633 period_step: usize,
2634 v_factor_start: f64,
2635 v_factor_end: f64,
2636 v_factor_step: f64,
2637) -> Result<Vec<f64>, JsValue> {
2638 let sweep = TilsonBatchRange {
2639 period: (period_start, period_end, period_step),
2640 volume_factor: (v_factor_start, v_factor_end, v_factor_step),
2641 };
2642
2643 let output = tilson_batch_with_kernel(data, &sweep, Kernel::ScalarBatch)
2644 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2645
2646 Ok(output.values)
2647}
2648
2649#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2650#[wasm_bindgen(js_name = tilson_batch_metadata_js)]
2651
2652pub fn tilson_batch_metadata_js(
2653 period_start: usize,
2654 period_end: usize,
2655 period_step: usize,
2656 v_factor_start: f64,
2657 v_factor_end: f64,
2658 v_factor_step: f64,
2659) -> Vec<f64> {
2660 let sweep = TilsonBatchRange {
2661 period: (period_start, period_end, period_step),
2662 volume_factor: (v_factor_start, v_factor_end, v_factor_step),
2663 };
2664
2665 let combos = match expand_grid(&sweep) {
2666 Ok(v) => v,
2667 Err(_) => return Vec::new(),
2668 };
2669 let mut result = Vec::with_capacity(combos.len() * 2);
2670
2671 for combo in &combos {
2672 result.push(combo.period.unwrap() as f64);
2673 }
2674
2675 for combo in &combos {
2676 result.push(combo.volume_factor.unwrap());
2677 }
2678
2679 result
2680}
2681
2682#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2683#[wasm_bindgen(js_name = tilson_batch)]
2684pub fn tilson_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2685 let config: TilsonBatchConfig = serde_wasm_bindgen::from_value(config)
2686 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2687
2688 let sweep = TilsonBatchRange {
2689 period: config.period_range,
2690 volume_factor: config.volume_factor_range,
2691 };
2692
2693 let output = tilson_batch_with_kernel(data, &sweep, Kernel::ScalarBatch)
2694 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2695
2696 let js_output = TilsonBatchJsOutput {
2697 values: output.values,
2698 combos: output.combos,
2699 rows: output.rows,
2700 cols: output.cols,
2701 };
2702
2703 serde_wasm_bindgen::to_value(&js_output)
2704 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2705}
2706
2707#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2708#[wasm_bindgen]
2709pub fn tilson_alloc(len: usize) -> *mut f64 {
2710 let mut vec = Vec::<f64>::with_capacity(len);
2711 let ptr = vec.as_mut_ptr();
2712 std::mem::forget(vec);
2713 ptr
2714}
2715
2716#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2717#[wasm_bindgen]
2718pub fn tilson_free(ptr: *mut f64, len: usize) {
2719 unsafe {
2720 let _ = Vec::from_raw_parts(ptr, len, len);
2721 }
2722}
2723
2724#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2725#[wasm_bindgen]
2726pub fn tilson_into(
2727 in_ptr: *const f64,
2728 out_ptr: *mut f64,
2729 len: usize,
2730 period: usize,
2731 volume_factor: f64,
2732) -> Result<(), JsValue> {
2733 if in_ptr.is_null() || out_ptr.is_null() {
2734 return Err(JsValue::from_str("null pointer passed to tilson_into"));
2735 }
2736
2737 unsafe {
2738 let data = std::slice::from_raw_parts(in_ptr, len);
2739
2740 if period == 0 || period > len {
2741 return Err(JsValue::from_str("Invalid period"));
2742 }
2743
2744 let params = TilsonParams {
2745 period: Some(period),
2746 volume_factor: Some(volume_factor),
2747 };
2748 let input = TilsonInput::from_slice(data, params);
2749
2750 let first = data
2751 .iter()
2752 .position(|&x| !x.is_nan())
2753 .ok_or_else(|| JsValue::from_str("All values are NaN"))?;
2754
2755 let warmup = first + 6 * (period - 1);
2756
2757 if in_ptr == out_ptr {
2758 let mut temp = vec![f64::NAN; len];
2759 tilson_compute_into(
2760 data,
2761 period,
2762 volume_factor,
2763 first,
2764 Kernel::Scalar,
2765 &mut temp,
2766 )
2767 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2768 std::ptr::copy_nonoverlapping(temp.as_ptr(), out_ptr, len);
2769 } else {
2770 let out = std::slice::from_raw_parts_mut(out_ptr, len);
2771
2772 for i in 0..warmup.min(len) {
2773 out[i] = f64::NAN;
2774 }
2775 tilson_compute_into(data, period, volume_factor, first, Kernel::Scalar, out)
2776 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2777 }
2778
2779 Ok(())
2780 }
2781}
2782
2783#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2784#[wasm_bindgen]
2785pub fn tilson_batch_into(
2786 in_ptr: *const f64,
2787 out_ptr: *mut f64,
2788 len: usize,
2789 period_start: usize,
2790 period_end: usize,
2791 period_step: usize,
2792 v_factor_start: f64,
2793 v_factor_end: f64,
2794 v_factor_step: f64,
2795) -> Result<usize, JsValue> {
2796 if in_ptr.is_null() || out_ptr.is_null() {
2797 return Err(JsValue::from_str(
2798 "null pointer passed to tilson_batch_into",
2799 ));
2800 }
2801
2802 unsafe {
2803 let data = std::slice::from_raw_parts(in_ptr, len);
2804
2805 let sweep = TilsonBatchRange {
2806 period: (period_start, period_end, period_step),
2807 volume_factor: (v_factor_start, v_factor_end, v_factor_step),
2808 };
2809
2810 let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2811 let rows = combos.len();
2812 let cols = len;
2813 let total = rows * cols;
2814
2815 if total == 0 {
2816 return Err(JsValue::from_str("Invalid batch configuration"));
2817 }
2818
2819 let out = std::slice::from_raw_parts_mut(out_ptr, total);
2820
2821 tilson_batch_inner_into(data, &sweep, Kernel::Scalar, false, out)
2822 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2823
2824 Ok(rows)
2825 }
2826}
2827
2828#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2829#[wasm_bindgen]
2830#[deprecated(
2831 since = "1.0.0",
2832 note = "For weight reuse patterns, use the fast/unsafe API with persistent buffers"
2833)]
2834pub struct TilsonContext {
2835 period: usize,
2836 c1: f64,
2837 c2: f64,
2838 c3: f64,
2839 c4: f64,
2840 kernel: Kernel,
2841
2842 ema1: f64,
2843 ema2: f64,
2844 ema3: f64,
2845 ema4: f64,
2846 ema5: f64,
2847 ema6: f64,
2848 initialized: bool,
2849 warmup_count: usize,
2850}
2851
2852#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2853#[wasm_bindgen]
2854#[allow(deprecated)]
2855impl TilsonContext {
2856 #[wasm_bindgen(constructor)]
2857 #[deprecated(
2858 since = "1.0.0",
2859 note = "For weight reuse patterns, use the fast/unsafe API with persistent buffers"
2860 )]
2861 pub fn new(period: usize, volume_factor: f64) -> Result<TilsonContext, JsValue> {
2862 if period == 0 {
2863 return Err(JsValue::from_str("Invalid period: 0"));
2864 }
2865 if volume_factor.is_nan() || volume_factor.is_infinite() {
2866 return Err(JsValue::from_str(&format!(
2867 "Invalid volume factor: {}",
2868 volume_factor
2869 )));
2870 }
2871
2872 let c1 = -volume_factor.powi(3);
2873 let c2 = 3.0 * volume_factor.powi(2) + 3.0 * volume_factor.powi(3);
2874 let c3 = -6.0 * volume_factor.powi(2) - 3.0 * volume_factor - 3.0 * volume_factor.powi(3);
2875 let c4 = 1.0 + 3.0 * volume_factor + volume_factor.powi(3) + 3.0 * volume_factor.powi(2);
2876
2877 Ok(TilsonContext {
2878 period,
2879 c1,
2880 c2,
2881 c3,
2882 c4,
2883 kernel: Kernel::Scalar,
2884 ema1: 0.0,
2885 ema2: 0.0,
2886 ema3: 0.0,
2887 ema4: 0.0,
2888 ema5: 0.0,
2889 ema6: 0.0,
2890 initialized: false,
2891 warmup_count: 0,
2892 })
2893 }
2894
2895 #[wasm_bindgen]
2896 pub fn update(&mut self, value: f64) -> Option<f64> {
2897 if value.is_nan() {
2898 return None;
2899 }
2900
2901 let alpha = 2.0 / (self.period as f64 + 1.0);
2902
2903 if !self.initialized {
2904 self.ema1 = value;
2905 self.ema2 = value;
2906 self.ema3 = value;
2907 self.ema4 = value;
2908 self.ema5 = value;
2909 self.ema6 = value;
2910 self.initialized = true;
2911 } else {
2912 self.ema1 = alpha * value + (1.0 - alpha) * self.ema1;
2913 self.ema2 = alpha * self.ema1 + (1.0 - alpha) * self.ema2;
2914 self.ema3 = alpha * self.ema2 + (1.0 - alpha) * self.ema3;
2915 self.ema4 = alpha * self.ema3 + (1.0 - alpha) * self.ema4;
2916 self.ema5 = alpha * self.ema4 + (1.0 - alpha) * self.ema5;
2917 self.ema6 = alpha * self.ema5 + (1.0 - alpha) * self.ema6;
2918 }
2919
2920 self.warmup_count += 1;
2921
2922 if self.warmup_count <= 6 * (self.period - 1) {
2923 None
2924 } else {
2925 Some(
2926 self.c1 * self.ema6
2927 + self.c2 * self.ema5
2928 + self.c3 * self.ema4
2929 + self.c4 * self.ema3,
2930 )
2931 }
2932 }
2933
2934 #[wasm_bindgen]
2935 pub fn reset(&mut self) {
2936 self.ema1 = 0.0;
2937 self.ema2 = 0.0;
2938 self.ema3 = 0.0;
2939 self.ema4 = 0.0;
2940 self.ema5 = 0.0;
2941 self.ema6 = 0.0;
2942 self.initialized = false;
2943 self.warmup_count = 0;
2944 }
2945
2946 #[wasm_bindgen]
2947 pub fn get_warmup_period(&self) -> usize {
2948 6 * (self.period - 1)
2949 }
2950}