Skip to main content

vector_ta/indicators/
insync_index.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{alloc_with_nan_prefix, make_uninit_matrix};
18#[cfg(feature = "python")]
19use crate::utilities::kernel_validation::validate_kernel;
20
21use std::collections::VecDeque;
22use std::mem::{ManuallyDrop, MaybeUninit};
23
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use thiserror::Error;
27
28const DEFAULT_EMO_DIVISOR: usize = 10_000;
29const DEFAULT_EMO_LENGTH: usize = 14;
30const DEFAULT_FAST_LENGTH: usize = 12;
31const DEFAULT_SLOW_LENGTH: usize = 26;
32const DEFAULT_MFI_LENGTH: usize = 20;
33const DEFAULT_BB_LENGTH: usize = 20;
34const DEFAULT_BB_MULTIPLIER: f64 = 2.0;
35const DEFAULT_CCI_LENGTH: usize = 14;
36const DEFAULT_DPO_LENGTH: usize = 18;
37const DEFAULT_ROC_LENGTH: usize = 10;
38const DEFAULT_RSI_LENGTH: usize = 14;
39const DEFAULT_STOCH_LENGTH: usize = 14;
40const DEFAULT_STOCH_D_LENGTH: usize = 3;
41const DEFAULT_STOCH_K_LENGTH: usize = 1;
42const DEFAULT_SMA_LENGTH: usize = 10;
43const DPO_DELAY: usize = 10;
44
45#[derive(Debug, Clone)]
46pub enum InsyncIndexData<'a> {
47    Candles {
48        candles: &'a Candles,
49    },
50    Slices {
51        high: &'a [f64],
52        low: &'a [f64],
53        close: &'a [f64],
54        volume: &'a [f64],
55    },
56}
57
58#[derive(Debug, Clone)]
59pub struct InsyncIndexOutput {
60    pub values: Vec<f64>,
61}
62
63#[derive(Debug, Clone)]
64#[cfg_attr(
65    all(target_arch = "wasm32", feature = "wasm"),
66    derive(Serialize, Deserialize)
67)]
68pub struct InsyncIndexParams {
69    pub emo_divisor: Option<usize>,
70    pub emo_length: Option<usize>,
71    pub fast_length: Option<usize>,
72    pub slow_length: Option<usize>,
73    pub mfi_length: Option<usize>,
74    pub bb_length: Option<usize>,
75    pub bb_multiplier: Option<f64>,
76    pub cci_length: Option<usize>,
77    pub dpo_length: Option<usize>,
78    pub roc_length: Option<usize>,
79    pub rsi_length: Option<usize>,
80    pub stoch_length: Option<usize>,
81    pub stoch_d_length: Option<usize>,
82    pub stoch_k_length: Option<usize>,
83    pub sma_length: Option<usize>,
84}
85
86impl Default for InsyncIndexParams {
87    fn default() -> Self {
88        Self {
89            emo_divisor: Some(DEFAULT_EMO_DIVISOR),
90            emo_length: Some(DEFAULT_EMO_LENGTH),
91            fast_length: Some(DEFAULT_FAST_LENGTH),
92            slow_length: Some(DEFAULT_SLOW_LENGTH),
93            mfi_length: Some(DEFAULT_MFI_LENGTH),
94            bb_length: Some(DEFAULT_BB_LENGTH),
95            bb_multiplier: Some(DEFAULT_BB_MULTIPLIER),
96            cci_length: Some(DEFAULT_CCI_LENGTH),
97            dpo_length: Some(DEFAULT_DPO_LENGTH),
98            roc_length: Some(DEFAULT_ROC_LENGTH),
99            rsi_length: Some(DEFAULT_RSI_LENGTH),
100            stoch_length: Some(DEFAULT_STOCH_LENGTH),
101            stoch_d_length: Some(DEFAULT_STOCH_D_LENGTH),
102            stoch_k_length: Some(DEFAULT_STOCH_K_LENGTH),
103            sma_length: Some(DEFAULT_SMA_LENGTH),
104        }
105    }
106}
107
108#[derive(Debug, Clone)]
109pub struct InsyncIndexInput<'a> {
110    pub data: InsyncIndexData<'a>,
111    pub params: InsyncIndexParams,
112}
113
114impl<'a> InsyncIndexInput<'a> {
115    #[inline]
116    pub fn from_candles(candles: &'a Candles, params: InsyncIndexParams) -> Self {
117        Self {
118            data: InsyncIndexData::Candles { candles },
119            params,
120        }
121    }
122
123    #[inline]
124    pub fn from_slices(
125        high: &'a [f64],
126        low: &'a [f64],
127        close: &'a [f64],
128        volume: &'a [f64],
129        params: InsyncIndexParams,
130    ) -> Self {
131        Self {
132            data: InsyncIndexData::Slices {
133                high,
134                low,
135                close,
136                volume,
137            },
138            params,
139        }
140    }
141
142    #[inline]
143    pub fn with_default_candles(candles: &'a Candles) -> Self {
144        Self::from_candles(candles, InsyncIndexParams::default())
145    }
146
147    #[inline]
148    pub fn as_refs(&'a self) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64]) {
149        match &self.data {
150            InsyncIndexData::Candles { candles } => (
151                candles.high.as_slice(),
152                candles.low.as_slice(),
153                candles.close.as_slice(),
154                candles.volume.as_slice(),
155            ),
156            InsyncIndexData::Slices {
157                high,
158                low,
159                close,
160                volume,
161            } => (*high, *low, *close, *volume),
162        }
163    }
164}
165
166#[derive(Copy, Clone, Debug)]
167struct ResolvedParams {
168    emo_divisor: usize,
169    emo_length: usize,
170    fast_length: usize,
171    slow_length: usize,
172    mfi_length: usize,
173    bb_length: usize,
174    bb_multiplier: f64,
175    cci_length: usize,
176    dpo_length: usize,
177    roc_length: usize,
178    rsi_length: usize,
179    stoch_length: usize,
180    stoch_d_length: usize,
181    stoch_k_length: usize,
182    sma_length: usize,
183}
184
185#[derive(Clone, Debug)]
186pub struct InsyncIndexBuilder {
187    params: InsyncIndexParams,
188    kernel: Kernel,
189}
190
191impl Default for InsyncIndexBuilder {
192    fn default() -> Self {
193        Self {
194            params: InsyncIndexParams::default(),
195            kernel: Kernel::Auto,
196        }
197    }
198}
199
200impl InsyncIndexBuilder {
201    #[inline(always)]
202    pub fn new() -> Self {
203        Self::default()
204    }
205
206    #[inline(always)]
207    pub fn emo_divisor(mut self, value: usize) -> Self {
208        self.params.emo_divisor = Some(value);
209        self
210    }
211
212    #[inline(always)]
213    pub fn emo_length(mut self, value: usize) -> Self {
214        self.params.emo_length = Some(value);
215        self
216    }
217
218    #[inline(always)]
219    pub fn fast_length(mut self, value: usize) -> Self {
220        self.params.fast_length = Some(value);
221        self
222    }
223
224    #[inline(always)]
225    pub fn slow_length(mut self, value: usize) -> Self {
226        self.params.slow_length = Some(value);
227        self
228    }
229
230    #[inline(always)]
231    pub fn mfi_length(mut self, value: usize) -> Self {
232        self.params.mfi_length = Some(value);
233        self
234    }
235
236    #[inline(always)]
237    pub fn bb_length(mut self, value: usize) -> Self {
238        self.params.bb_length = Some(value);
239        self
240    }
241
242    #[inline(always)]
243    pub fn bb_multiplier(mut self, value: f64) -> Self {
244        self.params.bb_multiplier = Some(value);
245        self
246    }
247
248    #[inline(always)]
249    pub fn cci_length(mut self, value: usize) -> Self {
250        self.params.cci_length = Some(value);
251        self
252    }
253
254    #[inline(always)]
255    pub fn dpo_length(mut self, value: usize) -> Self {
256        self.params.dpo_length = Some(value);
257        self
258    }
259
260    #[inline(always)]
261    pub fn roc_length(mut self, value: usize) -> Self {
262        self.params.roc_length = Some(value);
263        self
264    }
265
266    #[inline(always)]
267    pub fn rsi_length(mut self, value: usize) -> Self {
268        self.params.rsi_length = Some(value);
269        self
270    }
271
272    #[inline(always)]
273    pub fn stoch_length(mut self, value: usize) -> Self {
274        self.params.stoch_length = Some(value);
275        self
276    }
277
278    #[inline(always)]
279    pub fn stoch_d_length(mut self, value: usize) -> Self {
280        self.params.stoch_d_length = Some(value);
281        self
282    }
283
284    #[inline(always)]
285    pub fn stoch_k_length(mut self, value: usize) -> Self {
286        self.params.stoch_k_length = Some(value);
287        self
288    }
289
290    #[inline(always)]
291    pub fn sma_length(mut self, value: usize) -> Self {
292        self.params.sma_length = Some(value);
293        self
294    }
295
296    #[inline(always)]
297    pub fn kernel(mut self, value: Kernel) -> Self {
298        self.kernel = value;
299        self
300    }
301
302    #[inline(always)]
303    pub fn apply(self, candles: &Candles) -> Result<InsyncIndexOutput, InsyncIndexError> {
304        let input = InsyncIndexInput::from_candles(candles, self.params);
305        insync_index_with_kernel(&input, self.kernel)
306    }
307
308    #[inline(always)]
309    pub fn apply_slices(
310        self,
311        high: &[f64],
312        low: &[f64],
313        close: &[f64],
314        volume: &[f64],
315    ) -> Result<InsyncIndexOutput, InsyncIndexError> {
316        let input = InsyncIndexInput::from_slices(high, low, close, volume, self.params);
317        insync_index_with_kernel(&input, self.kernel)
318    }
319
320    #[inline(always)]
321    pub fn into_stream(self) -> Result<InsyncIndexStream, InsyncIndexError> {
322        InsyncIndexStream::try_new(self.params)
323    }
324}
325
326#[derive(Debug, Error)]
327pub enum InsyncIndexError {
328    #[error("insync_index: Empty input data.")]
329    EmptyInputData,
330    #[error("insync_index: Data length mismatch across high, low, close, and volume.")]
331    DataLengthMismatch,
332    #[error("insync_index: All OHLCV values are invalid.")]
333    AllValuesNaN,
334    #[error("insync_index: Invalid parameter {name}={value}.")]
335    InvalidPeriod { name: &'static str, value: usize },
336    #[error("insync_index: Invalid parameter {name}={value}.")]
337    InvalidFloat { name: &'static str, value: f64 },
338    #[error("insync_index: Output length mismatch: expected = {expected}, got = {got}.")]
339    OutputLengthMismatch { expected: usize, got: usize },
340    #[error("insync_index: Invalid range for {name}: start={start}, end={end}, step={step}.")]
341    InvalidRange {
342        name: &'static str,
343        start: String,
344        end: String,
345        step: String,
346    },
347    #[error("insync_index: Invalid kernel for batch: {0:?}.")]
348    InvalidKernelForBatch(Kernel),
349}
350
351#[inline(always)]
352fn resolve_params(params: &InsyncIndexParams) -> Result<ResolvedParams, InsyncIndexError> {
353    let resolved = ResolvedParams {
354        emo_divisor: params.emo_divisor.unwrap_or(DEFAULT_EMO_DIVISOR),
355        emo_length: params.emo_length.unwrap_or(DEFAULT_EMO_LENGTH),
356        fast_length: params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH),
357        slow_length: params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH),
358        mfi_length: params.mfi_length.unwrap_or(DEFAULT_MFI_LENGTH),
359        bb_length: params.bb_length.unwrap_or(DEFAULT_BB_LENGTH),
360        bb_multiplier: params.bb_multiplier.unwrap_or(DEFAULT_BB_MULTIPLIER),
361        cci_length: params.cci_length.unwrap_or(DEFAULT_CCI_LENGTH),
362        dpo_length: params.dpo_length.unwrap_or(DEFAULT_DPO_LENGTH),
363        roc_length: params.roc_length.unwrap_or(DEFAULT_ROC_LENGTH),
364        rsi_length: params.rsi_length.unwrap_or(DEFAULT_RSI_LENGTH),
365        stoch_length: params.stoch_length.unwrap_or(DEFAULT_STOCH_LENGTH),
366        stoch_d_length: params.stoch_d_length.unwrap_or(DEFAULT_STOCH_D_LENGTH),
367        stoch_k_length: params.stoch_k_length.unwrap_or(DEFAULT_STOCH_K_LENGTH),
368        sma_length: params.sma_length.unwrap_or(DEFAULT_SMA_LENGTH),
369    };
370
371    let periods = [
372        ("emo_divisor", resolved.emo_divisor),
373        ("emo_length", resolved.emo_length),
374        ("fast_length", resolved.fast_length),
375        ("slow_length", resolved.slow_length),
376        ("mfi_length", resolved.mfi_length),
377        ("bb_length", resolved.bb_length),
378        ("cci_length", resolved.cci_length),
379        ("dpo_length", resolved.dpo_length),
380        ("roc_length", resolved.roc_length),
381        ("rsi_length", resolved.rsi_length),
382        ("stoch_length", resolved.stoch_length),
383        ("stoch_d_length", resolved.stoch_d_length),
384        ("stoch_k_length", resolved.stoch_k_length),
385        ("sma_length", resolved.sma_length),
386    ];
387    for (name, value) in periods {
388        if value == 0 {
389            return Err(InsyncIndexError::InvalidPeriod { name, value });
390        }
391    }
392
393    if !resolved.bb_multiplier.is_finite() || resolved.bb_multiplier <= 0.0 {
394        return Err(InsyncIndexError::InvalidFloat {
395            name: "bb_multiplier",
396            value: resolved.bb_multiplier,
397        });
398    }
399
400    Ok(resolved)
401}
402
403#[inline(always)]
404fn normalize_kernel(_kernel: Kernel) -> Kernel {
405    Kernel::Scalar
406}
407
408#[inline(always)]
409fn valid_bar(high: f64, low: f64, close: f64, volume: f64) -> bool {
410    high.is_finite()
411        && low.is_finite()
412        && close.is_finite()
413        && volume.is_finite()
414        && volume > 0.0
415        && high >= low
416}
417
418#[inline(always)]
419fn has_any_valid_bar(high: &[f64], low: &[f64], close: &[f64], volume: &[f64]) -> bool {
420    (0..close.len()).any(|i| valid_bar(high[i], low[i], close[i], volume[i]))
421}
422
423#[inline(always)]
424fn prepare_input<'a>(
425    input: &'a InsyncIndexInput<'a>,
426) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64], ResolvedParams), InsyncIndexError> {
427    let (high, low, close, volume) = input.as_refs();
428    let len = close.len();
429    if len == 0 {
430        return Err(InsyncIndexError::EmptyInputData);
431    }
432    if high.len() != len || low.len() != len || volume.len() != len {
433        return Err(InsyncIndexError::DataLengthMismatch);
434    }
435    if !has_any_valid_bar(high, low, close, volume) {
436        return Err(InsyncIndexError::AllValuesNaN);
437    }
438    Ok((high, low, close, volume, resolve_params(&input.params)?))
439}
440
441#[derive(Clone, Debug)]
442struct RollingSmaState {
443    period: usize,
444    buf: Vec<f64>,
445    head: usize,
446    len: usize,
447    sum: f64,
448}
449
450impl RollingSmaState {
451    #[inline(always)]
452    fn new(period: usize) -> Self {
453        Self {
454            period,
455            buf: vec![0.0; period.max(1)],
456            head: 0,
457            len: 0,
458            sum: 0.0,
459        }
460    }
461
462    #[inline(always)]
463    fn reset(&mut self) {
464        self.buf.fill(0.0);
465        self.head = 0;
466        self.len = 0;
467        self.sum = 0.0;
468    }
469
470    #[inline(always)]
471    fn update(&mut self, value: f64) -> Option<f64> {
472        if self.period == 1 {
473            self.buf[0] = value;
474            self.len = 1;
475            self.sum = value;
476            return Some(value);
477        }
478        if self.len < self.period {
479            self.buf[self.len] = value;
480            self.sum += value;
481            self.len += 1;
482            if self.len == self.period {
483                return Some(self.sum / self.period as f64);
484            }
485            return None;
486        }
487        let old = self.buf[self.head];
488        self.buf[self.head] = value;
489        self.sum += value - old;
490        self.head += 1;
491        if self.head == self.period {
492            self.head = 0;
493        }
494        Some(self.sum / self.period as f64)
495    }
496}
497
498#[derive(Clone, Debug)]
499struct RollingVarianceState {
500    period: usize,
501    buf: Vec<f64>,
502    head: usize,
503    len: usize,
504    sum: f64,
505    sumsq: f64,
506}
507
508impl RollingVarianceState {
509    #[inline(always)]
510    fn new(period: usize) -> Self {
511        Self {
512            period,
513            buf: vec![0.0; period.max(1)],
514            head: 0,
515            len: 0,
516            sum: 0.0,
517            sumsq: 0.0,
518        }
519    }
520
521    #[inline(always)]
522    fn reset(&mut self) {
523        self.buf.fill(0.0);
524        self.head = 0;
525        self.len = 0;
526        self.sum = 0.0;
527        self.sumsq = 0.0;
528    }
529
530    #[inline(always)]
531    fn update(&mut self, value: f64) -> Option<(f64, f64)> {
532        if self.len < self.period {
533            self.buf[self.len] = value;
534            self.sum += value;
535            self.sumsq += value * value;
536            self.len += 1;
537            if self.len < self.period {
538                return None;
539            }
540        } else {
541            let old = self.buf[self.head];
542            self.buf[self.head] = value;
543            self.sum += value - old;
544            self.sumsq += value.mul_add(value, -(old * old));
545            self.head += 1;
546            if self.head == self.period {
547                self.head = 0;
548            }
549        }
550        let mean = self.sum / self.period as f64;
551        let variance = (self.sumsq / self.period as f64 - mean * mean).max(0.0);
552        Some((mean, variance.sqrt()))
553    }
554}
555
556#[derive(Clone, Debug)]
557struct RollingCciState {
558    period: usize,
559    buf: Vec<f64>,
560    head: usize,
561    len: usize,
562    sum: f64,
563}
564
565impl RollingCciState {
566    #[inline(always)]
567    fn new(period: usize) -> Self {
568        Self {
569            period,
570            buf: vec![0.0; period.max(1)],
571            head: 0,
572            len: 0,
573            sum: 0.0,
574        }
575    }
576
577    #[inline(always)]
578    fn reset(&mut self) {
579        self.buf.fill(0.0);
580        self.head = 0;
581        self.len = 0;
582        self.sum = 0.0;
583    }
584
585    #[inline(always)]
586    fn update(&mut self, value: f64) -> Option<f64> {
587        if self.len < self.period {
588            self.buf[self.len] = value;
589            self.sum += value;
590            self.len += 1;
591            if self.len < self.period {
592                return None;
593            }
594        } else {
595            let old = self.buf[self.head];
596            self.buf[self.head] = value;
597            self.sum += value - old;
598            self.head += 1;
599            if self.head == self.period {
600                self.head = 0;
601            }
602        }
603        let mean = self.sum / self.period as f64;
604        let mad = self
605            .buf
606            .iter()
607            .take(self.period)
608            .map(|x| (*x - mean).abs())
609            .sum::<f64>()
610            / self.period as f64;
611        if mad == 0.0 || !mad.is_finite() {
612            return None;
613        }
614        Some((value - mean) / (0.015 * mad))
615    }
616}
617
618#[derive(Clone, Debug)]
619struct EmaState {
620    alpha: f64,
621    value: Option<f64>,
622}
623
624impl EmaState {
625    #[inline(always)]
626    fn new(period: usize) -> Self {
627        Self {
628            alpha: 2.0 / (period as f64 + 1.0),
629            value: None,
630        }
631    }
632
633    #[inline(always)]
634    fn reset(&mut self) {
635        self.value = None;
636    }
637
638    #[inline(always)]
639    fn update(&mut self, value: f64) -> f64 {
640        let next = match self.value {
641            Some(prev) => self.alpha.mul_add(value, (1.0 - self.alpha) * prev),
642            None => value,
643        };
644        self.value = Some(next);
645        next
646    }
647}
648
649#[derive(Clone, Debug)]
650struct WilderRsiState {
651    period: usize,
652    prev: Option<f64>,
653    gains: f64,
654    losses: f64,
655    count: usize,
656    avg_gain: Option<f64>,
657    avg_loss: Option<f64>,
658}
659
660impl WilderRsiState {
661    #[inline(always)]
662    fn new(period: usize) -> Self {
663        Self {
664            period,
665            prev: None,
666            gains: 0.0,
667            losses: 0.0,
668            count: 0,
669            avg_gain: None,
670            avg_loss: None,
671        }
672    }
673
674    #[inline(always)]
675    fn reset(&mut self) {
676        self.prev = None;
677        self.gains = 0.0;
678        self.losses = 0.0;
679        self.count = 0;
680        self.avg_gain = None;
681        self.avg_loss = None;
682    }
683
684    #[inline(always)]
685    fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
686        if avg_gain == 0.0 && avg_loss == 0.0 {
687            50.0
688        } else if avg_loss == 0.0 {
689            100.0
690        } else if avg_gain == 0.0 {
691            0.0
692        } else {
693            let rs = avg_gain / avg_loss;
694            100.0 - 100.0 / (1.0 + rs)
695        }
696    }
697
698    #[inline(always)]
699    fn update(&mut self, value: f64) -> Option<f64> {
700        let prev = match self.prev {
701            Some(prev) => prev,
702            None => {
703                self.prev = Some(value);
704                return None;
705            }
706        };
707        let change = value - prev;
708        let gain = change.max(0.0);
709        let loss = (-change).max(0.0);
710        self.prev = Some(value);
711
712        if self.avg_gain.is_none() || self.avg_loss.is_none() {
713            self.gains += gain;
714            self.losses += loss;
715            self.count += 1;
716            if self.count < self.period {
717                return None;
718            }
719            let avg_gain = self.gains / self.period as f64;
720            let avg_loss = self.losses / self.period as f64;
721            self.avg_gain = Some(avg_gain);
722            self.avg_loss = Some(avg_loss);
723            return Some(Self::rsi_from_avgs(avg_gain, avg_loss));
724        }
725
726        let period_f = self.period as f64;
727        let avg_gain = ((self.avg_gain.unwrap_or(0.0) * (period_f - 1.0)) + gain) / period_f;
728        let avg_loss = ((self.avg_loss.unwrap_or(0.0) * (period_f - 1.0)) + loss) / period_f;
729        self.avg_gain = Some(avg_gain);
730        self.avg_loss = Some(avg_loss);
731        Some(Self::rsi_from_avgs(avg_gain, avg_loss))
732    }
733}
734
735#[derive(Clone, Debug)]
736struct RocState {
737    period: usize,
738    buf: Vec<f64>,
739    head: usize,
740    len: usize,
741}
742
743impl RocState {
744    #[inline(always)]
745    fn new(period: usize) -> Self {
746        Self {
747            period,
748            buf: vec![0.0; period.max(1)],
749            head: 0,
750            len: 0,
751        }
752    }
753
754    #[inline(always)]
755    fn reset(&mut self) {
756        self.buf.fill(0.0);
757        self.head = 0;
758        self.len = 0;
759    }
760
761    #[inline(always)]
762    fn update(&mut self, value: f64) -> Option<f64> {
763        if self.len < self.period {
764            self.buf[self.len] = value;
765            self.len += 1;
766            return None;
767        }
768        let prev = self.buf[self.head];
769        self.buf[self.head] = value;
770        self.head += 1;
771        if self.head == self.period {
772            self.head = 0;
773        }
774        if prev == 0.0 || !prev.is_finite() {
775            return None;
776        }
777        Some(100.0 * (value - prev) / prev)
778    }
779}
780
781#[derive(Clone, Debug)]
782struct DpoState {
783    close_sma: RollingSmaState,
784    dpo_sma: RollingSmaState,
785    barsback: usize,
786    sma_history: VecDeque<Option<f64>>,
787    delayed_components: VecDeque<i32>,
788}
789
790impl DpoState {
791    #[inline(always)]
792    fn new(period: usize, sma_length: usize) -> Self {
793        Self {
794            close_sma: RollingSmaState::new(period),
795            dpo_sma: RollingSmaState::new(sma_length),
796            barsback: period / 2 + 1,
797            sma_history: VecDeque::with_capacity(period / 2 + 3),
798            delayed_components: VecDeque::with_capacity(DPO_DELAY + 2),
799        }
800    }
801
802    #[inline(always)]
803    fn reset(&mut self) {
804        self.close_sma.reset();
805        self.dpo_sma.reset();
806        self.sma_history.clear();
807        self.delayed_components.clear();
808    }
809
810    #[inline(always)]
811    fn update(&mut self, value: f64) -> i32 {
812        let sma_now = self.close_sma.update(value);
813        self.sma_history.push_back(sma_now);
814
815        let mut component = 0;
816        if self.sma_history.len() > self.barsback {
817            let past_sma = self.sma_history.pop_front().unwrap_or(None);
818            if let Some(past_sma) = past_sma {
819                let dpo = value - past_sma;
820                if let Some(avg) = self.dpo_sma.update(dpo) {
821                    let diff = dpo - avg;
822                    if diff < 0.0 && avg < 0.0 {
823                        component = -5;
824                    } else if diff > 0.0 && avg > 0.0 {
825                        component = 5;
826                    }
827                }
828            }
829        }
830
831        self.delayed_components.push_back(component);
832        if self.delayed_components.len() <= DPO_DELAY {
833            0
834        } else {
835            self.delayed_components.pop_front().unwrap_or(0)
836        }
837    }
838}
839
840#[derive(Clone, Debug)]
841struct MfiState {
842    period: usize,
843    prev_tp: Option<f64>,
844    pos_buf: Vec<f64>,
845    neg_buf: Vec<f64>,
846    head: usize,
847    len: usize,
848    pos_sum: f64,
849    neg_sum: f64,
850}
851
852impl MfiState {
853    #[inline(always)]
854    fn new(period: usize) -> Self {
855        Self {
856            period,
857            prev_tp: None,
858            pos_buf: vec![0.0; period.max(1)],
859            neg_buf: vec![0.0; period.max(1)],
860            head: 0,
861            len: 0,
862            pos_sum: 0.0,
863            neg_sum: 0.0,
864        }
865    }
866
867    #[inline(always)]
868    fn reset(&mut self) {
869        self.prev_tp = None;
870        self.pos_buf.fill(0.0);
871        self.neg_buf.fill(0.0);
872        self.head = 0;
873        self.len = 0;
874        self.pos_sum = 0.0;
875        self.neg_sum = 0.0;
876    }
877
878    #[inline(always)]
879    fn update(&mut self, tp: f64, volume: f64) -> Option<f64> {
880        let prev_tp = match self.prev_tp {
881            Some(prev) => prev,
882            None => {
883                self.prev_tp = Some(tp);
884                return None;
885            }
886        };
887        let mut pos = 0.0;
888        let mut neg = 0.0;
889        if tp > prev_tp {
890            pos = volume * tp;
891        } else if tp < prev_tp {
892            neg = volume * tp;
893        }
894        self.prev_tp = Some(tp);
895
896        if self.len < self.period {
897            self.pos_buf[self.len] = pos;
898            self.neg_buf[self.len] = neg;
899            self.pos_sum += pos;
900            self.neg_sum += neg;
901            self.len += 1;
902            if self.len < self.period {
903                return None;
904            }
905        } else {
906            let old_pos = self.pos_buf[self.head];
907            let old_neg = self.neg_buf[self.head];
908            self.pos_buf[self.head] = pos;
909            self.neg_buf[self.head] = neg;
910            self.pos_sum += pos - old_pos;
911            self.neg_sum += neg - old_neg;
912            self.head += 1;
913            if self.head == self.period {
914                self.head = 0;
915            }
916        }
917
918        if self.pos_sum == 0.0 && self.neg_sum == 0.0 {
919            return Some(50.0);
920        }
921        if self.neg_sum == 0.0 {
922            return Some(100.0);
923        }
924        if self.pos_sum == 0.0 {
925            return Some(0.0);
926        }
927        let rs = self.pos_sum / self.neg_sum;
928        Some(100.0 - 100.0 / (1.0 + rs))
929    }
930}
931
932#[derive(Clone, Debug)]
933struct StochState {
934    length: usize,
935    index: usize,
936    highs: VecDeque<(usize, f64)>,
937    lows: VecDeque<(usize, f64)>,
938    k_sma: RollingSmaState,
939    d_sma: RollingSmaState,
940}
941
942impl StochState {
943    #[inline(always)]
944    fn new(length: usize, smooth_d: usize, smooth_k: usize) -> Self {
945        Self {
946            length,
947            index: 0,
948            highs: VecDeque::with_capacity(length.max(1)),
949            lows: VecDeque::with_capacity(length.max(1)),
950            k_sma: RollingSmaState::new(smooth_k),
951            d_sma: RollingSmaState::new(smooth_d),
952        }
953    }
954
955    #[inline(always)]
956    fn reset(&mut self) {
957        self.index = 0;
958        self.highs.clear();
959        self.lows.clear();
960        self.k_sma.reset();
961        self.d_sma.reset();
962    }
963
964    #[inline(always)]
965    fn update(&mut self, high: f64, low: f64, close: f64) -> (Option<f64>, Option<f64>) {
966        let idx = self.index;
967        self.index += 1;
968
969        while let Some(&(_, value)) = self.highs.back() {
970            if value <= high {
971                self.highs.pop_back();
972            } else {
973                break;
974            }
975        }
976        self.highs.push_back((idx, high));
977
978        while let Some(&(_, value)) = self.lows.back() {
979            if value >= low {
980                self.lows.pop_back();
981            } else {
982                break;
983            }
984        }
985        self.lows.push_back((idx, low));
986
987        let expire_before = idx.saturating_add(1).saturating_sub(self.length);
988        while let Some(&(window_idx, _)) = self.highs.front() {
989            if window_idx < expire_before {
990                self.highs.pop_front();
991            } else {
992                break;
993            }
994        }
995        while let Some(&(window_idx, _)) = self.lows.front() {
996            if window_idx < expire_before {
997                self.lows.pop_front();
998            } else {
999                break;
1000            }
1001        }
1002
1003        if idx + 1 < self.length {
1004            return (None, None);
1005        }
1006
1007        let highest = self.highs.front().map(|&(_, value)| value).unwrap_or(high);
1008        let lowest = self.lows.front().map(|&(_, value)| value).unwrap_or(low);
1009        let denom = highest - lowest;
1010        if denom <= 0.0 || !denom.is_finite() {
1011            return (None, None);
1012        }
1013        let fast = 100.0 * (close - lowest) / denom;
1014        let k = self.k_sma.update(fast);
1015        let d = match k {
1016            Some(k_value) => self.d_sma.update(k_value),
1017            None => None,
1018        };
1019        (k, d)
1020    }
1021}
1022
1023#[derive(Clone, Debug)]
1024struct EmoSignalState {
1025    divisor: f64,
1026    prev_hl2: Option<f64>,
1027    emo_sma: RollingSmaState,
1028    emo_avg_sma: RollingSmaState,
1029}
1030
1031impl EmoSignalState {
1032    #[inline(always)]
1033    fn new(divisor: usize, emo_length: usize, sma_length: usize) -> Self {
1034        Self {
1035            divisor: divisor as f64,
1036            prev_hl2: None,
1037            emo_sma: RollingSmaState::new(emo_length),
1038            emo_avg_sma: RollingSmaState::new(sma_length),
1039        }
1040    }
1041
1042    #[inline(always)]
1043    fn reset(&mut self) {
1044        self.prev_hl2 = None;
1045        self.emo_sma.reset();
1046        self.emo_avg_sma.reset();
1047    }
1048
1049    #[inline(always)]
1050    fn update(&mut self, high: f64, low: f64, volume: f64) -> i32 {
1051        let hl2 = 0.5 * (high + low);
1052        let prev_hl2 = match self.prev_hl2 {
1053            Some(prev) => prev,
1054            None => {
1055                self.prev_hl2 = Some(hl2);
1056                return 0;
1057            }
1058        };
1059        self.prev_hl2 = Some(hl2);
1060
1061        let raw = self.divisor * (hl2 - prev_hl2) * (high - low) / volume;
1062        let emo = match self.emo_sma.update(raw) {
1063            Some(value) => value,
1064            None => return 0,
1065        };
1066        let emo_avg = match self.emo_avg_sma.update(emo) {
1067            Some(value) => value,
1068            None => return 0,
1069        };
1070        let diff = emo - emo_avg;
1071        if diff < 0.0 && emo_avg < 0.0 {
1072            -5
1073        } else if diff > 0.0 && emo_avg > 0.0 {
1074            5
1075        } else {
1076            0
1077        }
1078    }
1079}
1080
1081#[derive(Clone, Debug)]
1082struct MacdSignalState {
1083    fast: EmaState,
1084    slow: EmaState,
1085    trend_sma: RollingSmaState,
1086}
1087
1088impl MacdSignalState {
1089    #[inline(always)]
1090    fn new(fast_length: usize, slow_length: usize, sma_length: usize) -> Self {
1091        Self {
1092            fast: EmaState::new(fast_length),
1093            slow: EmaState::new(slow_length),
1094            trend_sma: RollingSmaState::new(sma_length),
1095        }
1096    }
1097
1098    #[inline(always)]
1099    fn reset(&mut self) {
1100        self.fast.reset();
1101        self.slow.reset();
1102        self.trend_sma.reset();
1103    }
1104
1105    #[inline(always)]
1106    fn update(&mut self, close: f64) -> i32 {
1107        let macd = self.fast.update(close) - self.slow.update(close);
1108        let macd_avg = match self.trend_sma.update(macd) {
1109            Some(value) => value,
1110            None => return 0,
1111        };
1112        let diff = macd - macd_avg;
1113        if diff < 0.0 && macd_avg < 0.0 {
1114            -5
1115        } else if diff > 0.0 && macd_avg > 0.0 {
1116            5
1117        } else {
1118            0
1119        }
1120    }
1121}
1122
1123#[derive(Clone, Debug)]
1124struct RocSignalState {
1125    roc: RocState,
1126    roc_sma: RollingSmaState,
1127}
1128
1129impl RocSignalState {
1130    #[inline(always)]
1131    fn new(roc_length: usize, sma_length: usize) -> Self {
1132        Self {
1133            roc: RocState::new(roc_length),
1134            roc_sma: RollingSmaState::new(sma_length),
1135        }
1136    }
1137
1138    #[inline(always)]
1139    fn reset(&mut self) {
1140        self.roc.reset();
1141        self.roc_sma.reset();
1142    }
1143
1144    #[inline(always)]
1145    fn update(&mut self, close: f64) -> i32 {
1146        let roc = match self.roc.update(close) {
1147            Some(value) => value,
1148            None => return 0,
1149        };
1150        let roc_avg = match self.roc_sma.update(roc) {
1151            Some(value) => value,
1152            None => return 0,
1153        };
1154        let diff = roc - roc_avg;
1155        if diff < 0.0 && roc_avg < 0.0 {
1156            -5
1157        } else if diff > 0.0 && roc_avg > 0.0 {
1158            5
1159        } else {
1160            0
1161        }
1162    }
1163}
1164
1165#[derive(Clone, Debug)]
1166pub struct InsyncIndexStream {
1167    params: ResolvedParams,
1168    bb: RollingVarianceState,
1169    cci: RollingCciState,
1170    emo: EmoSignalState,
1171    macd: MacdSignalState,
1172    mfi: MfiState,
1173    dpo: DpoState,
1174    roc: RocSignalState,
1175    rsi: WilderRsiState,
1176    stoch: StochState,
1177}
1178
1179impl InsyncIndexStream {
1180    #[inline(always)]
1181    fn from_resolved(params: ResolvedParams) -> Self {
1182        Self {
1183            bb: RollingVarianceState::new(params.bb_length),
1184            cci: RollingCciState::new(params.cci_length),
1185            emo: EmoSignalState::new(params.emo_divisor, params.emo_length, params.sma_length),
1186            macd: MacdSignalState::new(params.fast_length, params.slow_length, params.sma_length),
1187            mfi: MfiState::new(params.mfi_length),
1188            dpo: DpoState::new(params.dpo_length, params.sma_length),
1189            roc: RocSignalState::new(params.roc_length, params.sma_length),
1190            rsi: WilderRsiState::new(params.rsi_length),
1191            stoch: StochState::new(
1192                params.stoch_length,
1193                params.stoch_d_length,
1194                params.stoch_k_length,
1195            ),
1196            params,
1197        }
1198    }
1199
1200    #[inline]
1201    pub fn try_new(params: InsyncIndexParams) -> Result<Self, InsyncIndexError> {
1202        Ok(Self::from_resolved(resolve_params(&params)?))
1203    }
1204
1205    #[inline(always)]
1206    pub fn reset(&mut self) {
1207        self.bb.reset();
1208        self.cci.reset();
1209        self.emo.reset();
1210        self.macd.reset();
1211        self.mfi.reset();
1212        self.dpo.reset();
1213        self.roc.reset();
1214        self.rsi.reset();
1215        self.stoch.reset();
1216    }
1217
1218    #[inline(always)]
1219    pub fn update(&mut self, high: f64, low: f64, close: f64, volume: f64) -> Option<f64> {
1220        self.update_reset_on_nan(high, low, close, volume)
1221    }
1222
1223    #[inline(always)]
1224    pub fn update_reset_on_nan(
1225        &mut self,
1226        high: f64,
1227        low: f64,
1228        close: f64,
1229        volume: f64,
1230    ) -> Option<f64> {
1231        if !valid_bar(high, low, close, volume) {
1232            self.reset();
1233            return None;
1234        }
1235
1236        let mut score = 50.0;
1237
1238        if let Some((mean, stddev)) = self.bb.update(close) {
1239            let lower = mean - self.params.bb_multiplier * stddev;
1240            let upper = mean + self.params.bb_multiplier * stddev;
1241            let denom = upper - lower;
1242            if denom > 0.0 {
1243                let position = (close - lower) / denom;
1244                if position < 0.05 {
1245                    score -= 5.0;
1246                } else if position > 0.95 {
1247                    score += 5.0;
1248                }
1249            }
1250        }
1251
1252        if let Some(cci) = self.cci.update(close) {
1253            if cci > 100.0 {
1254                score += 5.0;
1255            } else if cci < -100.0 {
1256                score -= 5.0;
1257            }
1258        }
1259
1260        score += self.emo.update(high, low, volume) as f64;
1261        score += self.macd.update(close) as f64;
1262
1263        let typical = (high + low + close) / 3.0;
1264        if let Some(mfi) = self.mfi.update(typical, volume) {
1265            if mfi > 80.0 {
1266                score += 5.0;
1267            } else if mfi < 20.0 {
1268                score -= 5.0;
1269            }
1270        }
1271
1272        score += self.dpo.update(close) as f64;
1273        score += self.roc.update(close) as f64;
1274
1275        if let Some(rsi) = self.rsi.update(close) {
1276            if rsi > 70.0 {
1277                score += 5.0;
1278            } else if rsi < 30.0 {
1279                score -= 5.0;
1280            }
1281        }
1282
1283        let (k, d) = self.stoch.update(high, low, close);
1284        if let Some(k) = k {
1285            if k > 80.0 {
1286                score += 5.0;
1287            } else if k < 20.0 {
1288                score -= 5.0;
1289            }
1290        }
1291        if let Some(d) = d {
1292            if d > 80.0 {
1293                score += 5.0;
1294            } else if d < 20.0 {
1295                score -= 5.0;
1296            }
1297        }
1298
1299        Some(score)
1300    }
1301}
1302
1303#[inline(always)]
1304fn insync_index_compute_into(
1305    high: &[f64],
1306    low: &[f64],
1307    close: &[f64],
1308    volume: &[f64],
1309    params: ResolvedParams,
1310    _kernel: Kernel,
1311    out: &mut [f64],
1312) {
1313    let mut stream = InsyncIndexStream::from_resolved(params);
1314    for i in 0..close.len() {
1315        out[i] = stream
1316            .update_reset_on_nan(high[i], low[i], close[i], volume[i])
1317            .unwrap_or(f64::NAN);
1318    }
1319}
1320
1321#[inline]
1322pub fn insync_index(input: &InsyncIndexInput) -> Result<InsyncIndexOutput, InsyncIndexError> {
1323    insync_index_with_kernel(input, Kernel::Auto)
1324}
1325
1326#[inline]
1327pub fn insync_index_with_kernel(
1328    input: &InsyncIndexInput,
1329    kernel: Kernel,
1330) -> Result<InsyncIndexOutput, InsyncIndexError> {
1331    let (high, low, close, volume, params) = prepare_input(input)?;
1332    let mut values = alloc_with_nan_prefix(close.len(), close.len());
1333    insync_index_compute_into(
1334        high,
1335        low,
1336        close,
1337        volume,
1338        params,
1339        normalize_kernel(kernel),
1340        &mut values,
1341    );
1342    Ok(InsyncIndexOutput { values })
1343}
1344
1345#[inline]
1346pub fn insync_index_into_slice(
1347    out: &mut [f64],
1348    input: &InsyncIndexInput,
1349    kernel: Kernel,
1350) -> Result<(), InsyncIndexError> {
1351    let (high, low, close, volume, params) = prepare_input(input)?;
1352    if out.len() != close.len() {
1353        return Err(InsyncIndexError::OutputLengthMismatch {
1354            expected: close.len(),
1355            got: out.len(),
1356        });
1357    }
1358    insync_index_compute_into(
1359        high,
1360        low,
1361        close,
1362        volume,
1363        params,
1364        normalize_kernel(kernel),
1365        out,
1366    );
1367    Ok(())
1368}
1369
1370#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1371#[inline]
1372pub fn insync_index_into(
1373    input: &InsyncIndexInput,
1374    out: &mut [f64],
1375) -> Result<(), InsyncIndexError> {
1376    insync_index_into_slice(out, input, Kernel::Auto)
1377}
1378
1379#[derive(Clone, Debug)]
1380pub struct InsyncIndexBatchRange {
1381    pub emo_divisor: (usize, usize, usize),
1382    pub emo_length: (usize, usize, usize),
1383    pub fast_length: (usize, usize, usize),
1384    pub slow_length: (usize, usize, usize),
1385    pub mfi_length: (usize, usize, usize),
1386    pub bb_length: (usize, usize, usize),
1387    pub bb_multiplier: (f64, f64, f64),
1388    pub cci_length: (usize, usize, usize),
1389    pub dpo_length: (usize, usize, usize),
1390    pub roc_length: (usize, usize, usize),
1391    pub rsi_length: (usize, usize, usize),
1392    pub stoch_length: (usize, usize, usize),
1393    pub stoch_d_length: (usize, usize, usize),
1394    pub stoch_k_length: (usize, usize, usize),
1395    pub sma_length: (usize, usize, usize),
1396}
1397
1398impl Default for InsyncIndexBatchRange {
1399    fn default() -> Self {
1400        Self {
1401            emo_divisor: (DEFAULT_EMO_DIVISOR, DEFAULT_EMO_DIVISOR, 0),
1402            emo_length: (DEFAULT_EMO_LENGTH, DEFAULT_EMO_LENGTH, 0),
1403            fast_length: (DEFAULT_FAST_LENGTH, DEFAULT_FAST_LENGTH, 0),
1404            slow_length: (DEFAULT_SLOW_LENGTH, DEFAULT_SLOW_LENGTH, 0),
1405            mfi_length: (DEFAULT_MFI_LENGTH, DEFAULT_MFI_LENGTH, 0),
1406            bb_length: (DEFAULT_BB_LENGTH, DEFAULT_BB_LENGTH, 0),
1407            bb_multiplier: (DEFAULT_BB_MULTIPLIER, DEFAULT_BB_MULTIPLIER, 0.0),
1408            cci_length: (DEFAULT_CCI_LENGTH, DEFAULT_CCI_LENGTH, 0),
1409            dpo_length: (DEFAULT_DPO_LENGTH, DEFAULT_DPO_LENGTH, 0),
1410            roc_length: (DEFAULT_ROC_LENGTH, DEFAULT_ROC_LENGTH, 0),
1411            rsi_length: (DEFAULT_RSI_LENGTH, DEFAULT_RSI_LENGTH, 0),
1412            stoch_length: (DEFAULT_STOCH_LENGTH, DEFAULT_STOCH_LENGTH, 0),
1413            stoch_d_length: (DEFAULT_STOCH_D_LENGTH, DEFAULT_STOCH_D_LENGTH, 0),
1414            stoch_k_length: (DEFAULT_STOCH_K_LENGTH, DEFAULT_STOCH_K_LENGTH, 0),
1415            sma_length: (DEFAULT_SMA_LENGTH, DEFAULT_SMA_LENGTH, 0),
1416        }
1417    }
1418}
1419
1420#[derive(Clone, Debug, Default)]
1421pub struct InsyncIndexBatchBuilder {
1422    range: InsyncIndexBatchRange,
1423    kernel: Kernel,
1424}
1425
1426impl InsyncIndexBatchBuilder {
1427    #[inline]
1428    pub fn new() -> Self {
1429        Self::default()
1430    }
1431
1432    #[inline]
1433    pub fn kernel(mut self, kernel: Kernel) -> Self {
1434        self.kernel = kernel;
1435        self
1436    }
1437
1438    #[inline]
1439    pub fn emo_divisor_range(mut self, start: usize, end: usize, step: usize) -> Self {
1440        self.range.emo_divisor = (start, end, step);
1441        self
1442    }
1443
1444    #[inline]
1445    pub fn emo_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1446        self.range.emo_length = (start, end, step);
1447        self
1448    }
1449
1450    #[inline]
1451    pub fn fast_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1452        self.range.fast_length = (start, end, step);
1453        self
1454    }
1455
1456    #[inline]
1457    pub fn slow_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1458        self.range.slow_length = (start, end, step);
1459        self
1460    }
1461
1462    #[inline]
1463    pub fn mfi_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1464        self.range.mfi_length = (start, end, step);
1465        self
1466    }
1467
1468    #[inline]
1469    pub fn bb_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1470        self.range.bb_length = (start, end, step);
1471        self
1472    }
1473
1474    #[inline]
1475    pub fn bb_multiplier_range(mut self, start: f64, end: f64, step: f64) -> Self {
1476        self.range.bb_multiplier = (start, end, step);
1477        self
1478    }
1479
1480    #[inline]
1481    pub fn cci_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1482        self.range.cci_length = (start, end, step);
1483        self
1484    }
1485
1486    #[inline]
1487    pub fn dpo_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1488        self.range.dpo_length = (start, end, step);
1489        self
1490    }
1491
1492    #[inline]
1493    pub fn roc_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1494        self.range.roc_length = (start, end, step);
1495        self
1496    }
1497
1498    #[inline]
1499    pub fn rsi_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1500        self.range.rsi_length = (start, end, step);
1501        self
1502    }
1503
1504    #[inline]
1505    pub fn stoch_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1506        self.range.stoch_length = (start, end, step);
1507        self
1508    }
1509
1510    #[inline]
1511    pub fn stoch_d_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1512        self.range.stoch_d_length = (start, end, step);
1513        self
1514    }
1515
1516    #[inline]
1517    pub fn stoch_k_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1518        self.range.stoch_k_length = (start, end, step);
1519        self
1520    }
1521
1522    #[inline]
1523    pub fn sma_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1524        self.range.sma_length = (start, end, step);
1525        self
1526    }
1527
1528    #[inline]
1529    pub fn emo_divisor_static(mut self, value: usize) -> Self {
1530        self.range.emo_divisor = (value, value, 0);
1531        self
1532    }
1533
1534    #[inline]
1535    pub fn emo_length_static(mut self, value: usize) -> Self {
1536        self.range.emo_length = (value, value, 0);
1537        self
1538    }
1539
1540    #[inline]
1541    pub fn fast_length_static(mut self, value: usize) -> Self {
1542        self.range.fast_length = (value, value, 0);
1543        self
1544    }
1545
1546    #[inline]
1547    pub fn slow_length_static(mut self, value: usize) -> Self {
1548        self.range.slow_length = (value, value, 0);
1549        self
1550    }
1551
1552    #[inline]
1553    pub fn mfi_length_static(mut self, value: usize) -> Self {
1554        self.range.mfi_length = (value, value, 0);
1555        self
1556    }
1557
1558    #[inline]
1559    pub fn bb_length_static(mut self, value: usize) -> Self {
1560        self.range.bb_length = (value, value, 0);
1561        self
1562    }
1563
1564    #[inline]
1565    pub fn bb_multiplier_static(mut self, value: f64) -> Self {
1566        self.range.bb_multiplier = (value, value, 0.0);
1567        self
1568    }
1569
1570    #[inline]
1571    pub fn cci_length_static(mut self, value: usize) -> Self {
1572        self.range.cci_length = (value, value, 0);
1573        self
1574    }
1575
1576    #[inline]
1577    pub fn dpo_length_static(mut self, value: usize) -> Self {
1578        self.range.dpo_length = (value, value, 0);
1579        self
1580    }
1581
1582    #[inline]
1583    pub fn roc_length_static(mut self, value: usize) -> Self {
1584        self.range.roc_length = (value, value, 0);
1585        self
1586    }
1587
1588    #[inline]
1589    pub fn rsi_length_static(mut self, value: usize) -> Self {
1590        self.range.rsi_length = (value, value, 0);
1591        self
1592    }
1593
1594    #[inline]
1595    pub fn stoch_length_static(mut self, value: usize) -> Self {
1596        self.range.stoch_length = (value, value, 0);
1597        self
1598    }
1599
1600    #[inline]
1601    pub fn stoch_d_length_static(mut self, value: usize) -> Self {
1602        self.range.stoch_d_length = (value, value, 0);
1603        self
1604    }
1605
1606    #[inline]
1607    pub fn stoch_k_length_static(mut self, value: usize) -> Self {
1608        self.range.stoch_k_length = (value, value, 0);
1609        self
1610    }
1611
1612    #[inline]
1613    pub fn sma_length_static(mut self, value: usize) -> Self {
1614        self.range.sma_length = (value, value, 0);
1615        self
1616    }
1617
1618    pub fn apply_slices(
1619        self,
1620        high: &[f64],
1621        low: &[f64],
1622        close: &[f64],
1623        volume: &[f64],
1624    ) -> Result<InsyncIndexBatchOutput, InsyncIndexError> {
1625        insync_index_batch_with_kernel(high, low, close, volume, &self.range, self.kernel)
1626    }
1627
1628    pub fn apply_candles(
1629        self,
1630        candles: &Candles,
1631    ) -> Result<InsyncIndexBatchOutput, InsyncIndexError> {
1632        self.apply_slices(
1633            candles.high.as_slice(),
1634            candles.low.as_slice(),
1635            candles.close.as_slice(),
1636            candles.volume.as_slice(),
1637        )
1638    }
1639}
1640
1641#[derive(Clone, Debug)]
1642pub struct InsyncIndexBatchOutput {
1643    pub values: Vec<f64>,
1644    pub combos: Vec<InsyncIndexParams>,
1645    pub rows: usize,
1646    pub cols: usize,
1647}
1648
1649impl InsyncIndexBatchOutput {
1650    pub fn row_for_params(&self, params: &InsyncIndexParams) -> Option<usize> {
1651        let target = resolve_params(params).ok()?;
1652        self.combos.iter().position(|combo| {
1653            resolve_params(combo)
1654                .map(|resolved| {
1655                    resolved.emo_divisor == target.emo_divisor
1656                        && resolved.emo_length == target.emo_length
1657                        && resolved.fast_length == target.fast_length
1658                        && resolved.slow_length == target.slow_length
1659                        && resolved.mfi_length == target.mfi_length
1660                        && resolved.bb_length == target.bb_length
1661                        && (resolved.bb_multiplier - target.bb_multiplier).abs() <= 1e-12
1662                        && resolved.cci_length == target.cci_length
1663                        && resolved.dpo_length == target.dpo_length
1664                        && resolved.roc_length == target.roc_length
1665                        && resolved.rsi_length == target.rsi_length
1666                        && resolved.stoch_length == target.stoch_length
1667                        && resolved.stoch_d_length == target.stoch_d_length
1668                        && resolved.stoch_k_length == target.stoch_k_length
1669                        && resolved.sma_length == target.sma_length
1670                })
1671                .unwrap_or(false)
1672        })
1673    }
1674}
1675
1676fn axis_usize(
1677    name: &'static str,
1678    range: (usize, usize, usize),
1679) -> Result<Vec<usize>, InsyncIndexError> {
1680    let (start, end, step) = range;
1681    if start == 0 || end == 0 {
1682        return Err(InsyncIndexError::InvalidRange {
1683            name,
1684            start: start.to_string(),
1685            end: end.to_string(),
1686            step: step.to_string(),
1687        });
1688    }
1689    if step == 0 || start == end {
1690        return Ok(vec![start]);
1691    }
1692    let mut out = Vec::new();
1693    if start < end {
1694        let mut value = start;
1695        while value <= end {
1696            out.push(value);
1697            match value.checked_add(step) {
1698                Some(next) if next > value => value = next,
1699                _ => break,
1700            }
1701        }
1702    } else {
1703        let mut value = start;
1704        while value >= end {
1705            out.push(value);
1706            if value < end.saturating_add(step) {
1707                break;
1708            }
1709            value = value.saturating_sub(step);
1710            if value == 0 {
1711                break;
1712            }
1713        }
1714    }
1715    if out.is_empty() {
1716        return Err(InsyncIndexError::InvalidRange {
1717            name,
1718            start: start.to_string(),
1719            end: end.to_string(),
1720            step: step.to_string(),
1721        });
1722    }
1723    Ok(out)
1724}
1725
1726fn axis_f64(name: &'static str, range: (f64, f64, f64)) -> Result<Vec<f64>, InsyncIndexError> {
1727    let (start, end, step) = range;
1728    if !start.is_finite() || !end.is_finite() || start <= 0.0 || end <= 0.0 {
1729        return Err(InsyncIndexError::InvalidRange {
1730            name,
1731            start: start.to_string(),
1732            end: end.to_string(),
1733            step: step.to_string(),
1734        });
1735    }
1736    if step == 0.0 || (start - end).abs() <= f64::EPSILON {
1737        return Ok(vec![start]);
1738    }
1739    if !step.is_finite() || step < 0.0 {
1740        return Err(InsyncIndexError::InvalidRange {
1741            name,
1742            start: start.to_string(),
1743            end: end.to_string(),
1744            step: step.to_string(),
1745        });
1746    }
1747    let mut out = Vec::new();
1748    if start < end {
1749        let mut value = start;
1750        while value <= end + 1e-12 {
1751            out.push(value);
1752            value += step;
1753            if step <= 0.0 {
1754                break;
1755            }
1756        }
1757    } else {
1758        let mut value = start;
1759        while value >= end - 1e-12 {
1760            out.push(value);
1761            value -= step;
1762            if step <= 0.0 {
1763                break;
1764            }
1765        }
1766    }
1767    if out.is_empty() {
1768        return Err(InsyncIndexError::InvalidRange {
1769            name,
1770            start: start.to_string(),
1771            end: end.to_string(),
1772            step: step.to_string(),
1773        });
1774    }
1775    Ok(out)
1776}
1777
1778pub fn expand_grid_insync_index(
1779    sweep: &InsyncIndexBatchRange,
1780) -> Result<Vec<InsyncIndexParams>, InsyncIndexError> {
1781    let emo_divisors = axis_usize("emo_divisor", sweep.emo_divisor)?;
1782    let emo_lengths = axis_usize("emo_length", sweep.emo_length)?;
1783    let fast_lengths = axis_usize("fast_length", sweep.fast_length)?;
1784    let slow_lengths = axis_usize("slow_length", sweep.slow_length)?;
1785    let mfi_lengths = axis_usize("mfi_length", sweep.mfi_length)?;
1786    let bb_lengths = axis_usize("bb_length", sweep.bb_length)?;
1787    let bb_multipliers = axis_f64("bb_multiplier", sweep.bb_multiplier)?;
1788    let cci_lengths = axis_usize("cci_length", sweep.cci_length)?;
1789    let dpo_lengths = axis_usize("dpo_length", sweep.dpo_length)?;
1790    let roc_lengths = axis_usize("roc_length", sweep.roc_length)?;
1791    let rsi_lengths = axis_usize("rsi_length", sweep.rsi_length)?;
1792    let stoch_lengths = axis_usize("stoch_length", sweep.stoch_length)?;
1793    let stoch_d_lengths = axis_usize("stoch_d_length", sweep.stoch_d_length)?;
1794    let stoch_k_lengths = axis_usize("stoch_k_length", sweep.stoch_k_length)?;
1795    let sma_lengths = axis_usize("sma_length", sweep.sma_length)?;
1796
1797    let mut out = Vec::new();
1798    for emo_divisor in emo_divisors {
1799        for &emo_length in &emo_lengths {
1800            for &fast_length in &fast_lengths {
1801                for &slow_length in &slow_lengths {
1802                    for &mfi_length in &mfi_lengths {
1803                        for &bb_length in &bb_lengths {
1804                            for &bb_multiplier in &bb_multipliers {
1805                                for &cci_length in &cci_lengths {
1806                                    for &dpo_length in &dpo_lengths {
1807                                        for &roc_length in &roc_lengths {
1808                                            for &rsi_length in &rsi_lengths {
1809                                                for &stoch_length in &stoch_lengths {
1810                                                    for &stoch_d_length in &stoch_d_lengths {
1811                                                        for &stoch_k_length in &stoch_k_lengths {
1812                                                            for &sma_length in &sma_lengths {
1813                                                                out.push(InsyncIndexParams {
1814                                                                    emo_divisor: Some(emo_divisor),
1815                                                                    emo_length: Some(emo_length),
1816                                                                    fast_length: Some(fast_length),
1817                                                                    slow_length: Some(slow_length),
1818                                                                    mfi_length: Some(mfi_length),
1819                                                                    bb_length: Some(bb_length),
1820                                                                    bb_multiplier: Some(
1821                                                                        bb_multiplier,
1822                                                                    ),
1823                                                                    cci_length: Some(cci_length),
1824                                                                    dpo_length: Some(dpo_length),
1825                                                                    roc_length: Some(roc_length),
1826                                                                    rsi_length: Some(rsi_length),
1827                                                                    stoch_length: Some(
1828                                                                        stoch_length,
1829                                                                    ),
1830                                                                    stoch_d_length: Some(
1831                                                                        stoch_d_length,
1832                                                                    ),
1833                                                                    stoch_k_length: Some(
1834                                                                        stoch_k_length,
1835                                                                    ),
1836                                                                    sma_length: Some(sma_length),
1837                                                                });
1838                                                            }
1839                                                        }
1840                                                    }
1841                                                }
1842                                            }
1843                                        }
1844                                    }
1845                                }
1846                            }
1847                        }
1848                    }
1849                }
1850            }
1851        }
1852    }
1853    Ok(out)
1854}
1855
1856pub fn insync_index_batch_with_kernel(
1857    high: &[f64],
1858    low: &[f64],
1859    close: &[f64],
1860    volume: &[f64],
1861    sweep: &InsyncIndexBatchRange,
1862    kernel: Kernel,
1863) -> Result<InsyncIndexBatchOutput, InsyncIndexError> {
1864    let batch_kernel = match kernel {
1865        Kernel::Auto => Kernel::ScalarBatch,
1866        other if other.is_batch() => other,
1867        other => return Err(InsyncIndexError::InvalidKernelForBatch(other)),
1868    };
1869    insync_index_batch_impl(
1870        high,
1871        low,
1872        close,
1873        volume,
1874        sweep,
1875        batch_kernel.to_non_batch(),
1876        true,
1877    )
1878}
1879
1880pub fn insync_index_batch_slice(
1881    high: &[f64],
1882    low: &[f64],
1883    close: &[f64],
1884    volume: &[f64],
1885    sweep: &InsyncIndexBatchRange,
1886) -> Result<InsyncIndexBatchOutput, InsyncIndexError> {
1887    insync_index_batch_impl(high, low, close, volume, sweep, Kernel::Scalar, false)
1888}
1889
1890pub fn insync_index_batch_par_slice(
1891    high: &[f64],
1892    low: &[f64],
1893    close: &[f64],
1894    volume: &[f64],
1895    sweep: &InsyncIndexBatchRange,
1896) -> Result<InsyncIndexBatchOutput, InsyncIndexError> {
1897    insync_index_batch_impl(high, low, close, volume, sweep, Kernel::Scalar, true)
1898}
1899
1900fn insync_index_batch_impl(
1901    high: &[f64],
1902    low: &[f64],
1903    close: &[f64],
1904    volume: &[f64],
1905    sweep: &InsyncIndexBatchRange,
1906    kernel: Kernel,
1907    parallel: bool,
1908) -> Result<InsyncIndexBatchOutput, InsyncIndexError> {
1909    let combos = expand_grid_insync_index(sweep)?;
1910    let rows = combos.len();
1911    let cols = close.len();
1912
1913    if cols == 0 {
1914        return Err(InsyncIndexError::EmptyInputData);
1915    }
1916    if high.len() != cols || low.len() != cols || volume.len() != cols {
1917        return Err(InsyncIndexError::DataLengthMismatch);
1918    }
1919    if !has_any_valid_bar(high, low, close, volume) {
1920        return Err(InsyncIndexError::AllValuesNaN);
1921    }
1922    for params in &combos {
1923        resolve_params(params)?;
1924    }
1925
1926    let matrix = make_uninit_matrix(rows, cols);
1927    let mut guard = ManuallyDrop::new(matrix);
1928    let values_mu: &mut [MaybeUninit<f64>] =
1929        unsafe { std::slice::from_raw_parts_mut(guard.as_mut_ptr(), guard.len()) };
1930
1931    let do_row = |row: usize, row_mu: &mut [MaybeUninit<f64>]| {
1932        let params = resolve_params(&combos[row]).expect("validated params");
1933        let dst = unsafe {
1934            std::slice::from_raw_parts_mut(row_mu.as_mut_ptr() as *mut f64, row_mu.len())
1935        };
1936        insync_index_compute_into(high, low, close, volume, params, kernel, dst);
1937    };
1938
1939    if parallel {
1940        #[cfg(not(target_arch = "wasm32"))]
1941        {
1942            values_mu
1943                .par_chunks_mut(cols)
1944                .enumerate()
1945                .for_each(|(row, row_mu)| do_row(row, row_mu));
1946        }
1947        #[cfg(target_arch = "wasm32")]
1948        {
1949            for (row, row_mu) in values_mu.chunks_mut(cols).enumerate() {
1950                do_row(row, row_mu);
1951            }
1952        }
1953    } else {
1954        for (row, row_mu) in values_mu.chunks_mut(cols).enumerate() {
1955            do_row(row, row_mu);
1956        }
1957    }
1958
1959    let values =
1960        unsafe { Vec::from_raw_parts(guard.as_mut_ptr() as *mut f64, rows * cols, rows * cols) };
1961    Ok(InsyncIndexBatchOutput {
1962        values,
1963        combos,
1964        rows,
1965        cols,
1966    })
1967}
1968
1969#[cfg(feature = "python")]
1970#[pyfunction(name = "insync_index")]
1971#[pyo3(signature = (
1972    high,
1973    low,
1974    close,
1975    volume,
1976    emo_divisor=DEFAULT_EMO_DIVISOR,
1977    emo_length=DEFAULT_EMO_LENGTH,
1978    fast_length=DEFAULT_FAST_LENGTH,
1979    slow_length=DEFAULT_SLOW_LENGTH,
1980    mfi_length=DEFAULT_MFI_LENGTH,
1981    bb_length=DEFAULT_BB_LENGTH,
1982    bb_multiplier=DEFAULT_BB_MULTIPLIER,
1983    cci_length=DEFAULT_CCI_LENGTH,
1984    dpo_length=DEFAULT_DPO_LENGTH,
1985    roc_length=DEFAULT_ROC_LENGTH,
1986    rsi_length=DEFAULT_RSI_LENGTH,
1987    stoch_length=DEFAULT_STOCH_LENGTH,
1988    stoch_d_length=DEFAULT_STOCH_D_LENGTH,
1989    stoch_k_length=DEFAULT_STOCH_K_LENGTH,
1990    sma_length=DEFAULT_SMA_LENGTH,
1991    kernel=None
1992))]
1993pub fn insync_index_py<'py>(
1994    py: Python<'py>,
1995    high: PyReadonlyArray1<'py, f64>,
1996    low: PyReadonlyArray1<'py, f64>,
1997    close: PyReadonlyArray1<'py, f64>,
1998    volume: PyReadonlyArray1<'py, f64>,
1999    emo_divisor: usize,
2000    emo_length: usize,
2001    fast_length: usize,
2002    slow_length: usize,
2003    mfi_length: usize,
2004    bb_length: usize,
2005    bb_multiplier: f64,
2006    cci_length: usize,
2007    dpo_length: usize,
2008    roc_length: usize,
2009    rsi_length: usize,
2010    stoch_length: usize,
2011    stoch_d_length: usize,
2012    stoch_k_length: usize,
2013    sma_length: usize,
2014    kernel: Option<&str>,
2015) -> PyResult<Bound<'py, PyArray1<f64>>> {
2016    let high = high.as_slice()?;
2017    let low = low.as_slice()?;
2018    let close = close.as_slice()?;
2019    let volume = volume.as_slice()?;
2020    let kernel = validate_kernel(kernel, false)?;
2021    let input = InsyncIndexInput::from_slices(
2022        high,
2023        low,
2024        close,
2025        volume,
2026        InsyncIndexParams {
2027            emo_divisor: Some(emo_divisor),
2028            emo_length: Some(emo_length),
2029            fast_length: Some(fast_length),
2030            slow_length: Some(slow_length),
2031            mfi_length: Some(mfi_length),
2032            bb_length: Some(bb_length),
2033            bb_multiplier: Some(bb_multiplier),
2034            cci_length: Some(cci_length),
2035            dpo_length: Some(dpo_length),
2036            roc_length: Some(roc_length),
2037            rsi_length: Some(rsi_length),
2038            stoch_length: Some(stoch_length),
2039            stoch_d_length: Some(stoch_d_length),
2040            stoch_k_length: Some(stoch_k_length),
2041            sma_length: Some(sma_length),
2042        },
2043    );
2044    let output = py
2045        .allow_threads(|| insync_index_with_kernel(&input, kernel))
2046        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2047    Ok(output.values.into_pyarray(py))
2048}
2049
2050#[cfg(feature = "python")]
2051#[pyclass(name = "InsyncIndexStream")]
2052pub struct InsyncIndexStreamPy {
2053    stream: InsyncIndexStream,
2054}
2055
2056#[cfg(feature = "python")]
2057#[pymethods]
2058impl InsyncIndexStreamPy {
2059    #[new]
2060    #[pyo3(signature = (
2061        emo_divisor=DEFAULT_EMO_DIVISOR,
2062        emo_length=DEFAULT_EMO_LENGTH,
2063        fast_length=DEFAULT_FAST_LENGTH,
2064        slow_length=DEFAULT_SLOW_LENGTH,
2065        mfi_length=DEFAULT_MFI_LENGTH,
2066        bb_length=DEFAULT_BB_LENGTH,
2067        bb_multiplier=DEFAULT_BB_MULTIPLIER,
2068        cci_length=DEFAULT_CCI_LENGTH,
2069        dpo_length=DEFAULT_DPO_LENGTH,
2070        roc_length=DEFAULT_ROC_LENGTH,
2071        rsi_length=DEFAULT_RSI_LENGTH,
2072        stoch_length=DEFAULT_STOCH_LENGTH,
2073        stoch_d_length=DEFAULT_STOCH_D_LENGTH,
2074        stoch_k_length=DEFAULT_STOCH_K_LENGTH,
2075        sma_length=DEFAULT_SMA_LENGTH
2076    ))]
2077    fn new(
2078        emo_divisor: usize,
2079        emo_length: usize,
2080        fast_length: usize,
2081        slow_length: usize,
2082        mfi_length: usize,
2083        bb_length: usize,
2084        bb_multiplier: f64,
2085        cci_length: usize,
2086        dpo_length: usize,
2087        roc_length: usize,
2088        rsi_length: usize,
2089        stoch_length: usize,
2090        stoch_d_length: usize,
2091        stoch_k_length: usize,
2092        sma_length: usize,
2093    ) -> PyResult<Self> {
2094        let stream = InsyncIndexStream::try_new(InsyncIndexParams {
2095            emo_divisor: Some(emo_divisor),
2096            emo_length: Some(emo_length),
2097            fast_length: Some(fast_length),
2098            slow_length: Some(slow_length),
2099            mfi_length: Some(mfi_length),
2100            bb_length: Some(bb_length),
2101            bb_multiplier: Some(bb_multiplier),
2102            cci_length: Some(cci_length),
2103            dpo_length: Some(dpo_length),
2104            roc_length: Some(roc_length),
2105            rsi_length: Some(rsi_length),
2106            stoch_length: Some(stoch_length),
2107            stoch_d_length: Some(stoch_d_length),
2108            stoch_k_length: Some(stoch_k_length),
2109            sma_length: Some(sma_length),
2110        })
2111        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2112        Ok(Self { stream })
2113    }
2114
2115    fn update(&mut self, high: f64, low: f64, close: f64, volume: f64) -> Option<f64> {
2116        self.stream.update_reset_on_nan(high, low, close, volume)
2117    }
2118}
2119
2120#[cfg(feature = "python")]
2121#[pyfunction(name = "insync_index_batch")]
2122#[pyo3(signature = (
2123    high,
2124    low,
2125    close,
2126    volume,
2127    emo_divisor_range=(DEFAULT_EMO_DIVISOR, DEFAULT_EMO_DIVISOR, 0),
2128    emo_length_range=(DEFAULT_EMO_LENGTH, DEFAULT_EMO_LENGTH, 0),
2129    fast_length_range=(DEFAULT_FAST_LENGTH, DEFAULT_FAST_LENGTH, 0),
2130    slow_length_range=(DEFAULT_SLOW_LENGTH, DEFAULT_SLOW_LENGTH, 0),
2131    mfi_length_range=(DEFAULT_MFI_LENGTH, DEFAULT_MFI_LENGTH, 0),
2132    bb_length_range=(DEFAULT_BB_LENGTH, DEFAULT_BB_LENGTH, 0),
2133    bb_multiplier_range=(DEFAULT_BB_MULTIPLIER, DEFAULT_BB_MULTIPLIER, 0.0),
2134    cci_length_range=(DEFAULT_CCI_LENGTH, DEFAULT_CCI_LENGTH, 0),
2135    dpo_length_range=(DEFAULT_DPO_LENGTH, DEFAULT_DPO_LENGTH, 0),
2136    roc_length_range=(DEFAULT_ROC_LENGTH, DEFAULT_ROC_LENGTH, 0),
2137    rsi_length_range=(DEFAULT_RSI_LENGTH, DEFAULT_RSI_LENGTH, 0),
2138    stoch_length_range=(DEFAULT_STOCH_LENGTH, DEFAULT_STOCH_LENGTH, 0),
2139    stoch_d_length_range=(DEFAULT_STOCH_D_LENGTH, DEFAULT_STOCH_D_LENGTH, 0),
2140    stoch_k_length_range=(DEFAULT_STOCH_K_LENGTH, DEFAULT_STOCH_K_LENGTH, 0),
2141    sma_length_range=(DEFAULT_SMA_LENGTH, DEFAULT_SMA_LENGTH, 0),
2142    kernel=None
2143))]
2144pub fn insync_index_batch_py<'py>(
2145    py: Python<'py>,
2146    high: PyReadonlyArray1<'py, f64>,
2147    low: PyReadonlyArray1<'py, f64>,
2148    close: PyReadonlyArray1<'py, f64>,
2149    volume: PyReadonlyArray1<'py, f64>,
2150    emo_divisor_range: (usize, usize, usize),
2151    emo_length_range: (usize, usize, usize),
2152    fast_length_range: (usize, usize, usize),
2153    slow_length_range: (usize, usize, usize),
2154    mfi_length_range: (usize, usize, usize),
2155    bb_length_range: (usize, usize, usize),
2156    bb_multiplier_range: (f64, f64, f64),
2157    cci_length_range: (usize, usize, usize),
2158    dpo_length_range: (usize, usize, usize),
2159    roc_length_range: (usize, usize, usize),
2160    rsi_length_range: (usize, usize, usize),
2161    stoch_length_range: (usize, usize, usize),
2162    stoch_d_length_range: (usize, usize, usize),
2163    stoch_k_length_range: (usize, usize, usize),
2164    sma_length_range: (usize, usize, usize),
2165    kernel: Option<&str>,
2166) -> PyResult<Bound<'py, PyDict>> {
2167    let high = high.as_slice()?;
2168    let low = low.as_slice()?;
2169    let close = close.as_slice()?;
2170    let volume = volume.as_slice()?;
2171    let kernel = validate_kernel(kernel, true)?;
2172    let sweep = InsyncIndexBatchRange {
2173        emo_divisor: emo_divisor_range,
2174        emo_length: emo_length_range,
2175        fast_length: fast_length_range,
2176        slow_length: slow_length_range,
2177        mfi_length: mfi_length_range,
2178        bb_length: bb_length_range,
2179        bb_multiplier: bb_multiplier_range,
2180        cci_length: cci_length_range,
2181        dpo_length: dpo_length_range,
2182        roc_length: roc_length_range,
2183        rsi_length: rsi_length_range,
2184        stoch_length: stoch_length_range,
2185        stoch_d_length: stoch_d_length_range,
2186        stoch_k_length: stoch_k_length_range,
2187        sma_length: sma_length_range,
2188    };
2189    let output = py
2190        .allow_threads(|| insync_index_batch_with_kernel(high, low, close, volume, &sweep, kernel))
2191        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2192
2193    let values = output
2194        .values
2195        .into_pyarray(py)
2196        .reshape((output.rows, output.cols))?;
2197    let dict = PyDict::new(py);
2198    dict.set_item("values", values)?;
2199    dict.set_item("rows", output.rows)?;
2200    dict.set_item("cols", output.cols)?;
2201    dict.set_item(
2202        "emo_divisors",
2203        output
2204            .combos
2205            .iter()
2206            .map(|p| p.emo_divisor.unwrap_or(DEFAULT_EMO_DIVISOR))
2207            .collect::<Vec<_>>()
2208            .into_pyarray(py),
2209    )?;
2210    dict.set_item(
2211        "emo_lengths",
2212        output
2213            .combos
2214            .iter()
2215            .map(|p| p.emo_length.unwrap_or(DEFAULT_EMO_LENGTH))
2216            .collect::<Vec<_>>()
2217            .into_pyarray(py),
2218    )?;
2219    dict.set_item(
2220        "fast_lengths",
2221        output
2222            .combos
2223            .iter()
2224            .map(|p| p.fast_length.unwrap_or(DEFAULT_FAST_LENGTH))
2225            .collect::<Vec<_>>()
2226            .into_pyarray(py),
2227    )?;
2228    dict.set_item(
2229        "slow_lengths",
2230        output
2231            .combos
2232            .iter()
2233            .map(|p| p.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH))
2234            .collect::<Vec<_>>()
2235            .into_pyarray(py),
2236    )?;
2237    dict.set_item(
2238        "mfi_lengths",
2239        output
2240            .combos
2241            .iter()
2242            .map(|p| p.mfi_length.unwrap_or(DEFAULT_MFI_LENGTH))
2243            .collect::<Vec<_>>()
2244            .into_pyarray(py),
2245    )?;
2246    dict.set_item(
2247        "bb_lengths",
2248        output
2249            .combos
2250            .iter()
2251            .map(|p| p.bb_length.unwrap_or(DEFAULT_BB_LENGTH))
2252            .collect::<Vec<_>>()
2253            .into_pyarray(py),
2254    )?;
2255    dict.set_item(
2256        "bb_multipliers",
2257        output
2258            .combos
2259            .iter()
2260            .map(|p| p.bb_multiplier.unwrap_or(DEFAULT_BB_MULTIPLIER))
2261            .collect::<Vec<_>>()
2262            .into_pyarray(py),
2263    )?;
2264    dict.set_item(
2265        "cci_lengths",
2266        output
2267            .combos
2268            .iter()
2269            .map(|p| p.cci_length.unwrap_or(DEFAULT_CCI_LENGTH))
2270            .collect::<Vec<_>>()
2271            .into_pyarray(py),
2272    )?;
2273    dict.set_item(
2274        "dpo_lengths",
2275        output
2276            .combos
2277            .iter()
2278            .map(|p| p.dpo_length.unwrap_or(DEFAULT_DPO_LENGTH))
2279            .collect::<Vec<_>>()
2280            .into_pyarray(py),
2281    )?;
2282    dict.set_item(
2283        "roc_lengths",
2284        output
2285            .combos
2286            .iter()
2287            .map(|p| p.roc_length.unwrap_or(DEFAULT_ROC_LENGTH))
2288            .collect::<Vec<_>>()
2289            .into_pyarray(py),
2290    )?;
2291    dict.set_item(
2292        "rsi_lengths",
2293        output
2294            .combos
2295            .iter()
2296            .map(|p| p.rsi_length.unwrap_or(DEFAULT_RSI_LENGTH))
2297            .collect::<Vec<_>>()
2298            .into_pyarray(py),
2299    )?;
2300    dict.set_item(
2301        "stoch_lengths",
2302        output
2303            .combos
2304            .iter()
2305            .map(|p| p.stoch_length.unwrap_or(DEFAULT_STOCH_LENGTH))
2306            .collect::<Vec<_>>()
2307            .into_pyarray(py),
2308    )?;
2309    dict.set_item(
2310        "stoch_d_lengths",
2311        output
2312            .combos
2313            .iter()
2314            .map(|p| p.stoch_d_length.unwrap_or(DEFAULT_STOCH_D_LENGTH))
2315            .collect::<Vec<_>>()
2316            .into_pyarray(py),
2317    )?;
2318    dict.set_item(
2319        "stoch_k_lengths",
2320        output
2321            .combos
2322            .iter()
2323            .map(|p| p.stoch_k_length.unwrap_or(DEFAULT_STOCH_K_LENGTH))
2324            .collect::<Vec<_>>()
2325            .into_pyarray(py),
2326    )?;
2327    dict.set_item(
2328        "sma_lengths",
2329        output
2330            .combos
2331            .iter()
2332            .map(|p| p.sma_length.unwrap_or(DEFAULT_SMA_LENGTH))
2333            .collect::<Vec<_>>()
2334            .into_pyarray(py),
2335    )?;
2336    Ok(dict)
2337}
2338
2339#[cfg(feature = "python")]
2340pub fn register_insync_index_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
2341    m.add_function(wrap_pyfunction!(insync_index_py, m)?)?;
2342    m.add_function(wrap_pyfunction!(insync_index_batch_py, m)?)?;
2343    m.add_class::<InsyncIndexStreamPy>()?;
2344    Ok(())
2345}
2346
2347#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2348#[derive(Debug, Clone, Serialize, Deserialize)]
2349struct InsyncIndexBatchConfig {
2350    emo_divisor_range: Option<Vec<usize>>,
2351    emo_length_range: Option<Vec<usize>>,
2352    fast_length_range: Option<Vec<usize>>,
2353    slow_length_range: Option<Vec<usize>>,
2354    mfi_length_range: Option<Vec<usize>>,
2355    bb_length_range: Option<Vec<usize>>,
2356    bb_multiplier_range: Option<Vec<f64>>,
2357    cci_length_range: Option<Vec<usize>>,
2358    dpo_length_range: Option<Vec<usize>>,
2359    roc_length_range: Option<Vec<usize>>,
2360    rsi_length_range: Option<Vec<usize>>,
2361    stoch_length_range: Option<Vec<usize>>,
2362    stoch_d_length_range: Option<Vec<usize>>,
2363    stoch_k_length_range: Option<Vec<usize>>,
2364    sma_length_range: Option<Vec<usize>>,
2365}
2366
2367#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2368#[derive(Debug, Clone, Serialize, Deserialize)]
2369struct InsyncIndexBatchJsOutput {
2370    values: Vec<f64>,
2371    rows: usize,
2372    cols: usize,
2373    combos: Vec<InsyncIndexParams>,
2374}
2375
2376#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2377fn parse_usize_js_range(
2378    range: Option<Vec<usize>>,
2379    default_value: usize,
2380    name: &'static str,
2381) -> Result<(usize, usize, usize), JsValue> {
2382    match range {
2383        Some(values) => {
2384            if values.len() != 3 {
2385                return Err(JsValue::from_str(&format!(
2386                    "Invalid config: {name} must have exactly 3 elements [start, end, step]"
2387                )));
2388            }
2389            Ok((values[0], values[1], values[2]))
2390        }
2391        None => Ok((default_value, default_value, 0)),
2392    }
2393}
2394
2395#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2396fn parse_f64_js_range(
2397    range: Option<Vec<f64>>,
2398    default_value: f64,
2399    name: &'static str,
2400) -> Result<(f64, f64, f64), JsValue> {
2401    match range {
2402        Some(values) => {
2403            if values.len() != 3 {
2404                return Err(JsValue::from_str(&format!(
2405                    "Invalid config: {name} must have exactly 3 elements [start, end, step]"
2406                )));
2407            }
2408            Ok((values[0], values[1], values[2]))
2409        }
2410        None => Ok((default_value, default_value, 0.0)),
2411    }
2412}
2413
2414#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2415fn sweep_from_js_config(config: InsyncIndexBatchConfig) -> Result<InsyncIndexBatchRange, JsValue> {
2416    Ok(InsyncIndexBatchRange {
2417        emo_divisor: parse_usize_js_range(
2418            config.emo_divisor_range,
2419            DEFAULT_EMO_DIVISOR,
2420            "emo_divisor_range",
2421        )?,
2422        emo_length: parse_usize_js_range(
2423            config.emo_length_range,
2424            DEFAULT_EMO_LENGTH,
2425            "emo_length_range",
2426        )?,
2427        fast_length: parse_usize_js_range(
2428            config.fast_length_range,
2429            DEFAULT_FAST_LENGTH,
2430            "fast_length_range",
2431        )?,
2432        slow_length: parse_usize_js_range(
2433            config.slow_length_range,
2434            DEFAULT_SLOW_LENGTH,
2435            "slow_length_range",
2436        )?,
2437        mfi_length: parse_usize_js_range(
2438            config.mfi_length_range,
2439            DEFAULT_MFI_LENGTH,
2440            "mfi_length_range",
2441        )?,
2442        bb_length: parse_usize_js_range(
2443            config.bb_length_range,
2444            DEFAULT_BB_LENGTH,
2445            "bb_length_range",
2446        )?,
2447        bb_multiplier: parse_f64_js_range(
2448            config.bb_multiplier_range,
2449            DEFAULT_BB_MULTIPLIER,
2450            "bb_multiplier_range",
2451        )?,
2452        cci_length: parse_usize_js_range(
2453            config.cci_length_range,
2454            DEFAULT_CCI_LENGTH,
2455            "cci_length_range",
2456        )?,
2457        dpo_length: parse_usize_js_range(
2458            config.dpo_length_range,
2459            DEFAULT_DPO_LENGTH,
2460            "dpo_length_range",
2461        )?,
2462        roc_length: parse_usize_js_range(
2463            config.roc_length_range,
2464            DEFAULT_ROC_LENGTH,
2465            "roc_length_range",
2466        )?,
2467        rsi_length: parse_usize_js_range(
2468            config.rsi_length_range,
2469            DEFAULT_RSI_LENGTH,
2470            "rsi_length_range",
2471        )?,
2472        stoch_length: parse_usize_js_range(
2473            config.stoch_length_range,
2474            DEFAULT_STOCH_LENGTH,
2475            "stoch_length_range",
2476        )?,
2477        stoch_d_length: parse_usize_js_range(
2478            config.stoch_d_length_range,
2479            DEFAULT_STOCH_D_LENGTH,
2480            "stoch_d_length_range",
2481        )?,
2482        stoch_k_length: parse_usize_js_range(
2483            config.stoch_k_length_range,
2484            DEFAULT_STOCH_K_LENGTH,
2485            "stoch_k_length_range",
2486        )?,
2487        sma_length: parse_usize_js_range(
2488            config.sma_length_range,
2489            DEFAULT_SMA_LENGTH,
2490            "sma_length_range",
2491        )?,
2492    })
2493}
2494
2495#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2496#[wasm_bindgen(js_name = "insync_index_js")]
2497pub fn insync_index_js(
2498    high: &[f64],
2499    low: &[f64],
2500    close: &[f64],
2501    volume: &[f64],
2502    emo_divisor: usize,
2503    emo_length: usize,
2504    fast_length: usize,
2505    slow_length: usize,
2506    mfi_length: usize,
2507    bb_length: usize,
2508    bb_multiplier: f64,
2509    cci_length: usize,
2510    dpo_length: usize,
2511    roc_length: usize,
2512    rsi_length: usize,
2513    stoch_length: usize,
2514    stoch_d_length: usize,
2515    stoch_k_length: usize,
2516    sma_length: usize,
2517) -> Result<Vec<f64>, JsValue> {
2518    let input = InsyncIndexInput::from_slices(
2519        high,
2520        low,
2521        close,
2522        volume,
2523        InsyncIndexParams {
2524            emo_divisor: Some(emo_divisor),
2525            emo_length: Some(emo_length),
2526            fast_length: Some(fast_length),
2527            slow_length: Some(slow_length),
2528            mfi_length: Some(mfi_length),
2529            bb_length: Some(bb_length),
2530            bb_multiplier: Some(bb_multiplier),
2531            cci_length: Some(cci_length),
2532            dpo_length: Some(dpo_length),
2533            roc_length: Some(roc_length),
2534            rsi_length: Some(rsi_length),
2535            stoch_length: Some(stoch_length),
2536            stoch_d_length: Some(stoch_d_length),
2537            stoch_k_length: Some(stoch_k_length),
2538            sma_length: Some(sma_length),
2539        },
2540    );
2541    insync_index(&input)
2542        .map(|out| out.values)
2543        .map_err(|e| JsValue::from_str(&e.to_string()))
2544}
2545
2546#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2547#[wasm_bindgen(js_name = "insync_index_batch_js")]
2548pub fn insync_index_batch_js(
2549    high: &[f64],
2550    low: &[f64],
2551    close: &[f64],
2552    volume: &[f64],
2553    config: JsValue,
2554) -> Result<JsValue, JsValue> {
2555    let config: InsyncIndexBatchConfig = serde_wasm_bindgen::from_value(config)
2556        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2557    let sweep = sweep_from_js_config(config)?;
2558    let batch = insync_index_batch_slice(high, low, close, volume, &sweep)
2559        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2560    serde_wasm_bindgen::to_value(&InsyncIndexBatchJsOutput {
2561        values: batch.values,
2562        rows: batch.rows,
2563        cols: batch.cols,
2564        combos: batch.combos,
2565    })
2566    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2567}
2568
2569#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2570#[wasm_bindgen]
2571pub fn insync_index_alloc(len: usize) -> *mut f64 {
2572    let mut buf = vec![0.0_f64; len];
2573    let ptr = buf.as_mut_ptr();
2574    std::mem::forget(buf);
2575    ptr
2576}
2577
2578#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2579#[wasm_bindgen]
2580pub fn insync_index_free(ptr: *mut f64, len: usize) {
2581    if ptr.is_null() {
2582        return;
2583    }
2584    unsafe {
2585        let _ = Vec::from_raw_parts(ptr, len, len);
2586    }
2587}
2588
2589#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2590#[wasm_bindgen]
2591pub fn insync_index_into(
2592    high_ptr: *const f64,
2593    low_ptr: *const f64,
2594    close_ptr: *const f64,
2595    volume_ptr: *const f64,
2596    out_ptr: *mut f64,
2597    len: usize,
2598    emo_divisor: usize,
2599    emo_length: usize,
2600    fast_length: usize,
2601    slow_length: usize,
2602    mfi_length: usize,
2603    bb_length: usize,
2604    bb_multiplier: f64,
2605    cci_length: usize,
2606    dpo_length: usize,
2607    roc_length: usize,
2608    rsi_length: usize,
2609    stoch_length: usize,
2610    stoch_d_length: usize,
2611    stoch_k_length: usize,
2612    sma_length: usize,
2613) -> Result<(), JsValue> {
2614    if high_ptr.is_null()
2615        || low_ptr.is_null()
2616        || close_ptr.is_null()
2617        || volume_ptr.is_null()
2618        || out_ptr.is_null()
2619    {
2620        return Err(JsValue::from_str(
2621            "null pointer passed to insync_index_into",
2622        ));
2623    }
2624    unsafe {
2625        let high = std::slice::from_raw_parts(high_ptr, len);
2626        let low = std::slice::from_raw_parts(low_ptr, len);
2627        let close = std::slice::from_raw_parts(close_ptr, len);
2628        let volume = std::slice::from_raw_parts(volume_ptr, len);
2629        let out = std::slice::from_raw_parts_mut(out_ptr, len);
2630        let input = InsyncIndexInput::from_slices(
2631            high,
2632            low,
2633            close,
2634            volume,
2635            InsyncIndexParams {
2636                emo_divisor: Some(emo_divisor),
2637                emo_length: Some(emo_length),
2638                fast_length: Some(fast_length),
2639                slow_length: Some(slow_length),
2640                mfi_length: Some(mfi_length),
2641                bb_length: Some(bb_length),
2642                bb_multiplier: Some(bb_multiplier),
2643                cci_length: Some(cci_length),
2644                dpo_length: Some(dpo_length),
2645                roc_length: Some(roc_length),
2646                rsi_length: Some(rsi_length),
2647                stoch_length: Some(stoch_length),
2648                stoch_d_length: Some(stoch_d_length),
2649                stoch_k_length: Some(stoch_k_length),
2650                sma_length: Some(sma_length),
2651            },
2652        );
2653        insync_index_into_slice(out, &input, Kernel::Auto)
2654            .map_err(|e| JsValue::from_str(&e.to_string()))
2655    }
2656}
2657
2658#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2659#[wasm_bindgen(js_name = "insync_index_into_host")]
2660pub fn insync_index_into_host(
2661    high: &[f64],
2662    low: &[f64],
2663    close: &[f64],
2664    volume: &[f64],
2665    out_ptr: *mut f64,
2666    emo_divisor: usize,
2667    emo_length: usize,
2668    fast_length: usize,
2669    slow_length: usize,
2670    mfi_length: usize,
2671    bb_length: usize,
2672    bb_multiplier: f64,
2673    cci_length: usize,
2674    dpo_length: usize,
2675    roc_length: usize,
2676    rsi_length: usize,
2677    stoch_length: usize,
2678    stoch_d_length: usize,
2679    stoch_k_length: usize,
2680    sma_length: usize,
2681) -> Result<(), JsValue> {
2682    if out_ptr.is_null() {
2683        return Err(JsValue::from_str(
2684            "null pointer passed to insync_index_into_host",
2685        ));
2686    }
2687    unsafe {
2688        let out = std::slice::from_raw_parts_mut(out_ptr, close.len());
2689        let input = InsyncIndexInput::from_slices(
2690            high,
2691            low,
2692            close,
2693            volume,
2694            InsyncIndexParams {
2695                emo_divisor: Some(emo_divisor),
2696                emo_length: Some(emo_length),
2697                fast_length: Some(fast_length),
2698                slow_length: Some(slow_length),
2699                mfi_length: Some(mfi_length),
2700                bb_length: Some(bb_length),
2701                bb_multiplier: Some(bb_multiplier),
2702                cci_length: Some(cci_length),
2703                dpo_length: Some(dpo_length),
2704                roc_length: Some(roc_length),
2705                rsi_length: Some(rsi_length),
2706                stoch_length: Some(stoch_length),
2707                stoch_d_length: Some(stoch_d_length),
2708                stoch_k_length: Some(stoch_k_length),
2709                sma_length: Some(sma_length),
2710            },
2711        );
2712        insync_index_into_slice(out, &input, Kernel::Auto)
2713            .map_err(|e| JsValue::from_str(&e.to_string()))
2714    }
2715}
2716
2717#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2718#[wasm_bindgen]
2719pub fn insync_index_batch_into(
2720    high_ptr: *const f64,
2721    low_ptr: *const f64,
2722    close_ptr: *const f64,
2723    volume_ptr: *const f64,
2724    out_ptr: *mut f64,
2725    len: usize,
2726    config: JsValue,
2727) -> Result<usize, JsValue> {
2728    if high_ptr.is_null()
2729        || low_ptr.is_null()
2730        || close_ptr.is_null()
2731        || volume_ptr.is_null()
2732        || out_ptr.is_null()
2733    {
2734        return Err(JsValue::from_str(
2735            "null pointer passed to insync_index_batch_into",
2736        ));
2737    }
2738    let config: InsyncIndexBatchConfig = serde_wasm_bindgen::from_value(config)
2739        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2740    let sweep = sweep_from_js_config(config)?;
2741    let combos = expand_grid_insync_index(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2742    let rows = combos.len();
2743    unsafe {
2744        let high = std::slice::from_raw_parts(high_ptr, len);
2745        let low = std::slice::from_raw_parts(low_ptr, len);
2746        let close = std::slice::from_raw_parts(close_ptr, len);
2747        let volume = std::slice::from_raw_parts(volume_ptr, len);
2748        let out = std::slice::from_raw_parts_mut(out_ptr, rows * len);
2749        let batch = insync_index_batch_slice(high, low, close, volume, &sweep)
2750            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2751        out.copy_from_slice(&batch.values);
2752    }
2753    Ok(rows)
2754}
2755
2756#[cfg(test)]
2757mod tests {
2758    use super::*;
2759
2760    fn sample_ohlcv(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
2761        let mut high = Vec::with_capacity(len);
2762        let mut low = Vec::with_capacity(len);
2763        let mut close = Vec::with_capacity(len);
2764        let mut volume = Vec::with_capacity(len);
2765        for i in 0..len {
2766            let base = 100.0 + (i as f64) * 0.2 + ((i as f64) * 0.07).sin();
2767            let spread = 1.0 + ((i as f64) * 0.03).cos().abs();
2768            low.push(base - spread);
2769            high.push(base + spread);
2770            close.push(base + ((i as f64) * 0.11).sin() * 0.35);
2771            volume.push(1_000.0 + (i as f64) * 3.0);
2772        }
2773        (high, low, close, volume)
2774    }
2775
2776    #[test]
2777    fn short_input_returns_baseline_not_error() {
2778        let high = [11.0, 12.0, 13.0];
2779        let low = [9.0, 10.0, 11.0];
2780        let close = [10.0, 11.0, 12.0];
2781        let volume = [100.0, 100.0, 100.0];
2782        let input = InsyncIndexInput::from_slices(
2783            &high,
2784            &low,
2785            &close,
2786            &volume,
2787            InsyncIndexParams::default(),
2788        );
2789        let out = insync_index(&input).unwrap();
2790        assert_eq!(out.values.len(), close.len());
2791        assert!(out.values[0].is_finite());
2792        assert_eq!(out.values[0], 50.0);
2793    }
2794
2795    #[test]
2796    fn stream_matches_batch_default() {
2797        let (high, low, close, volume) = sample_ohlcv(256);
2798        let input = InsyncIndexInput::from_slices(
2799            &high,
2800            &low,
2801            &close,
2802            &volume,
2803            InsyncIndexParams::default(),
2804        );
2805        let batch = insync_index(&input).unwrap();
2806
2807        let mut stream = InsyncIndexStream::try_new(InsyncIndexParams::default()).unwrap();
2808        let streamed: Vec<f64> = high
2809            .iter()
2810            .zip(&low)
2811            .zip(&close)
2812            .zip(&volume)
2813            .map(|(((h, l), c), v)| stream.update(*h, *l, *c, *v).unwrap_or(f64::NAN))
2814            .collect();
2815
2816        assert_eq!(batch.values.len(), streamed.len());
2817        for (a, b) in batch.values.iter().zip(streamed.iter()) {
2818            if a.is_nan() && b.is_nan() {
2819                continue;
2820            }
2821            assert!((a - b).abs() <= 1e-12, "batch {a} != stream {b}");
2822        }
2823    }
2824
2825    #[test]
2826    fn batch_single_param_matches_single() {
2827        let (high, low, close, volume) = sample_ohlcv(192);
2828        let input = InsyncIndexInput::from_slices(
2829            &high,
2830            &low,
2831            &close,
2832            &volume,
2833            InsyncIndexParams::default(),
2834        );
2835        let single = insync_index(&input).unwrap();
2836        let sweep = InsyncIndexBatchRange::default();
2837        let batch = insync_index_batch_slice(&high, &low, &close, &volume, &sweep).unwrap();
2838        assert_eq!(batch.rows, 1);
2839        assert_eq!(batch.cols, close.len());
2840        for (a, b) in batch.values[..close.len()].iter().zip(single.values.iter()) {
2841            if a.is_nan() && b.is_nan() {
2842                continue;
2843            }
2844            assert!((a - b).abs() <= 1e-12, "batch {a} != single {b}");
2845        }
2846    }
2847
2848    #[test]
2849    fn invalid_bb_multiplier_rejected() {
2850        let err = InsyncIndexStream::try_new(InsyncIndexParams {
2851            bb_multiplier: Some(0.0),
2852            ..InsyncIndexParams::default()
2853        })
2854        .unwrap_err();
2855        assert!(matches!(
2856            err,
2857            InsyncIndexError::InvalidFloat {
2858                name: "bb_multiplier",
2859                ..
2860            }
2861        ));
2862    }
2863}