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