Skip to main content

vector_ta/indicators/moving_averages/
tilson.rs

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, &params)
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}