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, ¶ms)
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}