Skip to main content

vector_ta/indicators/
otto.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
3#[cfg(feature = "python")]
4use numpy::{IntoPyArray, PyArray1};
5#[cfg(feature = "python")]
6use pyo3::exceptions::PyValueError;
7#[cfg(feature = "python")]
8use pyo3::prelude::*;
9#[cfg(feature = "python")]
10use pyo3::types::{PyDict, PyList};
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel,
21};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24
25use crate::indicators::cmo::{cmo, CmoData, CmoInput, CmoParams};
26use crate::indicators::moving_averages::dema::{dema, DemaData, DemaInput, DemaParams};
27use crate::indicators::moving_averages::ema::{ema, EmaData, EmaInput, EmaParams};
28use crate::indicators::moving_averages::hma::{hma, HmaData, HmaInput, HmaParams};
29use crate::indicators::moving_averages::linreg::{linreg, LinRegData, LinRegInput, LinRegParams};
30use crate::indicators::moving_averages::sma::{sma, SmaData, SmaInput, SmaParams};
31use crate::indicators::moving_averages::trima::{trima, TrimaData, TrimaInput, TrimaParams};
32use crate::indicators::moving_averages::wma::{wma, WmaData, WmaInput, WmaParams};
33use crate::indicators::moving_averages::zlema::{zlema, ZlemaData, ZlemaInput, ZlemaParams};
34use crate::indicators::tsf::{tsf, TsfData, TsfInput, TsfParams};
35
36#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
37use core::arch::x86_64::*;
38#[cfg(not(target_arch = "wasm32"))]
39use rayon::prelude::*;
40use std::alloc::{alloc, dealloc, Layout};
41use std::convert::AsRef;
42use std::error::Error;
43use std::mem::MaybeUninit;
44use thiserror::Error;
45
46impl<'a> AsRef<[f64]> for OttoInput<'a> {
47    #[inline(always)]
48    fn as_ref(&self) -> &[f64] {
49        match &self.data {
50            OttoData::Slice(slice) => slice,
51            OttoData::Candles { candles, source } => source_type(candles, source),
52        }
53    }
54}
55
56#[derive(Debug, Clone)]
57pub enum OttoData<'a> {
58    Candles {
59        candles: &'a Candles,
60        source: &'a str,
61    },
62    Slice(&'a [f64]),
63}
64
65#[derive(Debug, Clone)]
66pub struct OttoOutput {
67    pub hott: Vec<f64>,
68    pub lott: Vec<f64>,
69}
70
71#[derive(Debug, Clone, PartialEq)]
72#[cfg_attr(
73    all(target_arch = "wasm32", feature = "wasm"),
74    derive(Serialize, Deserialize)
75)]
76pub struct OttoParams {
77    pub ott_period: Option<usize>,
78    pub ott_percent: Option<f64>,
79    pub fast_vidya_length: Option<usize>,
80    pub slow_vidya_length: Option<usize>,
81    pub correcting_constant: Option<f64>,
82    pub ma_type: Option<String>,
83}
84
85impl Default for OttoParams {
86    fn default() -> Self {
87        Self {
88            ott_period: Some(2),
89            ott_percent: Some(0.6),
90            fast_vidya_length: Some(10),
91            slow_vidya_length: Some(25),
92            correcting_constant: Some(100000.0),
93            ma_type: Some("VAR".to_string()),
94        }
95    }
96}
97
98#[derive(Debug, Clone)]
99pub struct OttoInput<'a> {
100    pub data: OttoData<'a>,
101    pub params: OttoParams,
102}
103
104impl<'a> OttoInput<'a> {
105    #[inline]
106    pub fn from_candles(c: &'a Candles, s: &'a str, p: OttoParams) -> Self {
107        Self {
108            data: OttoData::Candles {
109                candles: c,
110                source: s,
111            },
112            params: p,
113        }
114    }
115
116    #[inline]
117    pub fn from_slice(sl: &'a [f64], p: OttoParams) -> Self {
118        Self {
119            data: OttoData::Slice(sl),
120            params: p,
121        }
122    }
123
124    #[inline]
125    pub fn with_default_candles(c: &'a Candles) -> Self {
126        Self::from_candles(c, "close", OttoParams::default())
127    }
128
129    #[inline]
130    pub fn get_ott_period(&self) -> usize {
131        self.params.ott_period.unwrap_or(2)
132    }
133
134    #[inline]
135    pub fn get_ott_percent(&self) -> f64 {
136        self.params.ott_percent.unwrap_or(0.6)
137    }
138
139    #[inline]
140    pub fn get_fast_vidya_length(&self) -> usize {
141        self.params.fast_vidya_length.unwrap_or(10)
142    }
143
144    #[inline]
145    pub fn get_slow_vidya_length(&self) -> usize {
146        self.params.slow_vidya_length.unwrap_or(25)
147    }
148
149    #[inline]
150    pub fn get_correcting_constant(&self) -> f64 {
151        self.params.correcting_constant.unwrap_or(100000.0)
152    }
153
154    #[inline]
155    pub fn get_ma_type(&self) -> &str {
156        self.params.ma_type.as_deref().unwrap_or("VAR")
157    }
158}
159
160#[derive(Debug, Error)]
161pub enum OttoError {
162    #[error("otto: Input data slice is empty.")]
163    EmptyInputData,
164    #[error("otto: All values are NaN.")]
165    AllValuesNaN,
166    #[error("otto: Invalid period: period = {period}, data length = {data_len}")]
167    InvalidPeriod { period: usize, data_len: usize },
168    #[error("otto: Not enough valid data: needed = {needed}, valid = {valid}")]
169    NotEnoughValidData { needed: usize, valid: usize },
170    #[error("otto: Invalid moving average type: {ma_type}")]
171    InvalidMaType { ma_type: String },
172    #[error("otto: CMO calculation failed: {0}")]
173    CmoError(String),
174    #[error("otto: Moving average calculation failed: {0}")]
175    MaError(String),
176    #[error("otto: Output length mismatch: expected {expected}, got {got}")]
177    OutputLengthMismatch { expected: usize, got: usize },
178    #[error("otto: Invalid range: start={start}, end={end}, step={step}")]
179    InvalidRange {
180        start: String,
181        end: String,
182        step: String,
183    },
184    #[error("otto: Invalid kernel for batch: {0:?}")]
185    InvalidKernelForBatch(Kernel),
186    #[error("otto: Invalid input: {0}")]
187    InvalidInput(String),
188}
189
190#[derive(Copy, Clone, Debug)]
191pub struct OttoBuilder {
192    ott_period: Option<usize>,
193    ott_percent: Option<f64>,
194    fast_vidya_length: Option<usize>,
195    slow_vidya_length: Option<usize>,
196    correcting_constant: Option<f64>,
197    ma_type: Option<&'static str>,
198    kernel: Kernel,
199}
200
201impl Default for OttoBuilder {
202    fn default() -> Self {
203        Self {
204            ott_period: None,
205            ott_percent: None,
206            fast_vidya_length: None,
207            slow_vidya_length: None,
208            correcting_constant: None,
209            ma_type: None,
210            kernel: Kernel::Auto,
211        }
212    }
213}
214
215impl OttoBuilder {
216    #[inline]
217    pub fn new() -> Self {
218        Self::default()
219    }
220
221    #[inline]
222    pub fn ott_period(mut self, p: usize) -> Self {
223        self.ott_period = Some(p);
224        self
225    }
226
227    #[inline]
228    pub fn ott_percent(mut self, p: f64) -> Self {
229        self.ott_percent = Some(p);
230        self
231    }
232
233    #[inline]
234    pub fn fast_vidya_length(mut self, l: usize) -> Self {
235        self.fast_vidya_length = Some(l);
236        self
237    }
238
239    #[inline]
240    pub fn slow_vidya_length(mut self, l: usize) -> Self {
241        self.slow_vidya_length = Some(l);
242        self
243    }
244
245    #[inline]
246    pub fn correcting_constant(mut self, c: f64) -> Self {
247        self.correcting_constant = Some(c);
248        self
249    }
250
251    #[inline]
252    pub fn ma_type(mut self, m: &'static str) -> Self {
253        self.ma_type = Some(m);
254        self
255    }
256
257    #[inline]
258    pub fn kernel(mut self, k: Kernel) -> Self {
259        self.kernel = k;
260        self
261    }
262
263    #[inline]
264    pub fn apply(self, c: &Candles) -> Result<OttoOutput, OttoError> {
265        let params = OttoParams {
266            ott_period: self.ott_period,
267            ott_percent: self.ott_percent,
268            fast_vidya_length: self.fast_vidya_length,
269            slow_vidya_length: self.slow_vidya_length,
270            correcting_constant: self.correcting_constant,
271            ma_type: self.ma_type.map(|s| s.to_string()),
272        };
273        let input = OttoInput::from_candles(c, "close", params);
274        otto_with_kernel(&input, self.kernel)
275    }
276
277    #[inline]
278    pub fn apply_slice(self, data: &[f64]) -> Result<OttoOutput, OttoError> {
279        let params = OttoParams {
280            ott_period: self.ott_period,
281            ott_percent: self.ott_percent,
282            fast_vidya_length: self.fast_vidya_length,
283            slow_vidya_length: self.slow_vidya_length,
284            correcting_constant: self.correcting_constant,
285            ma_type: self.ma_type.map(|s| s.to_string()),
286        };
287        let input = OttoInput::from_slice(data, params);
288        otto_with_kernel(&input, self.kernel)
289    }
290
291    #[inline]
292    pub fn into_stream(self) -> Result<OttoStream, OttoError> {
293        let params = OttoParams {
294            ott_period: self.ott_period,
295            ott_percent: self.ott_percent,
296            fast_vidya_length: self.fast_vidya_length,
297            slow_vidya_length: self.slow_vidya_length,
298            correcting_constant: self.correcting_constant,
299            ma_type: self.ma_type.map(|s| s.to_string()),
300        };
301        OttoStream::try_new(params)
302    }
303}
304
305#[derive(Debug, Clone)]
306pub struct OttoStream {
307    ott_period: usize,
308    ott_percent: f64,
309    fast_vidya_length: usize,
310    slow_vidya_length: usize,
311    correcting_constant: f64,
312    ma_type: String,
313
314    required_len: usize,
315    idx: usize,
316
317    a1_base: f64,
318    a2_base: f64,
319    a3_base: f64,
320
321    a_ott_base: f64,
322
323    fark: f64,
324    scale_up: f64,
325    scale_dn: f64,
326
327    ring_up_in: [f64; 9],
328    ring_dn_in: [f64; 9],
329    sum_up_in: f64,
330    sum_dn_in: f64,
331    head_in: usize,
332    prev_x_in: f64,
333    have_prev_in: bool,
334
335    v1: f64,
336    v2: f64,
337    v3: f64,
338
339    last_lott: f64,
340
341    ring_up_lott: [f64; 9],
342    ring_dn_lott: [f64; 9],
343    sum_up_lott: f64,
344    sum_dn_lott: f64,
345    head_lott: usize,
346    prev_lott: f64,
347    have_prev_lott: bool,
348    ma_prev: f64,
349
350    ema_alpha: f64,
351    ema_init: bool,
352
353    dema_alpha: f64,
354    dema_ema1: f64,
355    dema_ema2: f64,
356    dema_init: bool,
357
358    sma_sum: f64,
359    sma_buf: Vec<f64>,
360    sma_head: usize,
361    sma_count: usize,
362
363    wma_buf: Vec<f64>,
364    wma_head: usize,
365    wma_count: usize,
366    wma_sumx: f64,
367    wma_sumwx: f64,
368    wma_denom: f64,
369
370    tma_p1: usize,
371    tma_p2: usize,
372    tma_ring1: Vec<f64>,
373    tma_head1: usize,
374    tma_sum1: f64,
375    tma_count1: usize,
376    tma_ring2: Vec<f64>,
377    tma_head2: usize,
378    tma_sum2: f64,
379    tma_count2: usize,
380
381    zlema_alpha: f64,
382    zlema_prev: f64,
383    zlema_init: bool,
384    zlema_lag: usize,
385    zlema_ring: Vec<f64>,
386    zlema_head: usize,
387    zlema_count: usize,
388
389    long_stop_prev: f64,
390    short_stop_prev: f64,
391    dir_prev: i32,
392    ott_init: bool,
393}
394
395impl OttoStream {
396    pub fn try_new(params: OttoParams) -> Result<Self, OttoError> {
397        let ott_period = params.ott_period.unwrap_or(2);
398        let slow = params.slow_vidya_length.unwrap_or(25);
399        let fast = params.fast_vidya_length.unwrap_or(10);
400        let correcting_constant = params.correcting_constant.unwrap_or(100000.0);
401        let ma_type = params.ma_type.unwrap_or_else(|| "VAR".to_string());
402        let ott_percent = params.ott_percent.unwrap_or(0.6);
403
404        if ott_period == 0 {
405            return Err(OttoError::InvalidPeriod {
406                period: 0,
407                data_len: 0,
408            });
409        }
410
411        let p1 = slow / 2;
412        let p2 = slow;
413        let p3 = slow.saturating_mul(fast);
414        if p1 == 0 || p2 == 0 || p3 == 0 {
415            return Err(OttoError::InvalidPeriod {
416                period: 0,
417                data_len: 0,
418            });
419        }
420
421        let a1_base = 2.0 / (p1 as f64 + 1.0);
422        let a2_base = 2.0 / (p2 as f64 + 1.0);
423        let a3_base = 2.0 / (p3 as f64 + 1.0);
424        let a_ott_base = 2.0 / (ott_period as f64 + 1.0);
425
426        let required_len = p3 + 10;
427
428        let fark = ott_percent * 0.01;
429        let scale_up = (200.0 + ott_percent) / 200.0;
430        let scale_dn = (200.0 - ott_percent) / 200.0;
431
432        let sma_buf = vec![0.0; ott_period];
433        let wma_buf = vec![0.0; ott_period];
434        let wma_denom = (ott_period as f64) * (ott_period as f64 + 1.0) * 0.5;
435
436        let tma_p1 = (ott_period + 1) / 2;
437        let tma_p2 = ott_period / 2 + 1;
438        let tma_ring1 = vec![0.0; tma_p1.max(1)];
439        let tma_ring2 = vec![0.0; tma_p2.max(1)];
440
441        let zlema_lag = (ott_period.saturating_sub(1)) / 2;
442        let zlema_ring = vec![0.0; zlema_lag + 1];
443
444        Ok(Self {
445            ott_period,
446            ott_percent,
447            fast_vidya_length: fast,
448            slow_vidya_length: slow,
449            correcting_constant,
450            ma_type,
451
452            required_len,
453            idx: 0,
454
455            a1_base,
456            a2_base,
457            a3_base,
458            a_ott_base,
459
460            fark,
461            scale_up,
462            scale_dn,
463
464            ring_up_in: [0.0; 9],
465            ring_dn_in: [0.0; 9],
466            sum_up_in: 0.0,
467            sum_dn_in: 0.0,
468            head_in: 0,
469            prev_x_in: 0.0,
470            have_prev_in: false,
471
472            v1: 0.0,
473            v2: 0.0,
474            v3: 0.0,
475
476            last_lott: 0.0,
477
478            ring_up_lott: [0.0; 9],
479            ring_dn_lott: [0.0; 9],
480            sum_up_lott: 0.0,
481            sum_dn_lott: 0.0,
482            head_lott: 0,
483            prev_lott: 0.0,
484            have_prev_lott: false,
485            ma_prev: 0.0,
486
487            ema_alpha: 2.0 / (ott_period as f64 + 1.0),
488            ema_init: false,
489
490            dema_alpha: 2.0 / (ott_period as f64 + 1.0),
491            dema_ema1: 0.0,
492            dema_ema2: 0.0,
493            dema_init: false,
494
495            sma_sum: 0.0,
496            sma_buf,
497            sma_head: 0,
498            sma_count: 0,
499
500            wma_buf,
501            wma_head: 0,
502            wma_count: 0,
503            wma_sumx: 0.0,
504            wma_sumwx: 0.0,
505            wma_denom,
506
507            tma_p1,
508            tma_p2,
509            tma_ring1,
510            tma_head1: 0,
511            tma_sum1: 0.0,
512            tma_count1: 0,
513            tma_ring2,
514            tma_head2: 0,
515            tma_sum2: 0.0,
516            tma_count2: 0,
517
518            zlema_alpha: 2.0 / (ott_period as f64 + 1.0),
519            zlema_prev: 0.0,
520            zlema_init: false,
521            zlema_lag,
522            zlema_ring,
523            zlema_head: 0,
524            zlema_count: 0,
525
526            long_stop_prev: f64::NAN,
527            short_stop_prev: f64::NAN,
528            dir_prev: 1,
529            ott_init: false,
530        })
531    }
532
533    #[inline]
534    fn cmo_abs_from_ring(sum_up: f64, sum_dn: f64) -> f64 {
535        let denom = sum_up + sum_dn;
536        if denom != 0.0 {
537            ((sum_up - sum_dn) / denom).abs()
538        } else {
539            0.0
540        }
541    }
542
543    #[inline]
544    pub fn update(&mut self, value: f64) -> Option<(f64, f64)> {
545        let i = self.idx;
546        self.idx = self.idx.wrapping_add(1);
547
548        let x = if value.is_nan() { 0.0 } else { value };
549
550        if self.have_prev_in {
551            let mut d = value - self.prev_x_in;
552            if !value.is_finite() || !self.prev_x_in.is_finite() {
553                d = 0.0;
554            }
555            if i >= 9 {
556                self.sum_up_in -= self.ring_up_in[self.head_in];
557                self.sum_dn_in -= self.ring_dn_in[self.head_in];
558            }
559            let (up, dn) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
560            self.ring_up_in[self.head_in] = up;
561            self.ring_dn_in[self.head_in] = dn;
562            self.sum_up_in += up;
563            self.sum_dn_in += dn;
564            self.head_in += 1;
565            if self.head_in == 9 {
566                self.head_in = 0;
567            }
568        } else {
569            self.have_prev_in = true;
570        }
571        self.prev_x_in = value;
572
573        let c_abs = if i >= 9 {
574            Self::cmo_abs_from_ring(self.sum_up_in, self.sum_dn_in)
575        } else {
576            0.0
577        };
578
579        let a1 = self.a1_base * c_abs;
580        let a2 = self.a2_base * c_abs;
581        let a3 = self.a3_base * c_abs;
582
583        self.v1 = a1.mul_add(x, (1.0 - a1) * self.v1);
584        self.v2 = a2.mul_add(x, (1.0 - a2) * self.v2);
585        self.v3 = a3.mul_add(x, (1.0 - a3) * self.v3);
586
587        let denom_l = (self.v2 - self.v3) + self.correcting_constant;
588        let lott = self.v1 / denom_l;
589        self.last_lott = lott;
590
591        let ma_opt = match self.ma_type.as_str() {
592            "VAR" => {
593                if self.have_prev_lott {
594                    let mut d = lott - self.prev_lott;
595                    if !lott.is_finite() || !self.prev_lott.is_finite() {
596                        d = 0.0;
597                    }
598                    if i >= 9 {
599                        self.sum_up_lott -= self.ring_up_lott[self.head_lott];
600                        self.sum_dn_lott -= self.ring_dn_lott[self.head_lott];
601                    }
602                    let (up, dn) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
603                    self.ring_up_lott[self.head_lott] = up;
604                    self.ring_dn_lott[self.head_lott] = dn;
605                    self.sum_up_lott += up;
606                    self.sum_dn_lott += dn;
607                    self.head_lott += 1;
608                    if self.head_lott == 9 {
609                        self.head_lott = 0;
610                    }
611                } else {
612                    self.have_prev_lott = true;
613                }
614                self.prev_lott = lott;
615
616                let c2 = if i >= 9 {
617                    Self::cmo_abs_from_ring(self.sum_up_lott, self.sum_dn_lott)
618                } else {
619                    0.0
620                };
621                let a = self.a_ott_base * c2;
622                self.ma_prev = a.mul_add(lott, (1.0 - a) * self.ma_prev);
623                Some(self.ma_prev)
624            }
625
626            "EMA" => {
627                if !self.ema_init {
628                    self.ma_prev = lott;
629                    self.ema_init = true;
630                } else {
631                    let a = self.ema_alpha;
632                    self.ma_prev = a.mul_add(lott, (1.0 - a) * self.ma_prev);
633                }
634                Some(self.ma_prev)
635            }
636
637            "WWMA" => {
638                let a = 1.0 / (self.ott_period as f64);
639                if !self.ema_init {
640                    self.ma_prev = lott;
641                    self.ema_init = true;
642                } else {
643                    self.ma_prev = a.mul_add(lott, (1.0 - a) * self.ma_prev);
644                }
645                Some(self.ma_prev)
646            }
647
648            "DEMA" => {
649                let a = self.dema_alpha;
650                if !self.dema_init {
651                    self.dema_ema1 = lott;
652                    self.dema_ema2 = lott;
653                    self.dema_init = true;
654                } else {
655                    self.dema_ema1 = a.mul_add(lott, (1.0 - a) * self.dema_ema1);
656                    self.dema_ema2 = a.mul_add(self.dema_ema1, (1.0 - a) * self.dema_ema2);
657                }
658                Some(2.0 * self.dema_ema1 - self.dema_ema2)
659            }
660
661            "SMA" => {
662                let p = self.ott_period;
663                let _old = if self.sma_count < p {
664                    self.sma_count += 1;
665                    0.0
666                } else {
667                    let o = self.sma_buf[self.sma_head];
668                    self.sma_sum -= o;
669                    o
670                };
671                self.sma_buf[self.sma_head] = lott;
672                self.sma_sum += lott;
673                self.sma_head += 1;
674                if self.sma_head == p {
675                    self.sma_head = 0;
676                }
677                if self.sma_count >= p {
678                    Some(self.sma_sum / p as f64)
679                } else {
680                    None
681                }
682            }
683
684            "WMA" => {
685                let p = self.ott_period;
686                let x_old = if self.wma_count < p {
687                    self.wma_count += 1;
688                    0.0
689                } else {
690                    self.wma_buf[self.wma_head]
691                };
692
693                self.wma_buf[self.wma_head] = lott;
694                self.wma_head += 1;
695                if self.wma_head == p {
696                    self.wma_head = 0;
697                }
698
699                self.wma_sumwx = self.wma_sumwx - self.wma_sumx + (p as f64) * lott;
700                self.wma_sumx = self.wma_sumx + lott - x_old;
701                if self.wma_count >= p {
702                    Some(self.wma_sumwx / self.wma_denom)
703                } else {
704                    None
705                }
706            }
707
708            "TMA" => {
709                let p1 = self.tma_p1;
710                let _o1 = if self.tma_count1 < p1 {
711                    self.tma_count1 += 1;
712                    0.0
713                } else {
714                    let o = self.tma_ring1[self.tma_head1];
715                    self.tma_sum1 -= o;
716                    o
717                };
718                self.tma_ring1[self.tma_head1] = lott;
719                self.tma_sum1 += lott;
720                self.tma_head1 += 1;
721                if self.tma_head1 == p1 {
722                    self.tma_head1 = 0;
723                }
724                let stage1 = if self.tma_count1 >= p1 {
725                    self.tma_sum1 / p1 as f64
726                } else {
727                    return None;
728                };
729
730                let p2 = self.tma_p2;
731                let _o2 = if self.tma_count2 < p2 {
732                    self.tma_count2 += 1;
733                    0.0
734                } else {
735                    let o = self.tma_ring2[self.tma_head2];
736                    self.tma_sum2 -= o;
737                    o
738                };
739                self.tma_ring2[self.tma_head2] = stage1;
740                self.tma_sum2 += stage1;
741                self.tma_head2 += 1;
742                if self.tma_head2 == p2 {
743                    self.tma_head2 = 0;
744                }
745                if self.tma_count2 >= p2 {
746                    Some(self.tma_sum2 / p2 as f64)
747                } else {
748                    None
749                }
750            }
751
752            "ZLEMA" => {
753                let lag = self.zlema_lag;
754                let x_lag = if self.zlema_count <= lag {
755                    0.0
756                } else {
757                    self.zlema_ring[(self.zlema_head + self.zlema_ring.len() - lag - 1)
758                        % self.zlema_ring.len()]
759                };
760                let x_adj = 2.0 * lott - x_lag;
761
762                if self.zlema_count < self.zlema_ring.len() {
763                    self.zlema_count += 1;
764                }
765                self.zlema_ring[self.zlema_head] = lott;
766                self.zlema_head += 1;
767                if self.zlema_head == self.zlema_ring.len() {
768                    self.zlema_head = 0;
769                }
770
771                let a = self.zlema_alpha;
772                if !self.zlema_init {
773                    self.zlema_prev = x_adj;
774                    self.zlema_init = true;
775                } else {
776                    self.zlema_prev = a.mul_add(x_adj, (1.0 - a) * self.zlema_prev);
777                }
778                Some(self.zlema_prev)
779            }
780
781            _ => None,
782        };
783
784        if self.idx < self.required_len {
785            return None;
786        }
787
788        let ma = match ma_opt {
789            Some(v) => v,
790            None => return None,
791        };
792
793        if !self.ott_init {
794            self.long_stop_prev = ma * (1.0 - self.fark);
795            self.short_stop_prev = ma * (1.0 + self.fark);
796            let mt = self.long_stop_prev;
797            let hott0 = if ma > mt {
798                mt * self.scale_up
799            } else {
800                mt * self.scale_dn
801            };
802            self.ott_init = true;
803            return Some((hott0, lott));
804        }
805
806        let ls = ma * (1.0 - self.fark);
807        let ss = ma * (1.0 + self.fark);
808        let long_stop = if ma > self.long_stop_prev {
809            ls.max(self.long_stop_prev)
810        } else {
811            ls
812        };
813        let short_stop = if ma < self.short_stop_prev {
814            ss.min(self.short_stop_prev)
815        } else {
816            ss
817        };
818        let dir = if self.dir_prev == -1 && ma > self.short_stop_prev {
819            1
820        } else if self.dir_prev == 1 && ma < self.long_stop_prev {
821            -1
822        } else {
823            self.dir_prev
824        };
825        let mt = if dir == 1 { long_stop } else { short_stop };
826        let hott = if ma > mt {
827            mt * self.scale_up
828        } else {
829            mt * self.scale_dn
830        };
831
832        self.long_stop_prev = long_stop;
833        self.short_stop_prev = short_stop;
834        self.dir_prev = dir;
835
836        Some((hott, lott))
837    }
838
839    #[inline]
840    pub fn reset(&mut self) {
841        *self = Self::try_new(OttoParams {
842            ott_period: Some(self.ott_period),
843            ott_percent: Some(self.ott_percent),
844            fast_vidya_length: Some(self.fast_vidya_length),
845            slow_vidya_length: Some(self.slow_vidya_length),
846            correcting_constant: Some(self.correcting_constant),
847            ma_type: Some(self.ma_type.clone()),
848        })
849        .expect("OttoStream::reset: params should remain valid");
850    }
851}
852
853fn cmo_sum_based(data: &[f64], period: usize) -> Vec<f64> {
854    let mut output = vec![f64::NAN; data.len()];
855
856    if data.len() < period + 1 {
857        return output;
858    }
859
860    for i in period..data.len() {
861        let mut sum_up = 0.0;
862        let mut sum_down = 0.0;
863
864        for j in 1..=period {
865            let idx = i - period + j;
866            if idx > 0 {
867                let diff = data[idx] - data[idx - 1];
868                if diff > 0.0 {
869                    sum_up += diff;
870                } else {
871                    sum_down += diff.abs();
872                }
873            }
874        }
875
876        let sum_total = sum_up + sum_down;
877        if sum_total != 0.0 {
878            output[i] = (sum_up - sum_down) / sum_total;
879        } else {
880            output[i] = 0.0;
881        }
882    }
883
884    output
885}
886
887fn vidya(data: &[f64], period: usize) -> Result<Vec<f64>, OttoError> {
888    if data.is_empty() {
889        return Err(OttoError::EmptyInputData);
890    }
891
892    if period == 0 || period > data.len() {
893        return Err(OttoError::InvalidPeriod {
894            period,
895            data_len: data.len(),
896        });
897    }
898
899    let alpha = 2.0 / (period as f64 + 1.0);
900    let mut output = vec![f64::NAN; data.len()];
901
902    let cmo_values = cmo_sum_based(data, 9);
903
904    let mut var_prev = 0.0;
905
906    for i in 0..data.len() {
907        let current_value = if data[i].is_nan() { 0.0 } else { data[i] };
908        let current_cmo = if cmo_values[i].is_nan() {
909            0.0
910        } else {
911            cmo_values[i]
912        };
913
914        if i == 0 {
915            let abs_cmo = current_cmo.abs();
916            let adaptive_alpha = alpha * abs_cmo;
917            var_prev = adaptive_alpha * current_value + (1.0 - adaptive_alpha) * 0.0;
918            output[i] = var_prev;
919        } else {
920            let abs_cmo = current_cmo.abs();
921            let adaptive_alpha = alpha * abs_cmo;
922            var_prev = adaptive_alpha * current_value + (1.0 - adaptive_alpha) * var_prev;
923            output[i] = var_prev;
924        }
925    }
926
927    Ok(output)
928}
929
930fn tma_custom(data: &[f64], period: usize) -> Result<Vec<f64>, OttoError> {
931    if period <= 0 || period > data.len() {
932        return Err(OttoError::InvalidPeriod {
933            period,
934            data_len: data.len(),
935        });
936    }
937
938    let first_period = (period + 1) / 2;
939    let second_period = period / 2 + 1;
940
941    let params1 = SmaParams {
942        period: Some(first_period),
943    };
944    let input1 = SmaInput::from_slice(data, params1);
945    let sma1 = sma(&input1).map_err(|e| OttoError::MaError(e.to_string()))?;
946
947    let params2 = SmaParams {
948        period: Some(second_period),
949    };
950    let input2 = SmaInput::from_slice(&sma1.values, params2);
951    let sma2 = sma(&input2).map_err(|e| OttoError::MaError(e.to_string()))?;
952
953    Ok(sma2.values)
954}
955
956fn wwma(data: &[f64], period: usize) -> Result<Vec<f64>, OttoError> {
957    if data.is_empty() {
958        return Err(OttoError::EmptyInputData);
959    }
960
961    if period == 0 || period > data.len() {
962        return Err(OttoError::InvalidPeriod {
963            period,
964            data_len: data.len(),
965        });
966    }
967
968    let alpha = 1.0 / period as f64;
969    let mut output = vec![f64::NAN; data.len()];
970
971    let first_valid = data.iter().position(|&x| !x.is_nan()).unwrap_or(0);
972
973    let mut sum = 0.0;
974    let mut count = 0;
975    for i in first_valid..first_valid.saturating_add(period).min(data.len()) {
976        if !data[i].is_nan() {
977            sum += data[i];
978            count += 1;
979        }
980    }
981
982    if count > 0 {
983        let mut wwma_prev = sum / count as f64;
984        output[first_valid + period - 1] = wwma_prev;
985
986        for i in first_valid + period..data.len() {
987            if !data[i].is_nan() {
988                wwma_prev = alpha * data[i] + (1.0 - alpha) * wwma_prev;
989                output[i] = wwma_prev;
990            } else {
991                output[i] = wwma_prev;
992            }
993        }
994    }
995
996    Ok(output)
997}
998
999fn calculate_ma(data: &[f64], period: usize, ma_type: &str) -> Result<Vec<f64>, OttoError> {
1000    match ma_type {
1001        "SMA" => {
1002            let params = SmaParams {
1003                period: Some(period),
1004            };
1005            let input = SmaInput::from_slice(data, params);
1006            sma(&input)
1007                .map(|o| o.values)
1008                .map_err(|e| OttoError::MaError(e.to_string()))
1009        }
1010        "EMA" => {
1011            let params = EmaParams {
1012                period: Some(period),
1013            };
1014            let input = EmaInput::from_slice(data, params);
1015            ema(&input)
1016                .map(|o| o.values)
1017                .map_err(|e| OttoError::MaError(e.to_string()))
1018        }
1019        "WMA" => {
1020            let params = WmaParams {
1021                period: Some(period),
1022            };
1023            let input = WmaInput::from_slice(data, params);
1024            wma(&input)
1025                .map(|o| o.values)
1026                .map_err(|e| OttoError::MaError(e.to_string()))
1027        }
1028        "WWMA" => wwma(data, period),
1029        "DEMA" => {
1030            let params = DemaParams {
1031                period: Some(period),
1032            };
1033            let input = DemaInput::from_slice(data, params);
1034            dema(&input)
1035                .map(|o| o.values)
1036                .map_err(|e| OttoError::MaError(e.to_string()))
1037        }
1038        "TMA" => tma_custom(data, period),
1039        "VAR" => vidya(data, period),
1040        "ZLEMA" => {
1041            let params = ZlemaParams {
1042                period: Some(period),
1043            };
1044            let input = ZlemaInput::from_slice(data, params);
1045            zlema(&input)
1046                .map(|o| o.values)
1047                .map_err(|e| OttoError::MaError(e.to_string()))
1048        }
1049        "TSF" => {
1050            let params = TsfParams {
1051                period: Some(period),
1052            };
1053            let input = TsfInput::from_slice(data, params);
1054            tsf(&input)
1055                .map(|o| o.values)
1056                .map_err(|e| OttoError::MaError(e.to_string()))
1057        }
1058        "HULL" => {
1059            let params = HmaParams {
1060                period: Some(period),
1061            };
1062            let input = HmaInput::from_slice(data, params);
1063            hma(&input)
1064                .map(|o| o.values)
1065                .map_err(|e| OttoError::MaError(e.to_string()))
1066        }
1067        _ => Err(OttoError::InvalidMaType {
1068            ma_type: ma_type.to_string(),
1069        }),
1070    }
1071}
1072
1073#[inline]
1074fn resolve_single_kernel(k: Kernel) -> Kernel {
1075    match k {
1076        Kernel::Auto => detect_best_kernel(),
1077        other => other,
1078    }
1079}
1080
1081#[inline]
1082fn resolve_batch_kernel(k: Kernel) -> Result<Kernel, OttoError> {
1083    Ok(match k {
1084        Kernel::Auto => detect_best_batch_kernel(),
1085        b if b.is_batch() => b,
1086        other => {
1087            return Err(OttoError::InvalidKernelForBatch(other));
1088        }
1089    })
1090}
1091
1092#[inline]
1093fn first_valid_idx(d: &[f64]) -> Result<usize, OttoError> {
1094    d.iter()
1095        .position(|x| !x.is_nan())
1096        .ok_or(OttoError::AllValuesNaN)
1097}
1098
1099#[inline]
1100fn otto_prepare<'a>(
1101    input: &'a OttoInput,
1102) -> Result<(&'a [f64], usize, usize, usize, f64, String), OttoError> {
1103    let data = input.as_ref();
1104    if data.is_empty() {
1105        return Err(OttoError::EmptyInputData);
1106    }
1107
1108    let first = first_valid_idx(data)?;
1109    let ott_period = input.get_ott_period();
1110    if ott_period == 0 || ott_period > data.len() {
1111        return Err(OttoError::InvalidPeriod {
1112            period: ott_period,
1113            data_len: data.len(),
1114        });
1115    }
1116
1117    let ott_percent = input.get_ott_percent();
1118    let ma_type = input.get_ma_type().to_string();
1119
1120    let slow = input.get_slow_vidya_length();
1121    let fast = input.get_fast_vidya_length();
1122
1123    let needed = (slow * fast).max(10);
1124    let valid = data.len() - first;
1125    if valid < needed {
1126        return Err(OttoError::NotEnoughValidData { needed, valid });
1127    }
1128
1129    Ok((data, first, ott_period, needed, ott_percent, ma_type))
1130}
1131
1132#[inline]
1133pub fn otto_into_slices(
1134    hott_dst: &mut [f64],
1135    lott_dst: &mut [f64],
1136    input: &OttoInput,
1137    _kern: Kernel,
1138) -> Result<(), OttoError> {
1139    let (data, _first, ott_p, _needed, ott_percent, ma_type) = otto_prepare(input)?;
1140    let n = data.len();
1141    if hott_dst.len() != n || lott_dst.len() != n {
1142        let expected = n;
1143        let got = hott_dst.len().max(lott_dst.len());
1144        return Err(OttoError::OutputLengthMismatch { expected, got });
1145    }
1146
1147    let slow = input.get_slow_vidya_length();
1148    let fast = input.get_fast_vidya_length();
1149    let p1 = slow / 2;
1150    let p2 = slow;
1151    let p3 = slow.saturating_mul(fast);
1152
1153    if p1 == 0 || p2 == 0 || p3 == 0 {
1154        return Err(OttoError::InvalidPeriod {
1155            period: 0,
1156            data_len: n,
1157        });
1158    }
1159
1160    let coco = input.get_correcting_constant();
1161
1162    let a1_base = 2.0 / (p1 as f64 + 1.0);
1163    let a2_base = 2.0 / (p2 as f64 + 1.0);
1164    let a3_base = 2.0 / (p3 as f64 + 1.0);
1165
1166    const CMO_P: usize = 9;
1167    let mut ring_up = [0.0f64; CMO_P];
1168    let mut ring_dn = [0.0f64; CMO_P];
1169    let mut sum_up = 0.0f64;
1170    let mut sum_dn = 0.0f64;
1171    let mut head = 0usize;
1172
1173    let mut v1 = 0.0f64;
1174    let mut v2 = 0.0f64;
1175    let mut v3 = 0.0f64;
1176
1177    let mut prev_x = if n > 0 { data[0] } else { f64::NAN };
1178
1179    for i in 0..n {
1180        let x = data[i];
1181        let val = if x.is_nan() { 0.0 } else { x };
1182
1183        if i > 0 {
1184            let mut d = x - prev_x;
1185            if !x.is_finite() || !prev_x.is_finite() {
1186                d = 0.0;
1187            }
1188
1189            if i >= CMO_P {
1190                sum_up -= ring_up[head];
1191                sum_dn -= ring_dn[head];
1192            }
1193
1194            let (up, dn) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
1195            ring_up[head] = up;
1196            ring_dn[head] = dn;
1197            sum_up += up;
1198            sum_dn += dn;
1199
1200            head += 1;
1201            if head == CMO_P {
1202                head = 0;
1203            }
1204
1205            prev_x = x;
1206        }
1207
1208        let cmo_abs = if i >= CMO_P {
1209            let denom = sum_up + sum_dn;
1210            if denom != 0.0 {
1211                ((sum_up - sum_dn) / denom).abs()
1212            } else {
1213                0.0
1214            }
1215        } else {
1216            0.0
1217        };
1218
1219        let a1 = a1_base * cmo_abs;
1220        let a2 = a2_base * cmo_abs;
1221        let a3 = a3_base * cmo_abs;
1222        v1 = a1 * val + (1.0 - a1) * v1;
1223        v2 = a2 * val + (1.0 - a2) * v2;
1224        v3 = a3 * val + (1.0 - a3) * v3;
1225
1226        let denom_l = (v2 - v3) + coco;
1227        lott_dst[i] = v1 / denom_l;
1228    }
1229
1230    let fark = ott_percent * 0.01;
1231    let scale_up = (200.0 + ott_percent) / 200.0;
1232    let scale_dn = (200.0 - ott_percent) / 200.0;
1233
1234    if ma_type == "VAR" {
1235        const CMO_P2: usize = 9;
1236        let mut ring_up2 = [0.0f64; CMO_P2];
1237        let mut ring_dn2 = [0.0f64; CMO_P2];
1238        let mut sum_up2 = 0.0f64;
1239        let mut sum_dn2 = 0.0f64;
1240        let mut head2 = 0usize;
1241        let mut prev_lott = lott_dst[0];
1242
1243        let a_base = 2.0 / (ott_p as f64 + 1.0);
1244        let mut ma_prev = 0.0f64;
1245
1246        let mut long_stop_prev = f64::NAN;
1247        let mut short_stop_prev = f64::NAN;
1248        let mut dir_prev: i32 = 1;
1249
1250        for i in 0..n {
1251            if i > 0 {
1252                let x = lott_dst[i];
1253                let mut d = x - prev_lott;
1254                if !x.is_finite() || !prev_lott.is_finite() {
1255                    d = 0.0;
1256                }
1257                if i >= CMO_P2 {
1258                    sum_up2 -= ring_up2[head2];
1259                    sum_dn2 -= ring_dn2[head2];
1260                }
1261                let (up, dn) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
1262                ring_up2[head2] = up;
1263                ring_dn2[head2] = dn;
1264                sum_up2 += up;
1265                sum_dn2 += dn;
1266                head2 += 1;
1267                if head2 == CMO_P2 {
1268                    head2 = 0;
1269                }
1270                prev_lott = x;
1271            }
1272
1273            let c_abs = if i >= CMO_P2 {
1274                let denom = sum_up2 + sum_dn2;
1275                if denom != 0.0 {
1276                    ((sum_up2 - sum_dn2) / denom).abs()
1277                } else {
1278                    0.0
1279                }
1280            } else {
1281                0.0
1282            };
1283
1284            let a = a_base * c_abs;
1285            let ma = a * lott_dst[i] + (1.0 - a) * ma_prev;
1286            ma_prev = ma;
1287
1288            if i == 0 {
1289                long_stop_prev = ma * (1.0 - fark);
1290                short_stop_prev = ma * (1.0 + fark);
1291                let mt = long_stop_prev;
1292                hott_dst[i] = if ma > mt {
1293                    mt * scale_up
1294                } else {
1295                    mt * scale_dn
1296                };
1297            } else {
1298                let ls = ma * (1.0 - fark);
1299                let ss = ma * (1.0 + fark);
1300                let long_stop = if ma > long_stop_prev {
1301                    ls.max(long_stop_prev)
1302                } else {
1303                    ls
1304                };
1305                let short_stop = if ma < short_stop_prev {
1306                    ss.min(short_stop_prev)
1307                } else {
1308                    ss
1309                };
1310                let dir = if dir_prev == -1 && ma > short_stop_prev {
1311                    1
1312                } else if dir_prev == 1 && ma < long_stop_prev {
1313                    -1
1314                } else {
1315                    dir_prev
1316                };
1317                let mt = if dir == 1 { long_stop } else { short_stop };
1318                hott_dst[i] = if ma > mt {
1319                    mt * scale_up
1320                } else {
1321                    mt * scale_dn
1322                };
1323                long_stop_prev = long_stop;
1324                short_stop_prev = short_stop;
1325                dir_prev = dir;
1326            }
1327        }
1328    } else {
1329        let mavg = calculate_ma(lott_dst, ott_p, &ma_type)?;
1330
1331        let mut long_stop_prev = f64::NAN;
1332        let mut short_stop_prev = f64::NAN;
1333        let mut dir_prev: i32 = 1;
1334
1335        let start = mavg.iter().position(|&x| !x.is_nan()).unwrap_or(n);
1336        for i in 0..start.min(n) {
1337            hott_dst[i] = f64::NAN;
1338        }
1339        if start < n {
1340            let ma0 = mavg[start];
1341            long_stop_prev = ma0 * (1.0 - fark);
1342            short_stop_prev = ma0 * (1.0 + fark);
1343            let mt0 = long_stop_prev;
1344            hott_dst[start] = if ma0 > mt0 {
1345                mt0 * scale_up
1346            } else {
1347                mt0 * scale_dn
1348            };
1349            for i in (start + 1)..n {
1350                let ma = mavg[i];
1351                if ma.is_nan() {
1352                    hott_dst[i] = hott_dst[i - 1];
1353                    continue;
1354                }
1355                let ls = ma * (1.0 - fark);
1356                let ss = ma * (1.0 + fark);
1357                let long_stop = if ma > long_stop_prev {
1358                    ls.max(long_stop_prev)
1359                } else {
1360                    ls
1361                };
1362                let short_stop = if ma < short_stop_prev {
1363                    ss.min(short_stop_prev)
1364                } else {
1365                    ss
1366                };
1367                let dir = if dir_prev == -1 && ma > short_stop_prev {
1368                    1
1369                } else if dir_prev == 1 && ma < long_stop_prev {
1370                    -1
1371                } else {
1372                    dir_prev
1373                };
1374                let mt = if dir == 1 { long_stop } else { short_stop };
1375                hott_dst[i] = if ma > mt {
1376                    mt * scale_up
1377                } else {
1378                    mt * scale_dn
1379                };
1380                long_stop_prev = long_stop;
1381                short_stop_prev = short_stop;
1382                dir_prev = dir;
1383            }
1384        }
1385    }
1386
1387    Ok(())
1388}
1389
1390pub fn otto_with_kernel(input: &OttoInput, kern: Kernel) -> Result<OttoOutput, OttoError> {
1391    let chosen = resolve_single_kernel(kern);
1392    let data = input.as_ref();
1393    if data.is_empty() {
1394        return Err(OttoError::EmptyInputData);
1395    }
1396
1397    let mut hott = alloc_with_nan_prefix(data.len(), 0);
1398    let mut lott = alloc_with_nan_prefix(data.len(), 0);
1399
1400    otto_into_slices(&mut hott, &mut lott, input, chosen)?;
1401    Ok(OttoOutput { hott, lott })
1402}
1403
1404#[inline]
1405pub fn otto(input: &OttoInput) -> Result<OttoOutput, OttoError> {
1406    otto_with_kernel(input, Kernel::Auto)
1407}
1408
1409#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1410#[inline]
1411pub fn otto_into(
1412    input: &OttoInput,
1413    hott_out: &mut [f64],
1414    lott_out: &mut [f64],
1415) -> Result<(), OttoError> {
1416    otto_into_slices(hott_out, lott_out, input, Kernel::Auto)
1417}
1418
1419#[derive(Clone, Debug)]
1420pub struct OttoBatchRange {
1421    pub ott_period: (usize, usize, usize),
1422    pub ott_percent: (f64, f64, f64),
1423    pub fast_vidya: (usize, usize, usize),
1424    pub slow_vidya: (usize, usize, usize),
1425    pub correcting_constant: (f64, f64, f64),
1426    pub ma_types: Vec<String>,
1427}
1428
1429impl Default for OttoBatchRange {
1430    fn default() -> Self {
1431        Self {
1432            ott_period: (2, 251, 1),
1433            ott_percent: (0.6, 0.6, 0.0),
1434            fast_vidya: (10, 10, 0),
1435            slow_vidya: (25, 25, 0),
1436            correcting_constant: (100000.0, 100000.0, 0.0),
1437            ma_types: vec!["VAR".into()],
1438        }
1439    }
1440}
1441
1442#[derive(Clone, Debug, Default)]
1443pub struct OttoBatchBuilder {
1444    range: OttoBatchRange,
1445    kernel: Kernel,
1446}
1447
1448impl OttoBatchBuilder {
1449    pub fn new() -> Self {
1450        Self::default()
1451    }
1452    pub fn kernel(mut self, k: Kernel) -> Self {
1453        self.kernel = k;
1454        self
1455    }
1456    pub fn ott_period_range(mut self, s: usize, e: usize, step: usize) -> Self {
1457        self.range.ott_period = (s, e, step);
1458        self
1459    }
1460    pub fn ott_percent_range(mut self, s: f64, e: f64, step: f64) -> Self {
1461        self.range.ott_percent = (s, e, step);
1462        self
1463    }
1464    pub fn fast_vidya_range(mut self, s: usize, e: usize, step: usize) -> Self {
1465        self.range.fast_vidya = (s, e, step);
1466        self
1467    }
1468    pub fn slow_vidya_range(mut self, s: usize, e: usize, step: usize) -> Self {
1469        self.range.slow_vidya = (s, e, step);
1470        self
1471    }
1472    pub fn correcting_constant_range(mut self, s: f64, e: f64, step: f64) -> Self {
1473        self.range.correcting_constant = (s, e, step);
1474        self
1475    }
1476    pub fn ma_types(mut self, v: Vec<String>) -> Self {
1477        self.range.ma_types = v;
1478        self
1479    }
1480
1481    pub fn apply_slice(self, data: &[f64]) -> Result<OttoBatchOutput, OttoError> {
1482        otto_batch_with_kernel(data, &self.range, self.kernel)
1483    }
1484
1485    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<OttoBatchOutput, OttoError> {
1486        self.apply_slice(source_type(c, src))
1487    }
1488
1489    pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<OttoBatchOutput, OttoError> {
1490        OttoBatchBuilder::new().kernel(k).apply_slice(data)
1491    }
1492}
1493
1494#[derive(Clone, Debug, PartialEq)]
1495pub struct OttoBatchCombo(pub OttoParams);
1496
1497#[derive(Clone, Debug)]
1498pub struct OttoBatchOutput {
1499    pub hott: Vec<f64>,
1500    pub lott: Vec<f64>,
1501    pub combos: Vec<OttoParams>,
1502    pub rows: usize,
1503    pub cols: usize,
1504}
1505
1506#[inline]
1507fn axis_usize(a: (usize, usize, usize)) -> Result<Vec<usize>, OttoError> {
1508    let (start, end, step) = a;
1509    if step == 0 || start == end {
1510        return Ok(vec![start]);
1511    }
1512    if start < end {
1513        let v: Vec<_> = (start..=end).step_by(step).collect();
1514        if v.is_empty() {
1515            return Err(OttoError::InvalidRange {
1516                start: start.to_string(),
1517                end: end.to_string(),
1518                step: step.to_string(),
1519            });
1520        }
1521        Ok(v)
1522    } else {
1523        let mut v = Vec::new();
1524        let mut cur = start;
1525        while cur >= end {
1526            v.push(cur);
1527            if cur - end < step {
1528                break;
1529            }
1530            cur -= step;
1531        }
1532        if v.is_empty() {
1533            return Err(OttoError::InvalidRange {
1534                start: start.to_string(),
1535                end: end.to_string(),
1536                step: step.to_string(),
1537            });
1538        }
1539        Ok(v)
1540    }
1541}
1542
1543#[inline]
1544fn axis_f64(a: (f64, f64, f64)) -> Result<Vec<f64>, OttoError> {
1545    let (start, end, step) = a;
1546    if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
1547        return Ok(vec![start]);
1548    }
1549    let mut out = Vec::new();
1550    if start < end {
1551        let st = if step > 0.0 { step } else { -step };
1552        let mut x = start;
1553        while x <= end + 1e-12 {
1554            out.push(x);
1555            x += st;
1556        }
1557    } else {
1558        let st = if step > 0.0 { -step } else { step };
1559        if st.abs() < 1e-12 {
1560            return Ok(vec![start]);
1561        }
1562        let mut x = start;
1563        while x >= end - 1e-12 {
1564            out.push(x);
1565            x += st;
1566        }
1567    }
1568    if out.is_empty() {
1569        return Err(OttoError::InvalidRange {
1570            start: start.to_string(),
1571            end: end.to_string(),
1572            step: step.to_string(),
1573        });
1574    }
1575    Ok(out)
1576}
1577
1578fn expand_grid_otto(r: &OttoBatchRange) -> Result<Vec<OttoParams>, OttoError> {
1579    let p = axis_usize(r.ott_period)?;
1580    let op = axis_f64(r.ott_percent)?;
1581    let fv = axis_usize(r.fast_vidya)?;
1582    let sv = axis_usize(r.slow_vidya)?;
1583    let cc = axis_f64(r.correcting_constant)?;
1584    let mt = &r.ma_types;
1585
1586    if mt.is_empty() {
1587        return Err(OttoError::InvalidRange {
1588            start: "ma_types".into(),
1589            end: "ma_types".into(),
1590            step: "0".into(),
1591        });
1592    }
1593
1594    let mut v = Vec::with_capacity(
1595        p.len()
1596            .saturating_mul(op.len())
1597            .saturating_mul(fv.len())
1598            .saturating_mul(sv.len())
1599            .saturating_mul(cc.len())
1600            .saturating_mul(mt.len()),
1601    );
1602    for &pp in &p {
1603        for &oo in &op {
1604            for &ff in &fv {
1605                for &ss in &sv {
1606                    for &ccv in &cc {
1607                        for m in mt {
1608                            v.push(OttoParams {
1609                                ott_period: Some(pp),
1610                                ott_percent: Some(oo),
1611                                fast_vidya_length: Some(ff),
1612                                slow_vidya_length: Some(ss),
1613                                correcting_constant: Some(ccv),
1614                                ma_type: Some(m.clone()),
1615                            });
1616                        }
1617                    }
1618                }
1619            }
1620        }
1621    }
1622    if v.is_empty() {
1623        return Err(OttoError::InvalidRange {
1624            start: "otto_batch".into(),
1625            end: "otto_batch".into(),
1626            step: "0".into(),
1627        });
1628    }
1629    Ok(v)
1630}
1631
1632#[inline]
1633fn cmo_abs9_stream(data: &[f64]) -> Vec<f64> {
1634    const CMO_P: usize = 9;
1635    let n = data.len();
1636    let mut out = vec![0.0f64; n];
1637    if n == 0 {
1638        return out;
1639    }
1640
1641    let mut ring_up = [0.0f64; CMO_P];
1642    let mut ring_dn = [0.0f64; CMO_P];
1643    let mut sum_up = 0.0f64;
1644    let mut sum_dn = 0.0f64;
1645    let mut head = 0usize;
1646    let mut prev_x = data[0];
1647
1648    for i in 0..n {
1649        let x = data[i];
1650        if i > 0 {
1651            let mut d = x - prev_x;
1652            if !x.is_finite() || !prev_x.is_finite() {
1653                d = 0.0;
1654            }
1655            if i >= CMO_P {
1656                sum_up -= ring_up[head];
1657                sum_dn -= ring_dn[head];
1658            }
1659            let (up, dn) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
1660            ring_up[head] = up;
1661            ring_dn[head] = dn;
1662            sum_up += up;
1663            sum_dn += dn;
1664            head += 1;
1665            if head == CMO_P {
1666                head = 0;
1667            }
1668            prev_x = x;
1669        }
1670        if i >= CMO_P {
1671            let denom = sum_up + sum_dn;
1672            out[i] = if denom != 0.0 {
1673                ((sum_up - sum_dn) / denom).abs()
1674            } else {
1675                0.0
1676            };
1677        } else {
1678            out[i] = 0.0;
1679        }
1680    }
1681    out
1682}
1683
1684pub fn otto_batch_with_kernel(
1685    data: &[f64],
1686    sweep: &OttoBatchRange,
1687    k: Kernel,
1688) -> Result<OttoBatchOutput, OttoError> {
1689    if data.is_empty() {
1690        return Err(OttoError::EmptyInputData);
1691    }
1692    let kernel = resolve_batch_kernel(k)?;
1693
1694    let combos = expand_grid_otto(sweep)?;
1695    let rows = combos.len();
1696    let cols = data.len();
1697    let total = rows
1698        .checked_mul(cols)
1699        .ok_or_else(|| OttoError::InvalidInput("rows*cols overflow".into()))?;
1700
1701    let mut hott = vec![f64::NAN; total];
1702    let mut lott = vec![f64::NAN; total];
1703
1704    let cmo_abs = cmo_abs9_stream(data);
1705
1706    for (row, prm) in combos.iter().enumerate() {
1707        let input = OttoInput::from_slice(data, prm.clone());
1708
1709        let (_d, _first, ott_p, _needed, ott_percent, ma_type) = otto_prepare(&input)?;
1710
1711        let n = data.len();
1712        let slow = input.get_slow_vidya_length();
1713        let fast = input.get_fast_vidya_length();
1714        let p1 = slow / 2;
1715        let p2 = slow;
1716        let p3 = slow.saturating_mul(fast);
1717        if p1 == 0 || p2 == 0 || p3 == 0 {
1718            return Err(OttoError::InvalidPeriod {
1719                period: 0,
1720                data_len: n,
1721            });
1722        }
1723
1724        let a1_base = 2.0 / (p1 as f64 + 1.0);
1725        let a2_base = 2.0 / (p2 as f64 + 1.0);
1726        let a3_base = 2.0 / (p3 as f64 + 1.0);
1727        let coco = input.get_correcting_constant();
1728
1729        let row_l = &mut lott[row * cols..(row + 1) * cols];
1730        let row_h = &mut hott[row * cols..(row + 1) * cols];
1731
1732        let mut v1 = 0.0f64;
1733        let mut v2 = 0.0f64;
1734        let mut v3 = 0.0f64;
1735        for i in 0..n {
1736            let x = data[i];
1737            let val = if x.is_nan() { 0.0 } else { x };
1738            let c = cmo_abs[i];
1739            let a1 = a1_base * c;
1740            let a2 = a2_base * c;
1741            let a3 = a3_base * c;
1742            v1 = a1 * val + (1.0 - a1) * v1;
1743            v2 = a2 * val + (1.0 - a2) * v2;
1744            v3 = a3 * val + (1.0 - a3) * v3;
1745            row_l[i] = v1 / ((v2 - v3) + coco);
1746        }
1747
1748        let mavg = calculate_ma(row_l, ott_p, &ma_type)?;
1749        let fark = ott_percent * 0.01;
1750        let scale_up = (200.0 + ott_percent) / 200.0;
1751        let scale_dn = (200.0 - ott_percent) / 200.0;
1752
1753        let mut long_stop_prev = f64::NAN;
1754        let mut short_stop_prev = f64::NAN;
1755        let mut dir_prev: i32 = 1;
1756
1757        let start = mavg.iter().position(|&x| !x.is_nan()).unwrap_or(n);
1758        for i in 0..start.min(n) {
1759            row_h[i] = f64::NAN;
1760        }
1761
1762        if start < n {
1763            let ma0 = mavg[start];
1764            long_stop_prev = ma0 * (1.0 - fark);
1765            short_stop_prev = ma0 * (1.0 + fark);
1766            let mt0 = long_stop_prev;
1767            row_h[start] = if ma0 > mt0 {
1768                mt0 * scale_up
1769            } else {
1770                mt0 * scale_dn
1771            };
1772            for i in (start + 1)..n {
1773                let ma = mavg[i];
1774                if ma.is_nan() {
1775                    row_h[i] = row_h[i - 1];
1776                    continue;
1777                }
1778                let ls = ma * (1.0 - fark);
1779                let ss = ma * (1.0 + fark);
1780                let long_stop = if ma > long_stop_prev {
1781                    ls.max(long_stop_prev)
1782                } else {
1783                    ls
1784                };
1785                let short_stop = if ma < short_stop_prev {
1786                    ss.min(short_stop_prev)
1787                } else {
1788                    ss
1789                };
1790                let dir = if dir_prev == -1 && ma > short_stop_prev {
1791                    1
1792                } else if dir_prev == 1 && ma < long_stop_prev {
1793                    -1
1794                } else {
1795                    dir_prev
1796                };
1797                let mt = if dir == 1 { long_stop } else { short_stop };
1798                row_h[i] = if ma > mt {
1799                    mt * scale_up
1800                } else {
1801                    mt * scale_dn
1802                };
1803                long_stop_prev = long_stop;
1804                short_stop_prev = short_stop;
1805                dir_prev = dir;
1806            }
1807        }
1808    }
1809
1810    Ok(OttoBatchOutput {
1811        hott,
1812        lott,
1813        combos,
1814        rows,
1815        cols,
1816    })
1817}
1818
1819#[cfg(feature = "python")]
1820#[pyfunction(name = "otto")]
1821#[pyo3(signature = (data, ott_period, ott_percent, fast_vidya_length, slow_vidya_length, correcting_constant, ma_type, kernel=None))]
1822pub fn otto_py<'py>(
1823    py: Python<'py>,
1824    data: numpy::PyReadonlyArray1<'py, f64>,
1825    ott_period: usize,
1826    ott_percent: f64,
1827    fast_vidya_length: usize,
1828    slow_vidya_length: usize,
1829    correcting_constant: f64,
1830    ma_type: &str,
1831    kernel: Option<&str>,
1832) -> PyResult<(
1833    Bound<'py, numpy::PyArray1<f64>>,
1834    Bound<'py, numpy::PyArray1<f64>>,
1835)> {
1836    use numpy::{IntoPyArray, PyArray1};
1837
1838    let slice_in = data.as_slice()?;
1839    let kern = validate_kernel(kernel, false)?;
1840
1841    let params = OttoParams {
1842        ott_period: Some(ott_period),
1843        ott_percent: Some(ott_percent),
1844        fast_vidya_length: Some(fast_vidya_length),
1845        slow_vidya_length: Some(slow_vidya_length),
1846        correcting_constant: Some(correcting_constant),
1847        ma_type: Some(ma_type.to_string()),
1848    };
1849    let input = OttoInput::from_slice(slice_in, params);
1850
1851    let out = py
1852        .allow_threads(|| otto_with_kernel(&input, kern))
1853        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1854
1855    Ok((out.hott.into_pyarray(py), out.lott.into_pyarray(py)))
1856}
1857
1858#[cfg(feature = "python")]
1859#[pyfunction(name = "otto_batch")]
1860#[pyo3(signature = (data, ott_period_range, ott_percent_range, fast_vidya_range, slow_vidya_range, correcting_constant_range, ma_types, kernel=None))]
1861pub fn otto_batch_py<'py>(
1862    py: Python<'py>,
1863    data: numpy::PyReadonlyArray1<'py, f64>,
1864    ott_period_range: (usize, usize, usize),
1865    ott_percent_range: (f64, f64, f64),
1866    fast_vidya_range: (usize, usize, usize),
1867    slow_vidya_range: (usize, usize, usize),
1868    correcting_constant_range: (f64, f64, f64),
1869    ma_types: Vec<String>,
1870    kernel: Option<&str>,
1871) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1872    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1873    let slice_in = data.as_slice()?;
1874    let kern = validate_kernel(kernel, true)?;
1875    let sweep = OttoBatchRange {
1876        ott_period: ott_period_range,
1877        ott_percent: ott_percent_range,
1878        fast_vidya: fast_vidya_range,
1879        slow_vidya: slow_vidya_range,
1880        correcting_constant: correcting_constant_range,
1881        ma_types,
1882    };
1883    let out = py
1884        .allow_threads(|| otto_batch_with_kernel(slice_in, &sweep, kern))
1885        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1886
1887    let dict = PyDict::new(py);
1888    let hott = out.hott.into_pyarray(py).reshape([out.rows, out.cols])?;
1889    let lott = out.lott.into_pyarray(py).reshape([out.rows, out.cols])?;
1890    dict.set_item("hott", hott)?;
1891    dict.set_item("lott", lott)?;
1892    dict.set_item(
1893        "ott_periods",
1894        out.combos
1895            .iter()
1896            .map(|p| p.ott_period.unwrap() as u64)
1897            .collect::<Vec<_>>()
1898            .into_pyarray(py),
1899    )?;
1900    dict.set_item(
1901        "ott_percents",
1902        out.combos
1903            .iter()
1904            .map(|p| p.ott_percent.unwrap())
1905            .collect::<Vec<_>>()
1906            .into_pyarray(py),
1907    )?;
1908    dict.set_item(
1909        "fast_vidya",
1910        out.combos
1911            .iter()
1912            .map(|p| p.fast_vidya_length.unwrap() as u64)
1913            .collect::<Vec<_>>()
1914            .into_pyarray(py),
1915    )?;
1916    dict.set_item(
1917        "slow_vidya",
1918        out.combos
1919            .iter()
1920            .map(|p| p.slow_vidya_length.unwrap() as u64)
1921            .collect::<Vec<_>>()
1922            .into_pyarray(py),
1923    )?;
1924    let py_list = PyList::new(py, out.combos.iter().map(|p| p.ma_type.clone().unwrap()))?;
1925    dict.set_item("ma_types", py_list)?;
1926    Ok(dict)
1927}
1928
1929#[cfg(feature = "python")]
1930#[pyclass]
1931pub struct OttoStreamPy {
1932    ott_period: usize,
1933    ott_percent: f64,
1934    fast_vidya_length: usize,
1935    slow_vidya_length: usize,
1936    correcting_constant: f64,
1937    ma_type: String,
1938    buffer: Vec<f64>,
1939}
1940
1941#[cfg(feature = "python")]
1942#[pymethods]
1943impl OttoStreamPy {
1944    #[new]
1945    #[pyo3(signature = (ott_period=None, ott_percent=None, fast_vidya_length=None, slow_vidya_length=None, correcting_constant=None, ma_type=None))]
1946    pub fn new(
1947        ott_period: Option<usize>,
1948        ott_percent: Option<f64>,
1949        fast_vidya_length: Option<usize>,
1950        slow_vidya_length: Option<usize>,
1951        correcting_constant: Option<f64>,
1952        ma_type: Option<String>,
1953    ) -> Self {
1954        Self {
1955            ott_period: ott_period.unwrap_or(2),
1956            ott_percent: ott_percent.unwrap_or(0.6),
1957            fast_vidya_length: fast_vidya_length.unwrap_or(10),
1958            slow_vidya_length: slow_vidya_length.unwrap_or(25),
1959            correcting_constant: correcting_constant.unwrap_or(100000.0),
1960            ma_type: ma_type.unwrap_or_else(|| "VAR".to_string()),
1961            buffer: Vec::new(),
1962        }
1963    }
1964
1965    pub fn update(&mut self, value: f64) -> PyResult<(Option<f64>, Option<f64>)> {
1966        self.buffer.push(value);
1967
1968        let required_len = self.slow_vidya_length * self.fast_vidya_length + 10;
1969        if self.buffer.len() < required_len {
1970            return Ok((None, None));
1971        }
1972
1973        let params = OttoParams {
1974            ott_period: Some(self.ott_period),
1975            ott_percent: Some(self.ott_percent),
1976            fast_vidya_length: Some(self.fast_vidya_length),
1977            slow_vidya_length: Some(self.slow_vidya_length),
1978            correcting_constant: Some(self.correcting_constant),
1979            ma_type: Some(self.ma_type.clone()),
1980        };
1981
1982        let input = OttoInput::from_slice(&self.buffer, params);
1983
1984        match otto(&input) {
1985            Ok(output) => {
1986                let last_idx = output.hott.len() - 1;
1987                Ok((Some(output.hott[last_idx]), Some(output.lott[last_idx])))
1988            }
1989            Err(e) => Err(PyValueError::new_err(e.to_string())),
1990        }
1991    }
1992
1993    pub fn reset(&mut self) {
1994        self.buffer.clear();
1995    }
1996}
1997
1998#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1999#[derive(Serialize, Deserialize)]
2000pub struct OttoResult {
2001    pub values: Vec<f64>,
2002    pub rows: usize,
2003    pub cols: usize,
2004}
2005
2006#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2007#[wasm_bindgen]
2008pub fn otto_js(
2009    data: &[f64],
2010    ott_period: usize,
2011    ott_percent: f64,
2012    fast_vidya_length: usize,
2013    slow_vidya_length: usize,
2014    correcting_constant: f64,
2015    ma_type: &str,
2016) -> Result<JsValue, JsValue> {
2017    let params = OttoParams {
2018        ott_period: Some(ott_period),
2019        ott_percent: Some(ott_percent),
2020        fast_vidya_length: Some(fast_vidya_length),
2021        slow_vidya_length: Some(slow_vidya_length),
2022        correcting_constant: Some(correcting_constant),
2023        ma_type: Some(ma_type.to_string()),
2024    };
2025    let input = OttoInput::from_slice(data, params);
2026
2027    let out = otto_with_kernel(&input, detect_best_kernel())
2028        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2029
2030    let mut values = Vec::with_capacity(data.len() * 2);
2031    values.extend_from_slice(&out.hott);
2032    values.extend_from_slice(&out.lott);
2033
2034    let js = OttoResult {
2035        values,
2036        rows: 2,
2037        cols: data.len(),
2038    };
2039    serde_wasm_bindgen::to_value(&js)
2040        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2041}
2042
2043#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2044#[derive(Serialize, Deserialize)]
2045pub struct OttoBatchConfig {
2046    pub ott_period: (usize, usize, usize),
2047    pub ott_percent: (f64, f64, f64),
2048    pub fast_vidya: (usize, usize, usize),
2049    pub slow_vidya: (usize, usize, usize),
2050    pub correcting_constant: (f64, f64, f64),
2051    pub ma_types: Vec<String>,
2052}
2053
2054#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2055#[derive(Serialize, Deserialize)]
2056pub struct OttoBatchJsOutput {
2057    pub values: Vec<f64>,
2058    pub combos: Vec<OttoParams>,
2059    pub rows: usize,
2060    pub cols: usize,
2061    pub rows_per_combo: usize,
2062}
2063
2064#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2065#[wasm_bindgen(js_name = otto_batch)]
2066pub fn otto_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2067    let cfg: OttoBatchConfig = serde_wasm_bindgen::from_value(config)
2068        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2069
2070    let sweep = OttoBatchRange {
2071        ott_period: cfg.ott_period,
2072        ott_percent: cfg.ott_percent,
2073        fast_vidya: cfg.fast_vidya,
2074        slow_vidya: cfg.slow_vidya,
2075        correcting_constant: cfg.correcting_constant,
2076        ma_types: cfg.ma_types,
2077    };
2078
2079    let out = otto_batch_with_kernel(data, &sweep, detect_best_kernel())
2080        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2081
2082    let mut values = Vec::with_capacity(out.rows * out.cols * 2);
2083    for r in 0..out.rows {
2084        let base = r * out.cols;
2085        values.extend_from_slice(&out.hott[base..base + out.cols]);
2086        values.extend_from_slice(&out.lott[base..base + out.cols]);
2087    }
2088
2089    let js = OttoBatchJsOutput {
2090        values,
2091        combos: out.combos,
2092        rows: out.rows * 2,
2093        cols: out.cols,
2094        rows_per_combo: 2,
2095    };
2096    serde_wasm_bindgen::to_value(&js)
2097        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2098}
2099
2100#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2101#[wasm_bindgen]
2102pub fn otto_alloc(len: usize) -> *mut f64 {
2103    let mut v = Vec::<f64>::with_capacity(len);
2104    let p = v.as_mut_ptr();
2105    std::mem::forget(v);
2106    p
2107}
2108
2109#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2110#[wasm_bindgen]
2111pub fn otto_free(ptr: *mut f64, len: usize) {
2112    unsafe {
2113        let _ = Vec::from_raw_parts(ptr, len, len);
2114    }
2115}
2116
2117#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2118#[wasm_bindgen]
2119pub fn otto_into(
2120    in_ptr: *const f64,
2121    hott_ptr: *mut f64,
2122    lott_ptr: *mut f64,
2123    len: usize,
2124    ott_period: usize,
2125    ott_percent: f64,
2126    fast_vidya_length: usize,
2127    slow_vidya_length: usize,
2128    correcting_constant: f64,
2129    ma_type: &str,
2130) -> Result<(), JsValue> {
2131    if in_ptr.is_null() || hott_ptr.is_null() || lott_ptr.is_null() {
2132        return Err(JsValue::from_str("null pointer passed to otto_into"));
2133    }
2134    unsafe {
2135        let data = std::slice::from_raw_parts(in_ptr, len);
2136        let mut hott_tmp;
2137        let mut lott_tmp;
2138
2139        let alias_h = in_ptr == hott_ptr || hott_ptr == lott_ptr;
2140        let alias_l = in_ptr == lott_ptr || hott_ptr == lott_ptr;
2141
2142        let (h_dst, l_dst): (&mut [f64], &mut [f64]) = if alias_h || alias_l {
2143            hott_tmp = vec![f64::NAN; len];
2144            lott_tmp = vec![f64::NAN; len];
2145            (&mut hott_tmp, &mut lott_tmp)
2146        } else {
2147            (
2148                std::slice::from_raw_parts_mut(hott_ptr, len),
2149                std::slice::from_raw_parts_mut(lott_ptr, len),
2150            )
2151        };
2152
2153        let params = OttoParams {
2154            ott_period: Some(ott_period),
2155            ott_percent: Some(ott_percent),
2156            fast_vidya_length: Some(fast_vidya_length),
2157            slow_vidya_length: Some(slow_vidya_length),
2158            correcting_constant: Some(correcting_constant),
2159            ma_type: Some(ma_type.to_string()),
2160        };
2161        let input = OttoInput::from_slice(data, params);
2162
2163        otto_into_slices(h_dst, l_dst, &input, detect_best_kernel())
2164            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2165
2166        if alias_h || alias_l {
2167            std::slice::from_raw_parts_mut(hott_ptr, len).copy_from_slice(h_dst);
2168            std::slice::from_raw_parts_mut(lott_ptr, len).copy_from_slice(l_dst);
2169        }
2170        Ok(())
2171    }
2172}
2173
2174#[cfg(all(feature = "python", feature = "cuda"))]
2175#[pyfunction(name = "otto_cuda_batch_dev")]
2176#[pyo3(signature = (data_f32, ott_period_range, ott_percent_range=(0.6,0.6,0.0), fast_vidya_range=(10,10,0), slow_vidya_range=(25,25,0), correcting_constant_range=(100000.0,100000.0,0.0), ma_types=vec!["VAR".to_string()], device_id=0))]
2177pub fn otto_cuda_batch_dev_py(
2178    py: Python<'_>,
2179    data_f32: numpy::PyReadonlyArray1<'_, f32>,
2180    ott_period_range: (usize, usize, usize),
2181    ott_percent_range: (f64, f64, f64),
2182    fast_vidya_range: (usize, usize, usize),
2183    slow_vidya_range: (usize, usize, usize),
2184    correcting_constant_range: (f64, f64, f64),
2185    ma_types: Vec<String>,
2186    device_id: usize,
2187) -> PyResult<(DeviceArrayF32Py, DeviceArrayF32Py)> {
2188    use crate::cuda::cuda_available;
2189    if !cuda_available() {
2190        return Err(PyValueError::new_err("CUDA not available"));
2191    }
2192    let slice = data_f32.as_slice()?;
2193    let sweep = OttoBatchRange {
2194        ott_period: ott_period_range,
2195        ott_percent: ott_percent_range,
2196        fast_vidya: fast_vidya_range,
2197        slow_vidya: slow_vidya_range,
2198        correcting_constant: correcting_constant_range,
2199        ma_types,
2200    };
2201    let (hott, lott) = py.allow_threads(|| {
2202        let cuda = crate::cuda::moving_averages::CudaOtto::new(device_id)
2203            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2204        cuda.otto_batch_dev(slice, &sweep)
2205            .map(|(h, l, _)| (h, l))
2206            .map_err(|e| PyValueError::new_err(e.to_string()))
2207    })?;
2208    let hott_dev = make_device_array_py(device_id, hott)?;
2209    let lott_dev = make_device_array_py(device_id, lott)?;
2210    Ok((hott_dev, lott_dev))
2211}
2212
2213#[cfg(all(feature = "python", feature = "cuda"))]
2214#[pyfunction(name = "otto_cuda_many_series_one_param_dev")]
2215#[pyo3(signature = (prices_tm_f32, cols, rows, ott_period=2, ott_percent=0.6, fast_vidya_length=10, slow_vidya_length=25, correcting_constant=100000.0, _ma_type="VAR", device_id=0))]
2216pub fn otto_cuda_many_series_one_param_dev_py(
2217    py: Python<'_>,
2218    prices_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
2219    cols: usize,
2220    rows: usize,
2221    ott_period: usize,
2222    ott_percent: f64,
2223    fast_vidya_length: usize,
2224    slow_vidya_length: usize,
2225    correcting_constant: f64,
2226    _ma_type: &str,
2227    device_id: usize,
2228) -> PyResult<(DeviceArrayF32Py, DeviceArrayF32Py)> {
2229    use crate::cuda::cuda_available;
2230    if !cuda_available() {
2231        return Err(PyValueError::new_err("CUDA not available"));
2232    }
2233    let prices = prices_tm_f32.as_slice()?;
2234    let params = OttoParams {
2235        ott_period: Some(ott_period),
2236        ott_percent: Some(ott_percent),
2237        fast_vidya_length: Some(fast_vidya_length),
2238        slow_vidya_length: Some(slow_vidya_length),
2239        correcting_constant: Some(correcting_constant),
2240        ma_type: Some("VAR".to_string()),
2241    };
2242    let (hott, lott) = py.allow_threads(|| {
2243        let cuda = crate::cuda::moving_averages::CudaOtto::new(device_id)
2244            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2245        cuda.otto_many_series_one_param_time_major_dev(prices, cols, rows, &params)
2246            .map_err(|e| PyValueError::new_err(e.to_string()))
2247    })?;
2248    let hott_dev = make_device_array_py(device_id, hott)?;
2249    let lott_dev = make_device_array_py(device_id, lott)?;
2250    Ok((hott_dev, lott_dev))
2251}
2252
2253#[cfg(test)]
2254mod tests {
2255    use super::*;
2256    use crate::skip_if_unsupported;
2257    use crate::utilities::data_loader::read_candles_from_csv;
2258
2259    fn generate_otto_test_data(n: usize) -> Vec<f64> {
2260        let mut data = Vec::with_capacity(n);
2261        for i in 0..n {
2262            data.push(0.612 - (i as f64 * 0.00001));
2263        }
2264        data
2265    }
2266
2267    fn check_otto_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2268        skip_if_unsupported!(kernel, test_name);
2269
2270        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2271        let candles = read_candles_from_csv(file_path)?;
2272
2273        let params = OttoParams {
2274            ott_period: None,
2275            ott_percent: Some(0.8),
2276            fast_vidya_length: None,
2277            slow_vidya_length: Some(20),
2278            correcting_constant: None,
2279            ma_type: None,
2280        };
2281
2282        let input = OttoInput::from_candles(&candles, "close", params);
2283        let output = otto_with_kernel(&input, kernel)?;
2284
2285        assert_eq!(output.hott.len(), candles.close.len());
2286        assert_eq!(output.lott.len(), candles.close.len());
2287
2288        Ok(())
2289    }
2290
2291    fn check_otto_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2292        skip_if_unsupported!(kernel, test_name);
2293
2294        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2295        let candles = read_candles_from_csv(file_path)?;
2296
2297        let params = OttoParams::default();
2298        let input = OttoInput::from_candles(&candles, "close", params);
2299        let result = otto_with_kernel(&input, kernel)?;
2300
2301        let expected_hott = [
2302            0.6137310801679211,
2303            0.6136758137211143,
2304            0.6135129389965592,
2305            0.6133345015018311,
2306            0.6130191362868016,
2307        ];
2308        let expected_lott = [
2309            0.6118478692473065,
2310            0.6118237221582352,
2311            0.6116076875101266,
2312            0.6114220222840161,
2313            0.6110393343841534,
2314        ];
2315
2316        let start = result.hott.len().saturating_sub(5);
2317        for (i, &expected) in expected_hott.iter().enumerate() {
2318            let actual = result.hott[start + i];
2319            let diff = (actual - expected).abs();
2320            assert!(
2321                diff < 1e-8,
2322                "[{}] OTTO HOTT {:?} mismatch at idx {}: got {}, expected {}",
2323                test_name,
2324                kernel,
2325                i,
2326                actual,
2327                expected
2328            );
2329        }
2330
2331        for (i, &expected) in expected_lott.iter().enumerate() {
2332            let actual = result.lott[start + i];
2333            let diff = (actual - expected).abs();
2334            assert!(
2335                diff < 1e-8,
2336                "[{}] OTTO LOTT {:?} mismatch at idx {}: got {}, expected {}",
2337                test_name,
2338                kernel,
2339                i,
2340                actual,
2341                expected
2342            );
2343        }
2344
2345        Ok(())
2346    }
2347
2348    fn check_otto_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2349        skip_if_unsupported!(kernel, test_name);
2350
2351        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2352        let candles = read_candles_from_csv(file_path)?;
2353
2354        let input = OttoInput::with_default_candles(&candles);
2355        let output = otto_with_kernel(&input, kernel)?;
2356
2357        assert_eq!(output.hott.len(), candles.close.len());
2358        assert_eq!(output.lott.len(), candles.close.len());
2359
2360        Ok(())
2361    }
2362
2363    fn check_otto_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2364        skip_if_unsupported!(kernel, test_name);
2365
2366        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2367        let candles = read_candles_from_csv(file_path)?;
2368
2369        let params = OttoParams {
2370            ott_period: Some(0),
2371            ..Default::default()
2372        };
2373
2374        let input = OttoInput::from_candles(&candles, "close", params);
2375        let result = otto_with_kernel(&input, kernel);
2376
2377        assert!(
2378            result.is_err(),
2379            "[{}] Expected error for zero period",
2380            test_name
2381        );
2382
2383        Ok(())
2384    }
2385
2386    fn check_otto_period_exceeds_length(
2387        test_name: &str,
2388        kernel: Kernel,
2389    ) -> Result<(), Box<dyn Error>> {
2390        skip_if_unsupported!(kernel, test_name);
2391
2392        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2393        let candles = read_candles_from_csv(file_path)?;
2394
2395        let small_data = &candles.close[0..3];
2396
2397        let params = OttoParams {
2398            ott_period: Some(10),
2399            ..Default::default()
2400        };
2401
2402        let input = OttoInput::from_slice(small_data, params);
2403        let result = otto_with_kernel(&input, kernel);
2404
2405        assert!(
2406            result.is_err(),
2407            "[{}] Expected error when period exceeds length",
2408            test_name
2409        );
2410
2411        Ok(())
2412    }
2413
2414    fn check_otto_very_small_dataset(
2415        test_name: &str,
2416        kernel: Kernel,
2417    ) -> Result<(), Box<dyn Error>> {
2418        skip_if_unsupported!(kernel, test_name);
2419
2420        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2421        let candles = read_candles_from_csv(file_path)?;
2422
2423        let small_data = &candles.close[0..15];
2424
2425        let params = OttoParams {
2426            ott_period: Some(1),
2427            ott_percent: Some(0.5),
2428            fast_vidya_length: Some(1),
2429            slow_vidya_length: Some(2),
2430            correcting_constant: Some(1.0),
2431            ma_type: Some("SMA".to_string()),
2432        };
2433
2434        let input = OttoInput::from_slice(small_data, params);
2435        let result = otto_with_kernel(&input, kernel);
2436
2437        assert!(
2438            result.is_ok(),
2439            "[{}] Should handle very small dataset: {:?}",
2440            test_name,
2441            result
2442        );
2443
2444        Ok(())
2445    }
2446
2447    fn check_otto_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2448        skip_if_unsupported!(kernel, test_name);
2449
2450        let data: Vec<f64> = vec![];
2451        let params = OttoParams::default();
2452
2453        let input = OttoInput::from_slice(&data, params);
2454        let result = otto_with_kernel(&input, kernel);
2455
2456        assert!(
2457            result.is_err(),
2458            "[{}] Expected error for empty input",
2459            test_name
2460        );
2461
2462        Ok(())
2463    }
2464
2465    fn check_otto_invalid_ma_type(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2466        skip_if_unsupported!(kernel, test_name);
2467
2468        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2469        let candles = read_candles_from_csv(file_path)?;
2470
2471        let params = OttoParams {
2472            ma_type: Some("INVALID_MA".to_string()),
2473            ..Default::default()
2474        };
2475
2476        let input = OttoInput::from_candles(&candles, "close", params);
2477        let result = otto_with_kernel(&input, kernel);
2478
2479        assert!(
2480            result.is_err(),
2481            "[{}] Expected error for invalid MA type",
2482            test_name
2483        );
2484
2485        Ok(())
2486    }
2487
2488    fn check_otto_all_ma_types(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2489        skip_if_unsupported!(kernel, test_name);
2490
2491        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2492        let candles = read_candles_from_csv(file_path)?;
2493
2494        let ma_types = [
2495            "SMA", "EMA", "WMA", "DEMA", "TMA", "VAR", "ZLEMA", "TSF", "HULL",
2496        ];
2497
2498        for ma_type in &ma_types {
2499            let params = OttoParams {
2500                ma_type: Some(ma_type.to_string()),
2501                ..Default::default()
2502            };
2503
2504            let input = OttoInput::from_candles(&candles, "close", params);
2505            let result = otto_with_kernel(&input, kernel)?;
2506
2507            assert_eq!(
2508                result.hott.len(),
2509                candles.close.len(),
2510                "[{}] MA type {} output length mismatch",
2511                test_name,
2512                ma_type
2513            );
2514        }
2515
2516        Ok(())
2517    }
2518
2519    fn check_otto_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2520        skip_if_unsupported!(kernel, test_name);
2521
2522        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2523        let candles = read_candles_from_csv(file_path)?;
2524
2525        let params = OttoParams::default();
2526        let input = OttoInput::from_candles(&candles, "close", params);
2527
2528        let result1 = otto_with_kernel(&input, kernel)?;
2529        let result2 = otto_with_kernel(&input, kernel)?;
2530
2531        for i in 0..result1.hott.len() {
2532            if result1.hott[i].is_finite() && result2.hott[i].is_finite() {
2533                assert!(
2534                    (result1.hott[i] - result2.hott[i]).abs() < 1e-10,
2535                    "[{}] Reinput produced different HOTT at index {}",
2536                    test_name,
2537                    i
2538                );
2539            }
2540            if result1.lott[i].is_finite() && result2.lott[i].is_finite() {
2541                assert!(
2542                    (result1.lott[i] - result2.lott[i]).abs() < 1e-10,
2543                    "[{}] Reinput produced different LOTT at index {}",
2544                    test_name,
2545                    i
2546                );
2547            }
2548        }
2549
2550        Ok(())
2551    }
2552
2553    fn check_otto_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2554        skip_if_unsupported!(kernel, test_name);
2555
2556        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2557        let candles = read_candles_from_csv(file_path)?;
2558
2559        let mut data = candles.close.clone();
2560
2561        data[100] = f64::NAN;
2562        data[150] = f64::NAN;
2563        data[200] = f64::NAN;
2564
2565        let params = OttoParams::default();
2566        let input = OttoInput::from_slice(&data, params);
2567        let result = otto_with_kernel(&input, kernel)?;
2568
2569        assert_eq!(result.hott.len(), data.len());
2570        assert_eq!(result.lott.len(), data.len());
2571
2572        let valid_count = result
2573            .hott
2574            .iter()
2575            .skip(250)
2576            .filter(|&&x| x.is_finite())
2577            .count();
2578        assert!(
2579            valid_count > 0,
2580            "[{}] Should produce some valid values despite NaNs",
2581            test_name
2582        );
2583
2584        Ok(())
2585    }
2586
2587    fn check_otto_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2588        skip_if_unsupported!(kernel, test_name);
2589
2590        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2591        let candles = read_candles_from_csv(file_path)?;
2592
2593        let params = OttoParams::default();
2594
2595        let input = OttoInput::from_candles(&candles, "close", params.clone());
2596        let batch_output = otto_with_kernel(&input, kernel)?;
2597
2598        let mut stream = OttoStream::try_new(params)?;
2599        let mut stream_hott = Vec::new();
2600        let mut stream_lott = Vec::new();
2601
2602        for &value in &candles.close {
2603            match stream.update(value) {
2604                Some((h, l)) => {
2605                    stream_hott.push(h);
2606                    stream_lott.push(l);
2607                }
2608                None => {
2609                    stream_hott.push(f64::NAN);
2610                    stream_lott.push(f64::NAN);
2611                }
2612            }
2613        }
2614
2615        let start = stream_hott.len() - 10;
2616        for i in start..stream_hott.len() {
2617            if stream_hott[i].is_finite() && batch_output.hott[i].is_finite() {
2618                let diff = (stream_hott[i] - batch_output.hott[i]).abs();
2619
2620                assert!(
2621                    diff < 0.2,
2622                    "[{}] Stream HOTT mismatch at {}: {} vs {} (diff: {})",
2623                    test_name,
2624                    i,
2625                    stream_hott[i],
2626                    batch_output.hott[i],
2627                    diff
2628                );
2629            }
2630        }
2631
2632        Ok(())
2633    }
2634
2635    fn check_otto_builder(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2636        skip_if_unsupported!(kernel, test_name);
2637
2638        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2639        let candles = read_candles_from_csv(file_path)?;
2640
2641        let output = OttoBuilder::new()
2642            .ott_period(3)
2643            .ott_percent(0.8)
2644            .fast_vidya_length(12)
2645            .slow_vidya_length(30)
2646            .correcting_constant(50000.0)
2647            .ma_type("EMA")
2648            .kernel(kernel)
2649            .apply(&candles)?;
2650
2651        assert_eq!(output.hott.len(), candles.close.len());
2652        assert_eq!(output.lott.len(), candles.close.len());
2653
2654        Ok(())
2655    }
2656
2657    macro_rules! generate_all_otto_tests {
2658        ($($test_fn:ident),*) => {
2659            paste::paste! {
2660                $(
2661                    #[test]
2662                    fn [<$test_fn _scalar_f64>]() {
2663                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2664                    }
2665                )*
2666                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2667                $(
2668                    #[test]
2669                    fn [<$test_fn _avx2_f64>]() {
2670                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2671                    }
2672                    #[test]
2673                    fn [<$test_fn _avx512_f64>]() {
2674                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2675                    }
2676                )*
2677                #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
2678                $(
2679                    #[test]
2680                    fn [<$test_fn _simd128_f64>]() {
2681                        let _ = $test_fn(stringify!([<$test_fn _simd128_f64>]), Kernel::Scalar);
2682                    }
2683                )*
2684            }
2685        }
2686    }
2687
2688    generate_all_otto_tests!(
2689        check_otto_partial_params,
2690        check_otto_accuracy,
2691        check_otto_default_candles,
2692        check_otto_zero_period,
2693        check_otto_period_exceeds_length,
2694        check_otto_very_small_dataset,
2695        check_otto_empty_input,
2696        check_otto_invalid_ma_type,
2697        check_otto_all_ma_types,
2698        check_otto_reinput,
2699        check_otto_nan_handling,
2700        check_otto_streaming,
2701        check_otto_builder
2702    );
2703
2704    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2705        skip_if_unsupported!(kernel, test);
2706
2707        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2708        let candles = read_candles_from_csv(file_path)?;
2709
2710        let output = OttoBatchBuilder::new()
2711            .kernel(kernel)
2712            .apply_candles(&candles, "close")?;
2713
2714        let def = OttoParams::default();
2715        let default_idx = output
2716            .combos
2717            .iter()
2718            .position(|c| {
2719                c.ott_period == def.ott_period
2720                    && c.ott_percent == def.ott_percent
2721                    && c.fast_vidya_length == def.fast_vidya_length
2722                    && c.slow_vidya_length == def.slow_vidya_length
2723                    && c.correcting_constant == def.correcting_constant
2724                    && c.ma_type == def.ma_type
2725            })
2726            .expect("default params not found in batch output");
2727
2728        let hott_row = &output.hott[default_idx * output.cols..(default_idx + 1) * output.cols];
2729        let lott_row = &output.lott[default_idx * output.cols..(default_idx + 1) * output.cols];
2730
2731        assert_eq!(hott_row.len(), candles.close.len());
2732        assert_eq!(lott_row.len(), candles.close.len());
2733
2734        let non_nan_hott = hott_row.iter().filter(|&&x| !x.is_nan()).count();
2735        let non_nan_lott = lott_row.iter().filter(|&&x| !x.is_nan()).count();
2736        assert!(
2737            non_nan_hott > 0,
2738            "[{}] Expected some non-NaN HOTT values",
2739            test
2740        );
2741        assert!(
2742            non_nan_lott > 0,
2743            "[{}] Expected some non-NaN LOTT values",
2744            test
2745        );
2746
2747        Ok(())
2748    }
2749
2750    fn check_batch_sweep(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2751        skip_if_unsupported!(kernel, test);
2752
2753        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2754        let candles = read_candles_from_csv(file_path)?;
2755
2756        let output = OttoBatchBuilder::new()
2757            .kernel(kernel)
2758            .ott_period_range(2, 4, 1)
2759            .ott_percent_range(0.5, 0.7, 0.1)
2760            .fast_vidya_range(10, 12, 1)
2761            .slow_vidya_range(20, 22, 1)
2762            .correcting_constant_range(100000.0, 100000.0, 0.0)
2763            .ma_types(vec!["VAR".into(), "EMA".into()])
2764            .apply_candles(&candles, "close")?;
2765
2766        let expected_combos = 3 * 3 * 3 * 3 * 1 * 2;
2767        assert_eq!(
2768            output.combos.len(),
2769            expected_combos,
2770            "[{}] Expected {} combos",
2771            test,
2772            expected_combos
2773        );
2774        assert_eq!(output.rows, expected_combos);
2775        assert_eq!(output.cols, candles.close.len());
2776
2777        Ok(())
2778    }
2779
2780    #[cfg(debug_assertions)]
2781    fn check_no_poison_single(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2782        use crate::utilities::data_loader::read_candles_from_csv;
2783        skip_if_unsupported!(kernel, test);
2784        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2785        let c = read_candles_from_csv(file)?;
2786        let out = OttoBuilder::new().kernel(kernel).apply(&c)?;
2787        for &v in out.hott.iter().chain(out.lott.iter()) {
2788            if v.is_nan() {
2789                continue;
2790            }
2791            let b = v.to_bits();
2792            assert_ne!(
2793                b, 0x1111_1111_1111_1111,
2794                "[{test}] alloc_with_nan_prefix poison seen"
2795            );
2796            assert_ne!(
2797                b, 0x2222_2222_2222_2222,
2798                "[{test}] init_matrix_prefixes poison seen"
2799            );
2800            assert_ne!(
2801                b, 0x3333_3333_3333_3333,
2802                "[{test}] make_uninit_matrix poison seen"
2803            );
2804        }
2805        Ok(())
2806    }
2807
2808    #[cfg(debug_assertions)]
2809    fn check_no_poison_batch(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2810        skip_if_unsupported!(kernel, test);
2811        let data = (0..300)
2812            .map(|i| (i as f64).cos() * 2.0 + 10.0)
2813            .collect::<Vec<_>>();
2814        let out = OttoBatchBuilder::new().kernel(kernel).apply_slice(&data)?;
2815        for &v in out.hott.iter().chain(out.lott.iter()) {
2816            if v.is_nan() {
2817                continue;
2818            }
2819            let b = v.to_bits();
2820            assert_ne!(
2821                b, 0x1111_1111_1111_1111,
2822                "[{}] alloc_with_nan_prefix poison seen",
2823                test
2824            );
2825            assert_ne!(
2826                b, 0x2222_2222_2222_2222,
2827                "[{}] init_matrix_prefixes poison seen",
2828                test
2829            );
2830            assert_ne!(
2831                b, 0x3333_3333_3333_3333,
2832                "[{}] make_uninit_matrix poison seen",
2833                test
2834            );
2835        }
2836        Ok(())
2837    }
2838
2839    macro_rules! gen_batch_tests {
2840        ($fn_name:ident) => {
2841            paste::paste! {
2842                #[test] fn [<$fn_name _scalar>]()      {
2843                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2844                }
2845                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
2846                #[test] fn [<$fn_name _avx2>]()        {
2847                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2848                }
2849                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
2850                #[test] fn [<$fn_name _avx512>]()      {
2851                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2852                }
2853                #[test] fn [<$fn_name _auto_detect>]() {
2854                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2855                }
2856            }
2857        };
2858    }
2859
2860    gen_batch_tests!(check_batch_default_row);
2861    gen_batch_tests!(check_batch_sweep);
2862    #[cfg(debug_assertions)]
2863    gen_batch_tests!(check_no_poison_batch);
2864
2865    #[cfg(debug_assertions)]
2866    generate_all_otto_tests!(check_no_poison_single);
2867
2868    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2869    #[test]
2870    fn test_otto_into_matches_api() -> Result<(), Box<dyn Error>> {
2871        let n = 512usize;
2872        let data: Vec<f64> = (0..n)
2873            .map(|i| ((i as f64) * 0.013).sin() * 0.5 + 1.0)
2874            .collect();
2875
2876        let input = super::OttoInput::from_slice(&data, super::OttoParams::default());
2877
2878        let baseline = super::otto(&input)?;
2879
2880        let mut hott_out = vec![0.0f64; n];
2881        let mut lott_out = vec![0.0f64; n];
2882
2883        super::otto_into(&input, &mut hott_out, &mut lott_out)?;
2884
2885        assert_eq!(baseline.hott.len(), n);
2886        assert_eq!(baseline.lott.len(), n);
2887        assert_eq!(hott_out.len(), n);
2888        assert_eq!(lott_out.len(), n);
2889
2890        fn eq_or_both_nan(a: f64, b: f64) -> bool {
2891            (a.is_nan() && b.is_nan()) || (a == b)
2892        }
2893
2894        for i in 0..n {
2895            assert!(
2896                eq_or_both_nan(baseline.hott[i], hott_out[i]),
2897                "HOTT mismatch at {i}: got {}, expected {}",
2898                hott_out[i],
2899                baseline.hott[i]
2900            );
2901            assert!(
2902                eq_or_both_nan(baseline.lott[i], lott_out[i]),
2903                "LOTT mismatch at {i}: got {}, expected {}",
2904                lott_out[i],
2905                baseline.lott[i]
2906            );
2907        }
2908
2909        Ok(())
2910    }
2911}