1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, 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::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::detect_best_batch_kernel;
18#[cfg(feature = "python")]
19use crate::utilities::kernel_validation::validate_kernel;
20#[cfg(not(target_arch = "wasm32"))]
21use rayon::prelude::*;
22use std::collections::VecDeque;
23use thiserror::Error;
24
25const DEFAULT_SOURCE: &str = "close";
26const DEFAULT_LENGTH_1: usize = 48;
27const DEFAULT_LENGTH_2: usize = 21;
28const DEFAULT_LENGTH_3: usize = 9;
29const DEFAULT_LENGTH_4: usize = 6;
30const DEFAULT_TRIGGER_LENGTH: usize = 2;
31const PI: f64 = 3.14;
32
33#[derive(Debug, Clone)]
34pub enum MesaStochasticMultiLengthData<'a> {
35 Candles {
36 candles: &'a Candles,
37 source: &'a str,
38 },
39 Slice {
40 source: &'a [f64],
41 },
42}
43
44#[derive(Debug, Clone)]
45pub struct MesaStochasticMultiLengthOutput {
46 pub mesa_1: Vec<f64>,
47 pub mesa_2: Vec<f64>,
48 pub mesa_3: Vec<f64>,
49 pub mesa_4: Vec<f64>,
50 pub trigger_1: Vec<f64>,
51 pub trigger_2: Vec<f64>,
52 pub trigger_3: Vec<f64>,
53 pub trigger_4: Vec<f64>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg_attr(
58 all(target_arch = "wasm32", feature = "wasm"),
59 derive(Serialize, Deserialize)
60)]
61pub struct MesaStochasticMultiLengthParams {
62 pub length_1: Option<usize>,
63 pub length_2: Option<usize>,
64 pub length_3: Option<usize>,
65 pub length_4: Option<usize>,
66 pub trigger_length: Option<usize>,
67}
68
69impl Default for MesaStochasticMultiLengthParams {
70 fn default() -> Self {
71 Self {
72 length_1: Some(DEFAULT_LENGTH_1),
73 length_2: Some(DEFAULT_LENGTH_2),
74 length_3: Some(DEFAULT_LENGTH_3),
75 length_4: Some(DEFAULT_LENGTH_4),
76 trigger_length: Some(DEFAULT_TRIGGER_LENGTH),
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
82pub struct MesaStochasticMultiLengthInput<'a> {
83 pub data: MesaStochasticMultiLengthData<'a>,
84 pub params: MesaStochasticMultiLengthParams,
85}
86
87impl<'a> MesaStochasticMultiLengthInput<'a> {
88 #[inline]
89 pub fn from_candles(
90 candles: &'a Candles,
91 source: &'a str,
92 params: MesaStochasticMultiLengthParams,
93 ) -> Self {
94 Self {
95 data: MesaStochasticMultiLengthData::Candles { candles, source },
96 params,
97 }
98 }
99
100 #[inline]
101 pub fn from_slice(source: &'a [f64], params: MesaStochasticMultiLengthParams) -> Self {
102 Self {
103 data: MesaStochasticMultiLengthData::Slice { source },
104 params,
105 }
106 }
107
108 #[inline]
109 pub fn from_slices(source: &'a [f64], params: MesaStochasticMultiLengthParams) -> Self {
110 Self::from_slice(source, params)
111 }
112
113 #[inline]
114 pub fn with_default_candles(candles: &'a Candles) -> Self {
115 Self::from_candles(
116 candles,
117 DEFAULT_SOURCE,
118 MesaStochasticMultiLengthParams::default(),
119 )
120 }
121}
122
123#[derive(Debug, Clone, Copy)]
124struct ValidatedParams {
125 length_1: usize,
126 length_2: usize,
127 length_3: usize,
128 length_4: usize,
129 trigger_length: usize,
130}
131
132impl ValidatedParams {
133 fn from_params(
134 params: &MesaStochasticMultiLengthParams,
135 ) -> Result<Self, MesaStochasticMultiLengthError> {
136 let out = Self {
137 length_1: params.length_1.unwrap_or(DEFAULT_LENGTH_1),
138 length_2: params.length_2.unwrap_or(DEFAULT_LENGTH_2),
139 length_3: params.length_3.unwrap_or(DEFAULT_LENGTH_3),
140 length_4: params.length_4.unwrap_or(DEFAULT_LENGTH_4),
141 trigger_length: params.trigger_length.unwrap_or(DEFAULT_TRIGGER_LENGTH),
142 };
143 for (name, value) in [
144 ("length_1", out.length_1),
145 ("length_2", out.length_2),
146 ("length_3", out.length_3),
147 ("length_4", out.length_4),
148 ("trigger_length", out.trigger_length),
149 ] {
150 if value == 0 {
151 return Err(MesaStochasticMultiLengthError::InvalidPeriod {
152 name: name.to_string(),
153 value,
154 });
155 }
156 }
157 Ok(out)
158 }
159
160 fn into_params(self) -> MesaStochasticMultiLengthParams {
161 MesaStochasticMultiLengthParams {
162 length_1: Some(self.length_1),
163 length_2: Some(self.length_2),
164 length_3: Some(self.length_3),
165 length_4: Some(self.length_4),
166 trigger_length: Some(self.trigger_length),
167 }
168 }
169}
170
171#[derive(Debug, Error)]
172pub enum MesaStochasticMultiLengthError {
173 #[error("mesa_stochastic_multi_length: Input data slice is empty.")]
174 EmptyInputData,
175 #[error("mesa_stochastic_multi_length: All values are NaN.")]
176 AllValuesNaN,
177 #[error("mesa_stochastic_multi_length: Invalid period `{name}`: {value}")]
178 InvalidPeriod { name: String, value: usize },
179 #[error(
180 "mesa_stochastic_multi_length: Output length mismatch: expected={expected}, got={got}"
181 )]
182 OutputLengthMismatch { expected: usize, got: usize },
183 #[error("mesa_stochastic_multi_length: Invalid range: start={start}, end={end}, step={step}")]
184 InvalidRange {
185 start: String,
186 end: String,
187 step: String,
188 },
189 #[error("mesa_stochastic_multi_length: Invalid kernel for batch: {0:?}")]
190 InvalidKernelForBatch(Kernel),
191}
192
193#[inline(always)]
194fn extract_source<'a>(
195 input: &'a MesaStochasticMultiLengthInput<'a>,
196) -> Result<&'a [f64], MesaStochasticMultiLengthError> {
197 let source = match &input.data {
198 MesaStochasticMultiLengthData::Candles { candles, source } => source_type(candles, source),
199 MesaStochasticMultiLengthData::Slice { source } => *source,
200 };
201 if source.is_empty() {
202 return Err(MesaStochasticMultiLengthError::EmptyInputData);
203 }
204 Ok(source)
205}
206
207#[inline(always)]
208fn first_valid(source: &[f64]) -> Option<usize> {
209 source.iter().position(|value| value.is_finite())
210}
211
212#[derive(Debug, Clone, Copy)]
213pub struct MesaStochasticMultiLengthBuilder {
214 source: Option<&'static str>,
215 length_1: Option<usize>,
216 length_2: Option<usize>,
217 length_3: Option<usize>,
218 length_4: Option<usize>,
219 trigger_length: Option<usize>,
220 kernel: Kernel,
221}
222
223impl Default for MesaStochasticMultiLengthBuilder {
224 fn default() -> Self {
225 Self {
226 source: None,
227 length_1: None,
228 length_2: None,
229 length_3: None,
230 length_4: None,
231 trigger_length: None,
232 kernel: Kernel::Auto,
233 }
234 }
235}
236
237impl MesaStochasticMultiLengthBuilder {
238 #[inline(always)]
239 pub fn new() -> Self {
240 Self::default()
241 }
242
243 #[inline(always)]
244 pub fn source(mut self, value: &'static str) -> Self {
245 self.source = Some(value);
246 self
247 }
248
249 #[inline(always)]
250 pub fn length_1(mut self, value: usize) -> Self {
251 self.length_1 = Some(value);
252 self
253 }
254
255 #[inline(always)]
256 pub fn length_2(mut self, value: usize) -> Self {
257 self.length_2 = Some(value);
258 self
259 }
260
261 #[inline(always)]
262 pub fn length_3(mut self, value: usize) -> Self {
263 self.length_3 = Some(value);
264 self
265 }
266
267 #[inline(always)]
268 pub fn length_4(mut self, value: usize) -> Self {
269 self.length_4 = Some(value);
270 self
271 }
272
273 #[inline(always)]
274 pub fn trigger_length(mut self, value: usize) -> Self {
275 self.trigger_length = Some(value);
276 self
277 }
278
279 #[inline(always)]
280 pub fn kernel(mut self, value: Kernel) -> Self {
281 self.kernel = value;
282 self
283 }
284
285 #[inline(always)]
286 fn params(self) -> MesaStochasticMultiLengthParams {
287 MesaStochasticMultiLengthParams {
288 length_1: self.length_1,
289 length_2: self.length_2,
290 length_3: self.length_3,
291 length_4: self.length_4,
292 trigger_length: self.trigger_length,
293 }
294 }
295
296 #[inline(always)]
297 pub fn apply(
298 self,
299 candles: &Candles,
300 ) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
301 let input = MesaStochasticMultiLengthInput::from_candles(
302 candles,
303 self.source.unwrap_or(DEFAULT_SOURCE),
304 self.params(),
305 );
306 mesa_stochastic_multi_length_with_kernel(&input, self.kernel)
307 }
308
309 #[inline(always)]
310 pub fn apply_slice(
311 self,
312 source: &[f64],
313 ) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
314 let input = MesaStochasticMultiLengthInput::from_slice(source, self.params());
315 mesa_stochastic_multi_length_with_kernel(&input, self.kernel)
316 }
317
318 #[inline(always)]
319 pub fn into_stream(
320 self,
321 ) -> Result<MesaStochasticMultiLengthStream, MesaStochasticMultiLengthError> {
322 MesaStochasticMultiLengthStream::try_new(self.params())
323 }
324}
325
326#[inline(always)]
327fn nz(value: f64) -> f64 {
328 if value.is_finite() {
329 value
330 } else {
331 0.0
332 }
333}
334
335#[derive(Clone, Debug)]
336struct RollingSmaState {
337 length: usize,
338 window: VecDeque<f64>,
339 finite_sum: f64,
340 finite_count: usize,
341}
342
343impl RollingSmaState {
344 fn new(length: usize) -> Self {
345 Self {
346 length,
347 window: VecDeque::with_capacity(length + 1),
348 finite_sum: 0.0,
349 finite_count: 0,
350 }
351 }
352
353 fn update(&mut self, value: f64) -> f64 {
354 self.window.push_back(value);
355 if value.is_finite() {
356 self.finite_sum += value;
357 self.finite_count += 1;
358 }
359 if self.window.len() > self.length {
360 if let Some(old) = self.window.pop_front() {
361 if old.is_finite() {
362 self.finite_sum -= old;
363 self.finite_count -= 1;
364 }
365 }
366 }
367 if self.window.len() == self.length && self.finite_count == self.length {
368 self.finite_sum / self.length as f64
369 } else {
370 f64::NAN
371 }
372 }
373}
374
375#[derive(Clone, Debug)]
376struct MesaLineState {
377 length: usize,
378 filt_window: VecDeque<f64>,
379 prev_1: f64,
380 prev_2: f64,
381}
382
383impl MesaLineState {
384 fn new(length: usize) -> Self {
385 Self {
386 length,
387 filt_window: VecDeque::with_capacity(length + 1),
388 prev_1: f64::NAN,
389 prev_2: f64::NAN,
390 }
391 }
392
393 fn update(&mut self, filt: f64, c1: f64, c2: f64, c3: f64) -> f64 {
394 let filt_nz = nz(filt);
395 self.filt_window.push_back(filt_nz);
396 if self.filt_window.len() > self.length {
397 self.filt_window.pop_front();
398 }
399
400 let out = if filt.is_finite() {
401 let mut highest = filt;
402 let mut lowest = filt;
403 for &value in &self.filt_window {
404 if value > highest {
405 highest = value;
406 }
407 if value < lowest {
408 lowest = value;
409 }
410 }
411 if self.filt_window.len() < self.length {
412 if 0.0 > highest {
413 highest = 0.0;
414 }
415 if 0.0 < lowest {
416 lowest = 0.0;
417 }
418 }
419 let denom = highest - lowest;
420 if denom != 0.0 && denom.is_finite() {
421 let stoc = (filt - lowest) / denom;
422 if stoc.is_finite() {
423 c1.mul_add(stoc, c2.mul_add(nz(self.prev_1), c3 * nz(self.prev_2)))
424 } else {
425 f64::NAN
426 }
427 } else {
428 f64::NAN
429 }
430 } else {
431 f64::NAN
432 };
433
434 self.prev_2 = self.prev_1;
435 self.prev_1 = out;
436 out
437 }
438}
439
440#[derive(Clone, Debug)]
441struct SharedFilterState {
442 c1: f64,
443 c2: f64,
444 c3: f64,
445 hp_coef: f64,
446 hp_feedback_1: f64,
447 hp_feedback_2: f64,
448 prev_src_1: f64,
449 prev_src_2: f64,
450 prev_hp_1: f64,
451 prev_hp_2: f64,
452 prev_filt_1: f64,
453 prev_filt_2: f64,
454}
455
456impl SharedFilterState {
457 fn new() -> Self {
458 let alpha1 = ((0.707 * 2.0 * PI / 48.0).cos() + (0.707 * 2.0 * PI / 48.0).sin() - 1.0)
459 / (0.707 * 2.0 * PI / 48.0).cos();
460 let one_minus_alpha = 1.0 - alpha1;
461 let hp_coef = (1.0 - alpha1 * 0.5) * (1.0 - alpha1 * 0.5);
462 let a1 = (-1.414 * PI / 10.0).exp();
463 let b1 = 2.0 * a1 * (1.414 * PI / 10.0).cos();
464 let c2 = b1;
465 let c3 = -(a1 * a1);
466 let c1 = 1.0 - c2 - c3;
467 Self {
468 c1,
469 c2,
470 c3,
471 hp_coef,
472 hp_feedback_1: 2.0 * one_minus_alpha,
473 hp_feedback_2: -(one_minus_alpha * one_minus_alpha),
474 prev_src_1: f64::NAN,
475 prev_src_2: f64::NAN,
476 prev_hp_1: f64::NAN,
477 prev_hp_2: f64::NAN,
478 prev_filt_1: f64::NAN,
479 prev_filt_2: f64::NAN,
480 }
481 }
482
483 fn update(&mut self, source: f64) -> f64 {
484 let hp = if source.is_finite() {
485 self.hp_coef.mul_add(
486 source - 2.0 * nz(self.prev_src_1) + nz(self.prev_src_2),
487 self.hp_feedback_1
488 .mul_add(nz(self.prev_hp_1), self.hp_feedback_2 * nz(self.prev_hp_2)),
489 )
490 } else {
491 f64::NAN
492 };
493 let filt = if hp.is_finite() {
494 self.c1.mul_add(
495 hp,
496 self.c2
497 .mul_add(nz(self.prev_filt_1), self.c3 * nz(self.prev_filt_2)),
498 )
499 } else {
500 f64::NAN
501 };
502
503 self.prev_src_2 = self.prev_src_1;
504 self.prev_src_1 = source;
505 self.prev_hp_2 = self.prev_hp_1;
506 self.prev_hp_1 = hp;
507 self.prev_filt_2 = self.prev_filt_1;
508 self.prev_filt_1 = filt;
509 filt
510 }
511}
512
513#[derive(Clone, Debug)]
514pub struct MesaStochasticMultiLengthStream {
515 filter_state: SharedFilterState,
516 mesa_1_state: MesaLineState,
517 mesa_2_state: MesaLineState,
518 mesa_3_state: MesaLineState,
519 mesa_4_state: MesaLineState,
520 trigger_1_state: RollingSmaState,
521 trigger_2_state: RollingSmaState,
522 trigger_3_state: RollingSmaState,
523 trigger_4_state: RollingSmaState,
524}
525
526impl MesaStochasticMultiLengthStream {
527 pub fn try_new(
528 params: MesaStochasticMultiLengthParams,
529 ) -> Result<Self, MesaStochasticMultiLengthError> {
530 let params = ValidatedParams::from_params(¶ms)?;
531 Ok(Self {
532 filter_state: SharedFilterState::new(),
533 mesa_1_state: MesaLineState::new(params.length_1),
534 mesa_2_state: MesaLineState::new(params.length_2),
535 mesa_3_state: MesaLineState::new(params.length_3),
536 mesa_4_state: MesaLineState::new(params.length_4),
537 trigger_1_state: RollingSmaState::new(params.trigger_length),
538 trigger_2_state: RollingSmaState::new(params.trigger_length),
539 trigger_3_state: RollingSmaState::new(params.trigger_length),
540 trigger_4_state: RollingSmaState::new(params.trigger_length),
541 })
542 }
543
544 pub fn update(&mut self, source: f64) -> (f64, f64, f64, f64, f64, f64, f64, f64) {
545 let filt = self.filter_state.update(source);
546 let c1 = self.filter_state.c1;
547 let c2 = self.filter_state.c2;
548 let c3 = self.filter_state.c3;
549
550 let mesa_1 = self.mesa_1_state.update(filt, c1, c2, c3);
551 let mesa_2 = self.mesa_2_state.update(filt, c1, c2, c3);
552 let mesa_3 = self.mesa_3_state.update(filt, c1, c2, c3);
553 let mesa_4 = self.mesa_4_state.update(filt, c1, c2, c3);
554
555 let trigger_1 = self.trigger_1_state.update(mesa_1);
556 let trigger_2 = self.trigger_2_state.update(mesa_2);
557 let trigger_3 = self.trigger_3_state.update(mesa_3);
558 let trigger_4 = self.trigger_4_state.update(mesa_4);
559
560 (
561 mesa_1, mesa_2, mesa_3, mesa_4, trigger_1, trigger_2, trigger_3, trigger_4,
562 )
563 }
564}
565
566#[allow(clippy::too_many_arguments)]
567fn compute_mesa_stochastic_multi_length_into(
568 source: &[f64],
569 params: ValidatedParams,
570 mesa_1_out: &mut [f64],
571 mesa_2_out: &mut [f64],
572 mesa_3_out: &mut [f64],
573 mesa_4_out: &mut [f64],
574 trigger_1_out: &mut [f64],
575 trigger_2_out: &mut [f64],
576 trigger_3_out: &mut [f64],
577 trigger_4_out: &mut [f64],
578) -> Result<(), MesaStochasticMultiLengthError> {
579 let n = source.len();
580 if mesa_1_out.len() != n
581 || mesa_2_out.len() != n
582 || mesa_3_out.len() != n
583 || mesa_4_out.len() != n
584 || trigger_1_out.len() != n
585 || trigger_2_out.len() != n
586 || trigger_3_out.len() != n
587 || trigger_4_out.len() != n
588 {
589 let got = [
590 mesa_1_out.len(),
591 mesa_2_out.len(),
592 mesa_3_out.len(),
593 mesa_4_out.len(),
594 trigger_1_out.len(),
595 trigger_2_out.len(),
596 trigger_3_out.len(),
597 trigger_4_out.len(),
598 ]
599 .into_iter()
600 .max()
601 .unwrap_or(0);
602 return Err(MesaStochasticMultiLengthError::OutputLengthMismatch { expected: n, got });
603 }
604
605 mesa_1_out.fill(f64::NAN);
606 mesa_2_out.fill(f64::NAN);
607 mesa_3_out.fill(f64::NAN);
608 mesa_4_out.fill(f64::NAN);
609 trigger_1_out.fill(f64::NAN);
610 trigger_2_out.fill(f64::NAN);
611 trigger_3_out.fill(f64::NAN);
612 trigger_4_out.fill(f64::NAN);
613
614 let mut stream = MesaStochasticMultiLengthStream::try_new(params.into_params())?;
615 for (i, value) in source.iter().copied().enumerate() {
616 let (mesa_1, mesa_2, mesa_3, mesa_4, trigger_1, trigger_2, trigger_3, trigger_4) =
617 stream.update(value);
618 mesa_1_out[i] = mesa_1;
619 mesa_2_out[i] = mesa_2;
620 mesa_3_out[i] = mesa_3;
621 mesa_4_out[i] = mesa_4;
622 trigger_1_out[i] = trigger_1;
623 trigger_2_out[i] = trigger_2;
624 trigger_3_out[i] = trigger_3;
625 trigger_4_out[i] = trigger_4;
626 }
627
628 Ok(())
629}
630
631pub fn mesa_stochastic_multi_length(
632 input: &MesaStochasticMultiLengthInput,
633) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
634 mesa_stochastic_multi_length_with_kernel(input, Kernel::Auto)
635}
636
637pub fn mesa_stochastic_multi_length_with_kernel(
638 input: &MesaStochasticMultiLengthInput,
639 _kernel: Kernel,
640) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
641 let source = extract_source(input)?;
642 let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
643 let params = ValidatedParams::from_params(&input.params)?;
644 let n = source.len();
645 let mut out = MesaStochasticMultiLengthOutput {
646 mesa_1: vec![f64::NAN; n],
647 mesa_2: vec![f64::NAN; n],
648 mesa_3: vec![f64::NAN; n],
649 mesa_4: vec![f64::NAN; n],
650 trigger_1: vec![f64::NAN; n],
651 trigger_2: vec![f64::NAN; n],
652 trigger_3: vec![f64::NAN; n],
653 trigger_4: vec![f64::NAN; n],
654 };
655 compute_mesa_stochastic_multi_length_into(
656 source,
657 params,
658 &mut out.mesa_1,
659 &mut out.mesa_2,
660 &mut out.mesa_3,
661 &mut out.mesa_4,
662 &mut out.trigger_1,
663 &mut out.trigger_2,
664 &mut out.trigger_3,
665 &mut out.trigger_4,
666 )?;
667 Ok(out)
668}
669
670#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
671#[allow(clippy::too_many_arguments)]
672pub fn mesa_stochastic_multi_length_into(
673 mesa_1_out: &mut [f64],
674 mesa_2_out: &mut [f64],
675 mesa_3_out: &mut [f64],
676 mesa_4_out: &mut [f64],
677 trigger_1_out: &mut [f64],
678 trigger_2_out: &mut [f64],
679 trigger_3_out: &mut [f64],
680 trigger_4_out: &mut [f64],
681 input: &MesaStochasticMultiLengthInput,
682 kernel: Kernel,
683) -> Result<(), MesaStochasticMultiLengthError> {
684 mesa_stochastic_multi_length_into_slice(
685 mesa_1_out,
686 mesa_2_out,
687 mesa_3_out,
688 mesa_4_out,
689 trigger_1_out,
690 trigger_2_out,
691 trigger_3_out,
692 trigger_4_out,
693 input,
694 kernel,
695 )
696}
697
698#[allow(clippy::too_many_arguments)]
699pub fn mesa_stochastic_multi_length_into_slice(
700 mesa_1_out: &mut [f64],
701 mesa_2_out: &mut [f64],
702 mesa_3_out: &mut [f64],
703 mesa_4_out: &mut [f64],
704 trigger_1_out: &mut [f64],
705 trigger_2_out: &mut [f64],
706 trigger_3_out: &mut [f64],
707 trigger_4_out: &mut [f64],
708 input: &MesaStochasticMultiLengthInput,
709 _kernel: Kernel,
710) -> Result<(), MesaStochasticMultiLengthError> {
711 let source = extract_source(input)?;
712 let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
713 let params = ValidatedParams::from_params(&input.params)?;
714 compute_mesa_stochastic_multi_length_into(
715 source,
716 params,
717 mesa_1_out,
718 mesa_2_out,
719 mesa_3_out,
720 mesa_4_out,
721 trigger_1_out,
722 trigger_2_out,
723 trigger_3_out,
724 trigger_4_out,
725 )
726}
727
728#[derive(Clone, Debug)]
729pub struct MesaStochasticMultiLengthBatchRange {
730 pub length_1: (usize, usize, usize),
731 pub length_2: (usize, usize, usize),
732 pub length_3: (usize, usize, usize),
733 pub length_4: (usize, usize, usize),
734 pub trigger_length: (usize, usize, usize),
735}
736
737#[derive(Clone, Debug)]
738pub struct MesaStochasticMultiLengthBatchOutput {
739 pub mesa_1: Vec<f64>,
740 pub mesa_2: Vec<f64>,
741 pub mesa_3: Vec<f64>,
742 pub mesa_4: Vec<f64>,
743 pub trigger_1: Vec<f64>,
744 pub trigger_2: Vec<f64>,
745 pub trigger_3: Vec<f64>,
746 pub trigger_4: Vec<f64>,
747 pub combos: Vec<MesaStochasticMultiLengthParams>,
748 pub rows: usize,
749 pub cols: usize,
750}
751
752#[derive(Clone, Copy, Debug)]
753pub struct MesaStochasticMultiLengthBatchBuilder {
754 source: Option<&'static str>,
755 length_1: (usize, usize, usize),
756 length_2: (usize, usize, usize),
757 length_3: (usize, usize, usize),
758 length_4: (usize, usize, usize),
759 trigger_length: (usize, usize, usize),
760 kernel: Kernel,
761}
762
763impl Default for MesaStochasticMultiLengthBatchBuilder {
764 fn default() -> Self {
765 Self {
766 source: None,
767 length_1: (DEFAULT_LENGTH_1, DEFAULT_LENGTH_1, 0),
768 length_2: (DEFAULT_LENGTH_2, DEFAULT_LENGTH_2, 0),
769 length_3: (DEFAULT_LENGTH_3, DEFAULT_LENGTH_3, 0),
770 length_4: (DEFAULT_LENGTH_4, DEFAULT_LENGTH_4, 0),
771 trigger_length: (DEFAULT_TRIGGER_LENGTH, DEFAULT_TRIGGER_LENGTH, 0),
772 kernel: Kernel::Auto,
773 }
774 }
775}
776
777impl MesaStochasticMultiLengthBatchBuilder {
778 #[inline(always)]
779 pub fn new() -> Self {
780 Self::default()
781 }
782
783 #[inline(always)]
784 pub fn source(mut self, value: &'static str) -> Self {
785 self.source = Some(value);
786 self
787 }
788
789 #[inline(always)]
790 pub fn length_1_range(mut self, value: (usize, usize, usize)) -> Self {
791 self.length_1 = value;
792 self
793 }
794
795 #[inline(always)]
796 pub fn length_2_range(mut self, value: (usize, usize, usize)) -> Self {
797 self.length_2 = value;
798 self
799 }
800
801 #[inline(always)]
802 pub fn length_3_range(mut self, value: (usize, usize, usize)) -> Self {
803 self.length_3 = value;
804 self
805 }
806
807 #[inline(always)]
808 pub fn length_4_range(mut self, value: (usize, usize, usize)) -> Self {
809 self.length_4 = value;
810 self
811 }
812
813 #[inline(always)]
814 pub fn trigger_length_range(mut self, value: (usize, usize, usize)) -> Self {
815 self.trigger_length = value;
816 self
817 }
818
819 #[inline(always)]
820 pub fn kernel(mut self, value: Kernel) -> Self {
821 self.kernel = value;
822 self
823 }
824
825 #[inline(always)]
826 pub fn apply(
827 self,
828 candles: &Candles,
829 ) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
830 let source = source_type(candles, self.source.unwrap_or(DEFAULT_SOURCE));
831 mesa_stochastic_multi_length_batch_with_kernel(
832 source,
833 &MesaStochasticMultiLengthBatchRange {
834 length_1: self.length_1,
835 length_2: self.length_2,
836 length_3: self.length_3,
837 length_4: self.length_4,
838 trigger_length: self.trigger_length,
839 },
840 self.kernel,
841 )
842 }
843
844 #[inline(always)]
845 pub fn apply_slice(
846 self,
847 source: &[f64],
848 ) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
849 mesa_stochastic_multi_length_batch_with_kernel(
850 source,
851 &MesaStochasticMultiLengthBatchRange {
852 length_1: self.length_1,
853 length_2: self.length_2,
854 length_3: self.length_3,
855 length_4: self.length_4,
856 trigger_length: self.trigger_length,
857 },
858 self.kernel,
859 )
860 }
861}
862
863fn expand_one_range(
864 start: usize,
865 end: usize,
866 step: usize,
867) -> Result<Vec<usize>, MesaStochasticMultiLengthError> {
868 if start == 0 {
869 return Err(MesaStochasticMultiLengthError::InvalidRange {
870 start: start.to_string(),
871 end: end.to_string(),
872 step: step.to_string(),
873 });
874 }
875 if step == 0 {
876 if start != end {
877 return Err(MesaStochasticMultiLengthError::InvalidRange {
878 start: start.to_string(),
879 end: end.to_string(),
880 step: step.to_string(),
881 });
882 }
883 return Ok(vec![start]);
884 }
885 if start > end {
886 return Err(MesaStochasticMultiLengthError::InvalidRange {
887 start: start.to_string(),
888 end: end.to_string(),
889 step: step.to_string(),
890 });
891 }
892 let mut values = Vec::new();
893 let mut current = start;
894 while current <= end {
895 values.push(current);
896 current = match current.checked_add(step) {
897 Some(next) => next,
898 None => break,
899 };
900 }
901 Ok(values)
902}
903
904pub fn expand_grid(
905 sweep: &MesaStochasticMultiLengthBatchRange,
906) -> Result<Vec<MesaStochasticMultiLengthParams>, MesaStochasticMultiLengthError> {
907 let lengths_1 = expand_one_range(sweep.length_1.0, sweep.length_1.1, sweep.length_1.2)?;
908 let lengths_2 = expand_one_range(sweep.length_2.0, sweep.length_2.1, sweep.length_2.2)?;
909 let lengths_3 = expand_one_range(sweep.length_3.0, sweep.length_3.1, sweep.length_3.2)?;
910 let lengths_4 = expand_one_range(sweep.length_4.0, sweep.length_4.1, sweep.length_4.2)?;
911 let trigger_lengths = expand_one_range(
912 sweep.trigger_length.0,
913 sweep.trigger_length.1,
914 sweep.trigger_length.2,
915 )?;
916
917 let mut out = Vec::new();
918 for length_1 in lengths_1 {
919 for &length_2 in &lengths_2 {
920 for &length_3 in &lengths_3 {
921 for &length_4 in &lengths_4 {
922 for &trigger_length in &trigger_lengths {
923 out.push(MesaStochasticMultiLengthParams {
924 length_1: Some(length_1),
925 length_2: Some(length_2),
926 length_3: Some(length_3),
927 length_4: Some(length_4),
928 trigger_length: Some(trigger_length),
929 });
930 }
931 }
932 }
933 }
934 }
935 Ok(out)
936}
937
938fn batch_compute_rows(
939 source: &[f64],
940 sweep: &MesaStochasticMultiLengthBatchRange,
941 kernel: Kernel,
942 parallel: bool,
943) -> Result<
944 (
945 Vec<MesaStochasticMultiLengthParams>,
946 Vec<MesaStochasticMultiLengthOutput>,
947 ),
948 MesaStochasticMultiLengthError,
949> {
950 let combos = expand_grid(sweep)?;
951 let kernel = match kernel {
952 Kernel::Auto => detect_best_batch_kernel().to_non_batch(),
953 other if other.is_batch() => other.to_non_batch(),
954 other => other.to_non_batch(),
955 };
956 let compute = |params: &MesaStochasticMultiLengthParams| {
957 let input = MesaStochasticMultiLengthInput::from_slice(source, params.clone());
958 mesa_stochastic_multi_length_with_kernel(&input, kernel)
959 };
960 let rows = if parallel {
961 #[cfg(not(target_arch = "wasm32"))]
962 {
963 combos
964 .par_iter()
965 .map(compute)
966 .collect::<Result<Vec<_>, _>>()?
967 }
968 #[cfg(target_arch = "wasm32")]
969 {
970 combos.iter().map(compute).collect::<Result<Vec<_>, _>>()?
971 }
972 } else {
973 combos.iter().map(compute).collect::<Result<Vec<_>, _>>()?
974 };
975 Ok((combos, rows))
976}
977
978fn flatten_rows(
979 rows: &[MesaStochasticMultiLengthOutput],
980 cols: usize,
981) -> MesaStochasticMultiLengthOutput {
982 let total = rows.len() * cols;
983 let mut out = MesaStochasticMultiLengthOutput {
984 mesa_1: vec![f64::NAN; total],
985 mesa_2: vec![f64::NAN; total],
986 mesa_3: vec![f64::NAN; total],
987 mesa_4: vec![f64::NAN; total],
988 trigger_1: vec![f64::NAN; total],
989 trigger_2: vec![f64::NAN; total],
990 trigger_3: vec![f64::NAN; total],
991 trigger_4: vec![f64::NAN; total],
992 };
993 for (row_idx, row) in rows.iter().enumerate() {
994 let start = row_idx * cols;
995 let end = start + cols;
996 out.mesa_1[start..end].copy_from_slice(&row.mesa_1);
997 out.mesa_2[start..end].copy_from_slice(&row.mesa_2);
998 out.mesa_3[start..end].copy_from_slice(&row.mesa_3);
999 out.mesa_4[start..end].copy_from_slice(&row.mesa_4);
1000 out.trigger_1[start..end].copy_from_slice(&row.trigger_1);
1001 out.trigger_2[start..end].copy_from_slice(&row.trigger_2);
1002 out.trigger_3[start..end].copy_from_slice(&row.trigger_3);
1003 out.trigger_4[start..end].copy_from_slice(&row.trigger_4);
1004 }
1005 out
1006}
1007
1008pub fn mesa_stochastic_multi_length_batch_with_kernel(
1009 source: &[f64],
1010 sweep: &MesaStochasticMultiLengthBatchRange,
1011 kernel: Kernel,
1012) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
1013 let batch_kernel = match kernel {
1014 Kernel::Auto => detect_best_batch_kernel(),
1015 other if other.is_batch() => other,
1016 _ => {
1017 return Err(MesaStochasticMultiLengthError::InvalidKernelForBatch(
1018 kernel,
1019 ))
1020 }
1021 };
1022 mesa_stochastic_multi_length_batch_par_slice(source, sweep, batch_kernel.to_non_batch())
1023}
1024
1025pub fn mesa_stochastic_multi_length_batch_slice(
1026 source: &[f64],
1027 sweep: &MesaStochasticMultiLengthBatchRange,
1028 kernel: Kernel,
1029) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
1030 if source.is_empty() {
1031 return Err(MesaStochasticMultiLengthError::EmptyInputData);
1032 }
1033 let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
1034 let (combos, rows) = batch_compute_rows(source, sweep, kernel, false)?;
1035 let flat = flatten_rows(&rows, source.len());
1036 Ok(MesaStochasticMultiLengthBatchOutput {
1037 mesa_1: flat.mesa_1,
1038 mesa_2: flat.mesa_2,
1039 mesa_3: flat.mesa_3,
1040 mesa_4: flat.mesa_4,
1041 trigger_1: flat.trigger_1,
1042 trigger_2: flat.trigger_2,
1043 trigger_3: flat.trigger_3,
1044 trigger_4: flat.trigger_4,
1045 rows: combos.len(),
1046 cols: source.len(),
1047 combos,
1048 })
1049}
1050
1051pub fn mesa_stochastic_multi_length_batch_par_slice(
1052 source: &[f64],
1053 sweep: &MesaStochasticMultiLengthBatchRange,
1054 kernel: Kernel,
1055) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
1056 if source.is_empty() {
1057 return Err(MesaStochasticMultiLengthError::EmptyInputData);
1058 }
1059 let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
1060 let (combos, rows) = batch_compute_rows(source, sweep, kernel, true)?;
1061 let flat = flatten_rows(&rows, source.len());
1062 Ok(MesaStochasticMultiLengthBatchOutput {
1063 mesa_1: flat.mesa_1,
1064 mesa_2: flat.mesa_2,
1065 mesa_3: flat.mesa_3,
1066 mesa_4: flat.mesa_4,
1067 trigger_1: flat.trigger_1,
1068 trigger_2: flat.trigger_2,
1069 trigger_3: flat.trigger_3,
1070 trigger_4: flat.trigger_4,
1071 rows: combos.len(),
1072 cols: source.len(),
1073 combos,
1074 })
1075}
1076
1077#[allow(clippy::too_many_arguments)]
1078pub fn mesa_stochastic_multi_length_batch_into_slice(
1079 mesa_1_out: &mut [f64],
1080 mesa_2_out: &mut [f64],
1081 mesa_3_out: &mut [f64],
1082 mesa_4_out: &mut [f64],
1083 trigger_1_out: &mut [f64],
1084 trigger_2_out: &mut [f64],
1085 trigger_3_out: &mut [f64],
1086 trigger_4_out: &mut [f64],
1087 source: &[f64],
1088 sweep: &MesaStochasticMultiLengthBatchRange,
1089 kernel: Kernel,
1090) -> Result<(), MesaStochasticMultiLengthError> {
1091 if source.is_empty() {
1092 return Err(MesaStochasticMultiLengthError::EmptyInputData);
1093 }
1094 let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
1095 let combos = expand_grid(sweep)?;
1096 let expected = combos.len().checked_mul(source.len()).ok_or_else(|| {
1097 MesaStochasticMultiLengthError::InvalidRange {
1098 start: combos.len().to_string(),
1099 end: source.len().to_string(),
1100 step: "rows*cols".to_string(),
1101 }
1102 })?;
1103 let got = [
1104 mesa_1_out.len(),
1105 mesa_2_out.len(),
1106 mesa_3_out.len(),
1107 mesa_4_out.len(),
1108 trigger_1_out.len(),
1109 trigger_2_out.len(),
1110 trigger_3_out.len(),
1111 trigger_4_out.len(),
1112 ]
1113 .into_iter()
1114 .max()
1115 .unwrap_or(0);
1116 if mesa_1_out.len() != expected
1117 || mesa_2_out.len() != expected
1118 || mesa_3_out.len() != expected
1119 || mesa_4_out.len() != expected
1120 || trigger_1_out.len() != expected
1121 || trigger_2_out.len() != expected
1122 || trigger_3_out.len() != expected
1123 || trigger_4_out.len() != expected
1124 {
1125 return Err(MesaStochasticMultiLengthError::OutputLengthMismatch { expected, got });
1126 }
1127 let (combos, rows) = batch_compute_rows(source, sweep, kernel, false)?;
1128 let cols = source.len();
1129 for (row_idx, row) in rows.iter().enumerate() {
1130 let start = row_idx * cols;
1131 let end = start + cols;
1132 mesa_1_out[start..end].copy_from_slice(&row.mesa_1);
1133 mesa_2_out[start..end].copy_from_slice(&row.mesa_2);
1134 mesa_3_out[start..end].copy_from_slice(&row.mesa_3);
1135 mesa_4_out[start..end].copy_from_slice(&row.mesa_4);
1136 trigger_1_out[start..end].copy_from_slice(&row.trigger_1);
1137 trigger_2_out[start..end].copy_from_slice(&row.trigger_2);
1138 trigger_3_out[start..end].copy_from_slice(&row.trigger_3);
1139 trigger_4_out[start..end].copy_from_slice(&row.trigger_4);
1140 }
1141 debug_assert_eq!(combos.len() * cols, expected);
1142 Ok(())
1143}
1144
1145#[cfg(feature = "python")]
1146#[pyfunction(name = "mesa_stochastic_multi_length")]
1147#[pyo3(signature = (
1148 source,
1149 length_1=48,
1150 length_2=21,
1151 length_3=9,
1152 length_4=6,
1153 trigger_length=2,
1154 kernel=None
1155))]
1156pub fn mesa_stochastic_multi_length_py<'py>(
1157 py: Python<'py>,
1158 source: PyReadonlyArray1<'py, f64>,
1159 length_1: usize,
1160 length_2: usize,
1161 length_3: usize,
1162 length_4: usize,
1163 trigger_length: usize,
1164 kernel: Option<&str>,
1165) -> PyResult<Bound<'py, PyDict>> {
1166 let source = source.as_slice()?;
1167 let kernel = validate_kernel(kernel, false)?;
1168 let input = MesaStochasticMultiLengthInput::from_slice(
1169 source,
1170 MesaStochasticMultiLengthParams {
1171 length_1: Some(length_1),
1172 length_2: Some(length_2),
1173 length_3: Some(length_3),
1174 length_4: Some(length_4),
1175 trigger_length: Some(trigger_length),
1176 },
1177 );
1178 let out = py
1179 .allow_threads(|| mesa_stochastic_multi_length_with_kernel(&input, kernel))
1180 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1181 let dict = PyDict::new(py);
1182 dict.set_item("mesa_1", out.mesa_1.into_pyarray(py))?;
1183 dict.set_item("mesa_2", out.mesa_2.into_pyarray(py))?;
1184 dict.set_item("mesa_3", out.mesa_3.into_pyarray(py))?;
1185 dict.set_item("mesa_4", out.mesa_4.into_pyarray(py))?;
1186 dict.set_item("trigger_1", out.trigger_1.into_pyarray(py))?;
1187 dict.set_item("trigger_2", out.trigger_2.into_pyarray(py))?;
1188 dict.set_item("trigger_3", out.trigger_3.into_pyarray(py))?;
1189 dict.set_item("trigger_4", out.trigger_4.into_pyarray(py))?;
1190 Ok(dict)
1191}
1192
1193#[cfg(feature = "python")]
1194#[pyclass(name = "MesaStochasticMultiLengthStream")]
1195pub struct MesaStochasticMultiLengthStreamPy {
1196 stream: MesaStochasticMultiLengthStream,
1197}
1198
1199#[cfg(feature = "python")]
1200#[pymethods]
1201impl MesaStochasticMultiLengthStreamPy {
1202 #[new]
1203 #[pyo3(signature = (
1204 length_1=48,
1205 length_2=21,
1206 length_3=9,
1207 length_4=6,
1208 trigger_length=2
1209 ))]
1210 fn new(
1211 length_1: usize,
1212 length_2: usize,
1213 length_3: usize,
1214 length_4: usize,
1215 trigger_length: usize,
1216 ) -> PyResult<Self> {
1217 let stream = MesaStochasticMultiLengthStream::try_new(MesaStochasticMultiLengthParams {
1218 length_1: Some(length_1),
1219 length_2: Some(length_2),
1220 length_3: Some(length_3),
1221 length_4: Some(length_4),
1222 trigger_length: Some(trigger_length),
1223 })
1224 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1225 Ok(Self { stream })
1226 }
1227
1228 fn update<'py>(&mut self, py: Python<'py>, source: f64) -> PyResult<Bound<'py, PyDict>> {
1229 let values = self.stream.update(source);
1230 let dict = PyDict::new(py);
1231 dict.set_item("mesa_1", values.0)?;
1232 dict.set_item("mesa_2", values.1)?;
1233 dict.set_item("mesa_3", values.2)?;
1234 dict.set_item("mesa_4", values.3)?;
1235 dict.set_item("trigger_1", values.4)?;
1236 dict.set_item("trigger_2", values.5)?;
1237 dict.set_item("trigger_3", values.6)?;
1238 dict.set_item("trigger_4", values.7)?;
1239 Ok(dict)
1240 }
1241}
1242
1243#[cfg(feature = "python")]
1244#[pyfunction(name = "mesa_stochastic_multi_length_batch")]
1245#[pyo3(signature = (
1246 source,
1247 length_1_range=(48,48,0),
1248 length_2_range=(21,21,0),
1249 length_3_range=(9,9,0),
1250 length_4_range=(6,6,0),
1251 trigger_length_range=(2,2,0),
1252 kernel=None
1253))]
1254pub fn mesa_stochastic_multi_length_batch_py<'py>(
1255 py: Python<'py>,
1256 source: PyReadonlyArray1<'py, f64>,
1257 length_1_range: (usize, usize, usize),
1258 length_2_range: (usize, usize, usize),
1259 length_3_range: (usize, usize, usize),
1260 length_4_range: (usize, usize, usize),
1261 trigger_length_range: (usize, usize, usize),
1262 kernel: Option<&str>,
1263) -> PyResult<Bound<'py, PyDict>> {
1264 let source = source.as_slice()?;
1265 let kernel = validate_kernel(kernel, true)?;
1266 let sweep = MesaStochasticMultiLengthBatchRange {
1267 length_1: length_1_range,
1268 length_2: length_2_range,
1269 length_3: length_3_range,
1270 length_4: length_4_range,
1271 trigger_length: trigger_length_range,
1272 };
1273 let out = py
1274 .allow_threads(|| mesa_stochastic_multi_length_batch_with_kernel(source, &sweep, kernel))
1275 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1276 let dict = PyDict::new(py);
1277 dict.set_item(
1278 "mesa_1",
1279 out.mesa_1.into_pyarray(py).reshape((out.rows, out.cols))?,
1280 )?;
1281 dict.set_item(
1282 "mesa_2",
1283 out.mesa_2.into_pyarray(py).reshape((out.rows, out.cols))?,
1284 )?;
1285 dict.set_item(
1286 "mesa_3",
1287 out.mesa_3.into_pyarray(py).reshape((out.rows, out.cols))?,
1288 )?;
1289 dict.set_item(
1290 "mesa_4",
1291 out.mesa_4.into_pyarray(py).reshape((out.rows, out.cols))?,
1292 )?;
1293 dict.set_item(
1294 "trigger_1",
1295 out.trigger_1
1296 .into_pyarray(py)
1297 .reshape((out.rows, out.cols))?,
1298 )?;
1299 dict.set_item(
1300 "trigger_2",
1301 out.trigger_2
1302 .into_pyarray(py)
1303 .reshape((out.rows, out.cols))?,
1304 )?;
1305 dict.set_item(
1306 "trigger_3",
1307 out.trigger_3
1308 .into_pyarray(py)
1309 .reshape((out.rows, out.cols))?,
1310 )?;
1311 dict.set_item(
1312 "trigger_4",
1313 out.trigger_4
1314 .into_pyarray(py)
1315 .reshape((out.rows, out.cols))?,
1316 )?;
1317 dict.set_item(
1318 "length_1",
1319 out.combos
1320 .iter()
1321 .map(|p| p.length_1.unwrap())
1322 .collect::<Vec<_>>(),
1323 )?;
1324 dict.set_item(
1325 "length_2",
1326 out.combos
1327 .iter()
1328 .map(|p| p.length_2.unwrap())
1329 .collect::<Vec<_>>(),
1330 )?;
1331 dict.set_item(
1332 "length_3",
1333 out.combos
1334 .iter()
1335 .map(|p| p.length_3.unwrap())
1336 .collect::<Vec<_>>(),
1337 )?;
1338 dict.set_item(
1339 "length_4",
1340 out.combos
1341 .iter()
1342 .map(|p| p.length_4.unwrap())
1343 .collect::<Vec<_>>(),
1344 )?;
1345 dict.set_item(
1346 "trigger_length",
1347 out.combos
1348 .iter()
1349 .map(|p| p.trigger_length.unwrap())
1350 .collect::<Vec<_>>(),
1351 )?;
1352 dict.set_item("rows", out.rows)?;
1353 dict.set_item("cols", out.cols)?;
1354 Ok(dict)
1355}
1356
1357#[cfg(feature = "python")]
1358pub fn register_mesa_stochastic_multi_length_module(
1359 m: &Bound<'_, pyo3::types::PyModule>,
1360) -> PyResult<()> {
1361 m.add_function(wrap_pyfunction!(mesa_stochastic_multi_length_py, m)?)?;
1362 m.add_function(wrap_pyfunction!(mesa_stochastic_multi_length_batch_py, m)?)?;
1363 m.add_class::<MesaStochasticMultiLengthStreamPy>()?;
1364 Ok(())
1365}
1366
1367#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1368#[derive(Serialize, Deserialize)]
1369pub struct MesaStochasticMultiLengthJsOutput {
1370 pub mesa_1: Vec<f64>,
1371 pub mesa_2: Vec<f64>,
1372 pub mesa_3: Vec<f64>,
1373 pub mesa_4: Vec<f64>,
1374 pub trigger_1: Vec<f64>,
1375 pub trigger_2: Vec<f64>,
1376 pub trigger_3: Vec<f64>,
1377 pub trigger_4: Vec<f64>,
1378}
1379
1380#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1381#[derive(Serialize, Deserialize)]
1382pub struct MesaStochasticMultiLengthBatchConfig {
1383 pub length_1_range: Vec<f64>,
1384 pub length_2_range: Vec<f64>,
1385 pub length_3_range: Vec<f64>,
1386 pub length_4_range: Vec<f64>,
1387 pub trigger_length_range: Vec<f64>,
1388}
1389
1390#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1391#[derive(Serialize, Deserialize)]
1392pub struct MesaStochasticMultiLengthBatchJsOutput {
1393 pub mesa_1: Vec<f64>,
1394 pub mesa_2: Vec<f64>,
1395 pub mesa_3: Vec<f64>,
1396 pub mesa_4: Vec<f64>,
1397 pub trigger_1: Vec<f64>,
1398 pub trigger_2: Vec<f64>,
1399 pub trigger_3: Vec<f64>,
1400 pub trigger_4: Vec<f64>,
1401 pub length_1: Vec<usize>,
1402 pub length_2: Vec<usize>,
1403 pub length_3: Vec<usize>,
1404 pub length_4: Vec<usize>,
1405 pub trigger_length: Vec<usize>,
1406 pub rows: usize,
1407 pub cols: usize,
1408}
1409
1410#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1411fn js_vec3_to_usize(name: &str, values: &[f64]) -> Result<(usize, usize, usize), JsValue> {
1412 if values.len() != 3 {
1413 return Err(JsValue::from_str(&format!(
1414 "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1415 )));
1416 }
1417 let mut out = [0usize; 3];
1418 for (i, value) in values.iter().copied().enumerate() {
1419 if !value.is_finite() || value < 0.0 {
1420 return Err(JsValue::from_str(&format!(
1421 "Invalid config: {name}[{i}] must be a finite non-negative whole number"
1422 )));
1423 }
1424 let rounded = value.round();
1425 if (value - rounded).abs() > 1e-9 {
1426 return Err(JsValue::from_str(&format!(
1427 "Invalid config: {name}[{i}] must be a whole number"
1428 )));
1429 }
1430 out[i] = rounded as usize;
1431 }
1432 Ok((out[0], out[1], out[2]))
1433}
1434
1435#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1436#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_js")]
1437pub fn mesa_stochastic_multi_length_js(
1438 source: &[f64],
1439 length_1: usize,
1440 length_2: usize,
1441 length_3: usize,
1442 length_4: usize,
1443 trigger_length: usize,
1444) -> Result<JsValue, JsValue> {
1445 let input = MesaStochasticMultiLengthInput::from_slice(
1446 source,
1447 MesaStochasticMultiLengthParams {
1448 length_1: Some(length_1),
1449 length_2: Some(length_2),
1450 length_3: Some(length_3),
1451 length_4: Some(length_4),
1452 trigger_length: Some(trigger_length),
1453 },
1454 );
1455 let out = mesa_stochastic_multi_length_with_kernel(&input, Kernel::Auto)
1456 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1457 serde_wasm_bindgen::to_value(&MesaStochasticMultiLengthJsOutput {
1458 mesa_1: out.mesa_1,
1459 mesa_2: out.mesa_2,
1460 mesa_3: out.mesa_3,
1461 mesa_4: out.mesa_4,
1462 trigger_1: out.trigger_1,
1463 trigger_2: out.trigger_2,
1464 trigger_3: out.trigger_3,
1465 trigger_4: out.trigger_4,
1466 })
1467 .map_err(|e| JsValue::from_str(&e.to_string()))
1468}
1469
1470#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1471#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_batch_js")]
1472pub fn mesa_stochastic_multi_length_batch_js(
1473 source: &[f64],
1474 config: JsValue,
1475) -> Result<JsValue, JsValue> {
1476 let config: MesaStochasticMultiLengthBatchConfig =
1477 serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1478 let sweep = MesaStochasticMultiLengthBatchRange {
1479 length_1: js_vec3_to_usize("length_1_range", &config.length_1_range)?,
1480 length_2: js_vec3_to_usize("length_2_range", &config.length_2_range)?,
1481 length_3: js_vec3_to_usize("length_3_range", &config.length_3_range)?,
1482 length_4: js_vec3_to_usize("length_4_range", &config.length_4_range)?,
1483 trigger_length: js_vec3_to_usize("trigger_length_range", &config.trigger_length_range)?,
1484 };
1485 let out = mesa_stochastic_multi_length_batch_with_kernel(source, &sweep, Kernel::Auto)
1486 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1487 serde_wasm_bindgen::to_value(&MesaStochasticMultiLengthBatchJsOutput {
1488 mesa_1: out.mesa_1,
1489 mesa_2: out.mesa_2,
1490 mesa_3: out.mesa_3,
1491 mesa_4: out.mesa_4,
1492 trigger_1: out.trigger_1,
1493 trigger_2: out.trigger_2,
1494 trigger_3: out.trigger_3,
1495 trigger_4: out.trigger_4,
1496 length_1: out.combos.iter().map(|p| p.length_1.unwrap()).collect(),
1497 length_2: out.combos.iter().map(|p| p.length_2.unwrap()).collect(),
1498 length_3: out.combos.iter().map(|p| p.length_3.unwrap()).collect(),
1499 length_4: out.combos.iter().map(|p| p.length_4.unwrap()).collect(),
1500 trigger_length: out
1501 .combos
1502 .iter()
1503 .map(|p| p.trigger_length.unwrap())
1504 .collect(),
1505 rows: out.rows,
1506 cols: out.cols,
1507 })
1508 .map_err(|e| JsValue::from_str(&e.to_string()))
1509}
1510
1511#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1512#[wasm_bindgen]
1513pub fn mesa_stochastic_multi_length_alloc(len: usize) -> *mut f64 {
1514 let mut buf = vec![0.0; len];
1515 let ptr = buf.as_mut_ptr();
1516 std::mem::forget(buf);
1517 ptr
1518}
1519
1520#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1521#[wasm_bindgen]
1522pub fn mesa_stochastic_multi_length_free(ptr: *mut f64, len: usize) {
1523 if ptr.is_null() || len == 0 {
1524 return;
1525 }
1526 unsafe {
1527 drop(Vec::from_raw_parts(ptr, len, len));
1528 }
1529}
1530
1531#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1532#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_into")]
1533#[allow(clippy::too_many_arguments)]
1534pub fn mesa_stochastic_multi_length_into(
1535 source_ptr: *const f64,
1536 mesa_1_ptr: *mut f64,
1537 mesa_2_ptr: *mut f64,
1538 mesa_3_ptr: *mut f64,
1539 mesa_4_ptr: *mut f64,
1540 trigger_1_ptr: *mut f64,
1541 trigger_2_ptr: *mut f64,
1542 trigger_3_ptr: *mut f64,
1543 trigger_4_ptr: *mut f64,
1544 len: usize,
1545 length_1: usize,
1546 length_2: usize,
1547 length_3: usize,
1548 length_4: usize,
1549 trigger_length: usize,
1550) -> Result<(), JsValue> {
1551 if source_ptr.is_null()
1552 || mesa_1_ptr.is_null()
1553 || mesa_2_ptr.is_null()
1554 || mesa_3_ptr.is_null()
1555 || mesa_4_ptr.is_null()
1556 || trigger_1_ptr.is_null()
1557 || trigger_2_ptr.is_null()
1558 || trigger_3_ptr.is_null()
1559 || trigger_4_ptr.is_null()
1560 {
1561 return Err(JsValue::from_str(
1562 "null pointer passed to mesa_stochastic_multi_length_into",
1563 ));
1564 }
1565 let source = unsafe { std::slice::from_raw_parts(source_ptr, len) };
1566 let mesa_1_out = unsafe { std::slice::from_raw_parts_mut(mesa_1_ptr, len) };
1567 let mesa_2_out = unsafe { std::slice::from_raw_parts_mut(mesa_2_ptr, len) };
1568 let mesa_3_out = unsafe { std::slice::from_raw_parts_mut(mesa_3_ptr, len) };
1569 let mesa_4_out = unsafe { std::slice::from_raw_parts_mut(mesa_4_ptr, len) };
1570 let trigger_1_out = unsafe { std::slice::from_raw_parts_mut(trigger_1_ptr, len) };
1571 let trigger_2_out = unsafe { std::slice::from_raw_parts_mut(trigger_2_ptr, len) };
1572 let trigger_3_out = unsafe { std::slice::from_raw_parts_mut(trigger_3_ptr, len) };
1573 let trigger_4_out = unsafe { std::slice::from_raw_parts_mut(trigger_4_ptr, len) };
1574 let input = MesaStochasticMultiLengthInput::from_slice(
1575 source,
1576 MesaStochasticMultiLengthParams {
1577 length_1: Some(length_1),
1578 length_2: Some(length_2),
1579 length_3: Some(length_3),
1580 length_4: Some(length_4),
1581 trigger_length: Some(trigger_length),
1582 },
1583 );
1584 mesa_stochastic_multi_length_into_slice(
1585 mesa_1_out,
1586 mesa_2_out,
1587 mesa_3_out,
1588 mesa_4_out,
1589 trigger_1_out,
1590 trigger_2_out,
1591 trigger_3_out,
1592 trigger_4_out,
1593 &input,
1594 Kernel::Auto,
1595 )
1596 .map_err(|e| JsValue::from_str(&e.to_string()))
1597}
1598
1599#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1600#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_batch_into")]
1601#[allow(clippy::too_many_arguments)]
1602pub fn mesa_stochastic_multi_length_batch_into(
1603 source_ptr: *const f64,
1604 mesa_1_ptr: *mut f64,
1605 mesa_2_ptr: *mut f64,
1606 mesa_3_ptr: *mut f64,
1607 mesa_4_ptr: *mut f64,
1608 trigger_1_ptr: *mut f64,
1609 trigger_2_ptr: *mut f64,
1610 trigger_3_ptr: *mut f64,
1611 trigger_4_ptr: *mut f64,
1612 len: usize,
1613 length_1_start: usize,
1614 length_1_end: usize,
1615 length_1_step: usize,
1616 length_2_start: usize,
1617 length_2_end: usize,
1618 length_2_step: usize,
1619 length_3_start: usize,
1620 length_3_end: usize,
1621 length_3_step: usize,
1622 length_4_start: usize,
1623 length_4_end: usize,
1624 length_4_step: usize,
1625 trigger_length_start: usize,
1626 trigger_length_end: usize,
1627 trigger_length_step: usize,
1628) -> Result<usize, JsValue> {
1629 if source_ptr.is_null()
1630 || mesa_1_ptr.is_null()
1631 || mesa_2_ptr.is_null()
1632 || mesa_3_ptr.is_null()
1633 || mesa_4_ptr.is_null()
1634 || trigger_1_ptr.is_null()
1635 || trigger_2_ptr.is_null()
1636 || trigger_3_ptr.is_null()
1637 || trigger_4_ptr.is_null()
1638 {
1639 return Err(JsValue::from_str(
1640 "null pointer passed to mesa_stochastic_multi_length_batch_into",
1641 ));
1642 }
1643 let source = unsafe { std::slice::from_raw_parts(source_ptr, len) };
1644 let sweep = MesaStochasticMultiLengthBatchRange {
1645 length_1: (length_1_start, length_1_end, length_1_step),
1646 length_2: (length_2_start, length_2_end, length_2_step),
1647 length_3: (length_3_start, length_3_end, length_3_step),
1648 length_4: (length_4_start, length_4_end, length_4_step),
1649 trigger_length: (
1650 trigger_length_start,
1651 trigger_length_end,
1652 trigger_length_step,
1653 ),
1654 };
1655 let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1656 let rows = combos.len();
1657 let total = rows.checked_mul(len).ok_or_else(|| {
1658 JsValue::from_str("rows*cols overflow in mesa_stochastic_multi_length_batch_into")
1659 })?;
1660 let mesa_1_out = unsafe { std::slice::from_raw_parts_mut(mesa_1_ptr, total) };
1661 let mesa_2_out = unsafe { std::slice::from_raw_parts_mut(mesa_2_ptr, total) };
1662 let mesa_3_out = unsafe { std::slice::from_raw_parts_mut(mesa_3_ptr, total) };
1663 let mesa_4_out = unsafe { std::slice::from_raw_parts_mut(mesa_4_ptr, total) };
1664 let trigger_1_out = unsafe { std::slice::from_raw_parts_mut(trigger_1_ptr, total) };
1665 let trigger_2_out = unsafe { std::slice::from_raw_parts_mut(trigger_2_ptr, total) };
1666 let trigger_3_out = unsafe { std::slice::from_raw_parts_mut(trigger_3_ptr, total) };
1667 let trigger_4_out = unsafe { std::slice::from_raw_parts_mut(trigger_4_ptr, total) };
1668 mesa_stochastic_multi_length_batch_into_slice(
1669 mesa_1_out,
1670 mesa_2_out,
1671 mesa_3_out,
1672 mesa_4_out,
1673 trigger_1_out,
1674 trigger_2_out,
1675 trigger_3_out,
1676 trigger_4_out,
1677 source,
1678 &sweep,
1679 Kernel::Auto,
1680 )
1681 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1682 Ok(rows)
1683}
1684
1685#[cfg(test)]
1686mod tests {
1687 use super::*;
1688 use crate::utilities::data_loader::read_candles_from_csv;
1689
1690 fn manual_sma(values: &[f64], length: usize) -> Vec<f64> {
1691 let mut out = vec![f64::NAN; values.len()];
1692 if length == 0 || values.len() < length {
1693 return out;
1694 }
1695 for i in (length - 1)..values.len() {
1696 let window = &values[i + 1 - length..=i];
1697 if window.iter().all(|v| v.is_finite()) {
1698 out[i] = window.iter().sum::<f64>() / length as f64;
1699 }
1700 }
1701 out
1702 }
1703
1704 fn manual_reference(
1705 source: &[f64],
1706 params: ValidatedParams,
1707 ) -> MesaStochasticMultiLengthOutput {
1708 let n = source.len();
1709 let mut out = MesaStochasticMultiLengthOutput {
1710 mesa_1: vec![f64::NAN; n],
1711 mesa_2: vec![f64::NAN; n],
1712 mesa_3: vec![f64::NAN; n],
1713 mesa_4: vec![f64::NAN; n],
1714 trigger_1: vec![f64::NAN; n],
1715 trigger_2: vec![f64::NAN; n],
1716 trigger_3: vec![f64::NAN; n],
1717 trigger_4: vec![f64::NAN; n],
1718 };
1719
1720 let alpha1 = ((0.707 * 2.0 * PI / 48.0).cos() + (0.707 * 2.0 * PI / 48.0).sin() - 1.0)
1721 / (0.707 * 2.0 * PI / 48.0).cos();
1722 let hp_coef = (1.0 - alpha1 * 0.5) * (1.0 - alpha1 * 0.5);
1723 let one_minus_alpha = 1.0 - alpha1;
1724 let hp_feedback_1 = 2.0 * one_minus_alpha;
1725 let hp_feedback_2 = -(one_minus_alpha * one_minus_alpha);
1726 let a1 = (-1.414 * PI / 10.0).exp();
1727 let b1 = 2.0 * a1 * (1.414 * PI / 10.0).cos();
1728 let c2 = b1;
1729 let c3 = -(a1 * a1);
1730 let c1 = 1.0 - c2 - c3;
1731
1732 let mut hp = vec![f64::NAN; n];
1733 let mut filt = vec![f64::NAN; n];
1734 for i in 0..n {
1735 if source[i].is_finite() {
1736 let src1 = if i >= 1 { nz(source[i - 1]) } else { 0.0 };
1737 let src2 = if i >= 2 { nz(source[i - 2]) } else { 0.0 };
1738 let hp1 = if i >= 1 { nz(hp[i - 1]) } else { 0.0 };
1739 let hp2 = if i >= 2 { nz(hp[i - 2]) } else { 0.0 };
1740 hp[i] = hp_coef.mul_add(
1741 source[i] - 2.0 * src1 + src2,
1742 hp_feedback_1.mul_add(hp1, hp_feedback_2 * hp2),
1743 );
1744 let filt1 = if i >= 1 { nz(filt[i - 1]) } else { 0.0 };
1745 let filt2 = if i >= 2 { nz(filt[i - 2]) } else { 0.0 };
1746 filt[i] = c1.mul_add(hp[i], c2.mul_add(filt1, c3 * filt2));
1747 }
1748 }
1749
1750 fn mesa_from_filt(filt: &[f64], length: usize, c1: f64, c2: f64, c3: f64) -> Vec<f64> {
1751 let n = filt.len();
1752 let mut out = vec![f64::NAN; n];
1753 for i in 0..n {
1754 if !filt[i].is_finite() {
1755 continue;
1756 }
1757 let mut highest = filt[i];
1758 let mut lowest = filt[i];
1759 for count in 0..length {
1760 let value = if i >= count { nz(filt[i - count]) } else { 0.0 };
1761 if value > highest {
1762 highest = value;
1763 }
1764 if value < lowest {
1765 lowest = value;
1766 }
1767 }
1768 let denom = highest - lowest;
1769 if denom == 0.0 || !denom.is_finite() {
1770 continue;
1771 }
1772 let stoc = (filt[i] - lowest) / denom;
1773 if !stoc.is_finite() {
1774 continue;
1775 }
1776 let prev1 = if i >= 1 { nz(out[i - 1]) } else { 0.0 };
1777 let prev2 = if i >= 2 { nz(out[i - 2]) } else { 0.0 };
1778 out[i] = c1.mul_add(stoc, c2.mul_add(prev1, c3 * prev2));
1779 }
1780 out
1781 }
1782
1783 out.mesa_1 = mesa_from_filt(&filt, params.length_1, c1, c2, c3);
1784 out.mesa_2 = mesa_from_filt(&filt, params.length_2, c1, c2, c3);
1785 out.mesa_3 = mesa_from_filt(&filt, params.length_3, c1, c2, c3);
1786 out.mesa_4 = mesa_from_filt(&filt, params.length_4, c1, c2, c3);
1787 out.trigger_1 = manual_sma(&out.mesa_1, params.trigger_length);
1788 out.trigger_2 = manual_sma(&out.mesa_2, params.trigger_length);
1789 out.trigger_3 = manual_sma(&out.mesa_3, params.trigger_length);
1790 out.trigger_4 = manual_sma(&out.mesa_4, params.trigger_length);
1791 out
1792 }
1793
1794 fn assert_close(actual: &[f64], expected: &[f64]) {
1795 assert_eq!(actual.len(), expected.len());
1796 for (i, (&a, &b)) in actual.iter().zip(expected.iter()).enumerate() {
1797 if a.is_nan() && b.is_nan() {
1798 continue;
1799 }
1800 assert!(
1801 (a - b).abs() <= 1e-12,
1802 "mismatch at {i}: actual={a:?} expected={b:?}"
1803 );
1804 }
1805 }
1806
1807 #[test]
1808 fn manual_reference_matches_core() {
1809 let candles =
1810 read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1811 let source = &candles.close[..160];
1812 let params = MesaStochasticMultiLengthParams::default();
1813 let input = MesaStochasticMultiLengthInput::from_slice(source, params.clone());
1814 let got = mesa_stochastic_multi_length(&input).unwrap();
1815 let want = manual_reference(source, ValidatedParams::from_params(¶ms).unwrap());
1816 assert_close(&got.mesa_1, &want.mesa_1);
1817 assert_close(&got.mesa_2, &want.mesa_2);
1818 assert_close(&got.mesa_3, &want.mesa_3);
1819 assert_close(&got.mesa_4, &want.mesa_4);
1820 assert_close(&got.trigger_1, &want.trigger_1);
1821 assert_close(&got.trigger_2, &want.trigger_2);
1822 assert_close(&got.trigger_3, &want.trigger_3);
1823 assert_close(&got.trigger_4, &want.trigger_4);
1824 }
1825
1826 #[test]
1827 fn stream_matches_batch() {
1828 let candles =
1829 read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1830 let source = &candles.close[..160];
1831 let input = MesaStochasticMultiLengthInput::from_slice(
1832 source,
1833 MesaStochasticMultiLengthParams::default(),
1834 );
1835 let batch = mesa_stochastic_multi_length(&input).unwrap();
1836 let mut stream =
1837 MesaStochasticMultiLengthStream::try_new(MesaStochasticMultiLengthParams::default())
1838 .unwrap();
1839 let mut last = (
1840 f64::NAN,
1841 f64::NAN,
1842 f64::NAN,
1843 f64::NAN,
1844 f64::NAN,
1845 f64::NAN,
1846 f64::NAN,
1847 f64::NAN,
1848 );
1849 for &value in source {
1850 last = stream.update(value);
1851 }
1852 assert!(
1853 (last.0 - batch.mesa_1[source.len() - 1]).abs() <= 1e-12
1854 || (last.0.is_nan() && batch.mesa_1[source.len() - 1].is_nan())
1855 );
1856 assert!(
1857 (last.1 - batch.mesa_2[source.len() - 1]).abs() <= 1e-12
1858 || (last.1.is_nan() && batch.mesa_2[source.len() - 1].is_nan())
1859 );
1860 assert!(
1861 (last.2 - batch.mesa_3[source.len() - 1]).abs() <= 1e-12
1862 || (last.2.is_nan() && batch.mesa_3[source.len() - 1].is_nan())
1863 );
1864 assert!(
1865 (last.3 - batch.mesa_4[source.len() - 1]).abs() <= 1e-12
1866 || (last.3.is_nan() && batch.mesa_4[source.len() - 1].is_nan())
1867 );
1868 assert!(
1869 (last.4 - batch.trigger_1[source.len() - 1]).abs() <= 1e-12
1870 || (last.4.is_nan() && batch.trigger_1[source.len() - 1].is_nan())
1871 );
1872 assert!(
1873 (last.5 - batch.trigger_2[source.len() - 1]).abs() <= 1e-12
1874 || (last.5.is_nan() && batch.trigger_2[source.len() - 1].is_nan())
1875 );
1876 assert!(
1877 (last.6 - batch.trigger_3[source.len() - 1]).abs() <= 1e-12
1878 || (last.6.is_nan() && batch.trigger_3[source.len() - 1].is_nan())
1879 );
1880 assert!(
1881 (last.7 - batch.trigger_4[source.len() - 1]).abs() <= 1e-12
1882 || (last.7.is_nan() && batch.trigger_4[source.len() - 1].is_nan())
1883 );
1884 }
1885
1886 #[test]
1887 fn batch_first_row_matches_single() {
1888 let candles =
1889 read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1890 let source = &candles.close[..128];
1891 let single = mesa_stochastic_multi_length(&MesaStochasticMultiLengthInput::from_slice(
1892 source,
1893 MesaStochasticMultiLengthParams::default(),
1894 ))
1895 .unwrap();
1896 let batch = mesa_stochastic_multi_length_batch_with_kernel(
1897 source,
1898 &MesaStochasticMultiLengthBatchRange {
1899 length_1: (48, 50, 2),
1900 length_2: (21, 21, 0),
1901 length_3: (9, 9, 0),
1902 length_4: (6, 6, 0),
1903 trigger_length: (2, 2, 0),
1904 },
1905 Kernel::ScalarBatch,
1906 )
1907 .unwrap();
1908 let cols = source.len();
1909 assert_eq!(batch.rows, 2);
1910 assert_close(&batch.mesa_1[..cols], &single.mesa_1);
1911 assert_close(&batch.trigger_1[..cols], &single.trigger_1);
1912 }
1913
1914 #[test]
1915 fn into_slice_matches_owned_output() {
1916 let candles =
1917 read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1918 let source = &candles.close[..128];
1919 let input = MesaStochasticMultiLengthInput::from_slice(
1920 source,
1921 MesaStochasticMultiLengthParams::default(),
1922 );
1923 let single = mesa_stochastic_multi_length(&input).unwrap();
1924 let mut mesa_1 = vec![f64::NAN; source.len()];
1925 let mut mesa_2 = vec![f64::NAN; source.len()];
1926 let mut mesa_3 = vec![f64::NAN; source.len()];
1927 let mut mesa_4 = vec![f64::NAN; source.len()];
1928 let mut trigger_1 = vec![f64::NAN; source.len()];
1929 let mut trigger_2 = vec![f64::NAN; source.len()];
1930 let mut trigger_3 = vec![f64::NAN; source.len()];
1931 let mut trigger_4 = vec![f64::NAN; source.len()];
1932 mesa_stochastic_multi_length_into_slice(
1933 &mut mesa_1,
1934 &mut mesa_2,
1935 &mut mesa_3,
1936 &mut mesa_4,
1937 &mut trigger_1,
1938 &mut trigger_2,
1939 &mut trigger_3,
1940 &mut trigger_4,
1941 &input,
1942 Kernel::Auto,
1943 )
1944 .unwrap();
1945 assert_close(&mesa_1, &single.mesa_1);
1946 assert_close(&mesa_2, &single.mesa_2);
1947 assert_close(&mesa_3, &single.mesa_3);
1948 assert_close(&mesa_4, &single.mesa_4);
1949 assert_close(&trigger_1, &single.trigger_1);
1950 assert_close(&trigger_2, &single.trigger_2);
1951 assert_close(&trigger_3, &single.trigger_3);
1952 assert_close(&trigger_4, &single.trigger_4);
1953 }
1954
1955 #[test]
1956 fn rejects_invalid_period() {
1957 let candles =
1958 read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1959 let input = MesaStochasticMultiLengthInput::from_slice(
1960 &candles.close[..64],
1961 MesaStochasticMultiLengthParams {
1962 length_1: Some(0),
1963 ..MesaStochasticMultiLengthParams::default()
1964 },
1965 );
1966 let err = mesa_stochastic_multi_length(&input).unwrap_err();
1967 assert!(err.to_string().contains("Invalid period"));
1968 }
1969}