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#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::indicators::dispatch::{
18 compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
19 ParamValue,
20};
21use crate::indicators::moving_averages::ema::{EmaParams, EmaStream};
22use crate::indicators::moving_averages::hma::{HmaParams, HmaStream};
23use crate::indicators::moving_averages::sma::{SmaParams, SmaStream};
24use crate::indicators::stddev::{StdDevParams, StdDevStream};
25use crate::utilities::data_loader::Candles;
26use crate::utilities::enums::Kernel;
27use crate::utilities::helpers::{
28 alloc_with_nan_prefix, detect_best_batch_kernel, init_matrix_prefixes, make_uninit_matrix,
29};
30#[cfg(feature = "python")]
31use crate::utilities::kernel_validation::validate_kernel;
32#[cfg(not(target_arch = "wasm32"))]
33use rayon::prelude::*;
34use std::collections::VecDeque;
35use std::str::FromStr;
36use thiserror::Error;
37
38const DEFAULT_MA_TYPE: MovingAverageCrossProbabilityMaType =
39 MovingAverageCrossProbabilityMaType::Ema;
40const DEFAULT_SMOOTHING_WINDOW: usize = 7;
41const DEFAULT_SLOW_LENGTH: usize = 30;
42const DEFAULT_FAST_LENGTH: usize = 14;
43const DEFAULT_RESOLUTION: usize = 50;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[cfg_attr(
47 all(target_arch = "wasm32", feature = "wasm"),
48 derive(Serialize, Deserialize),
49 serde(rename_all = "snake_case")
50)]
51pub enum MovingAverageCrossProbabilityMaType {
52 Ema,
53 Sma,
54}
55
56impl Default for MovingAverageCrossProbabilityMaType {
57 fn default() -> Self {
58 DEFAULT_MA_TYPE
59 }
60}
61
62impl MovingAverageCrossProbabilityMaType {
63 #[inline(always)]
64 fn as_str(self) -> &'static str {
65 match self {
66 Self::Ema => "ema",
67 Self::Sma => "sma",
68 }
69 }
70}
71
72impl FromStr for MovingAverageCrossProbabilityMaType {
73 type Err = String;
74
75 fn from_str(value: &str) -> Result<Self, Self::Err> {
76 match value.trim().to_ascii_lowercase().as_str() {
77 "ema" => Ok(Self::Ema),
78 "sma" => Ok(Self::Sma),
79 _ => Err(format!("invalid ma_type: {value}")),
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
85pub enum MovingAverageCrossProbabilityData<'a> {
86 Candles { candles: &'a Candles },
87 Slice(&'a [f64]),
88}
89
90#[derive(Debug, Clone)]
91pub struct MovingAverageCrossProbabilityOutput {
92 pub value: Vec<f64>,
93 pub slow_ma: Vec<f64>,
94 pub fast_ma: Vec<f64>,
95 pub forecast: Vec<f64>,
96 pub upper: Vec<f64>,
97 pub lower: Vec<f64>,
98 pub direction: Vec<f64>,
99}
100
101#[derive(Debug, Clone)]
102#[cfg_attr(
103 all(target_arch = "wasm32", feature = "wasm"),
104 derive(Serialize, Deserialize)
105)]
106pub struct MovingAverageCrossProbabilityParams {
107 pub ma_type: Option<MovingAverageCrossProbabilityMaType>,
108 pub smoothing_window: Option<usize>,
109 pub slow_length: Option<usize>,
110 pub fast_length: Option<usize>,
111 pub resolution: Option<usize>,
112}
113
114impl Default for MovingAverageCrossProbabilityParams {
115 fn default() -> Self {
116 Self {
117 ma_type: Some(DEFAULT_MA_TYPE),
118 smoothing_window: Some(DEFAULT_SMOOTHING_WINDOW),
119 slow_length: Some(DEFAULT_SLOW_LENGTH),
120 fast_length: Some(DEFAULT_FAST_LENGTH),
121 resolution: Some(DEFAULT_RESOLUTION),
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
127pub struct MovingAverageCrossProbabilityInput<'a> {
128 pub data: MovingAverageCrossProbabilityData<'a>,
129 pub params: MovingAverageCrossProbabilityParams,
130}
131
132impl<'a> MovingAverageCrossProbabilityInput<'a> {
133 #[inline]
134 pub fn from_candles(candles: &'a Candles, params: MovingAverageCrossProbabilityParams) -> Self {
135 Self {
136 data: MovingAverageCrossProbabilityData::Candles { candles },
137 params,
138 }
139 }
140
141 #[inline]
142 pub fn from_slice(data: &'a [f64], params: MovingAverageCrossProbabilityParams) -> Self {
143 Self {
144 data: MovingAverageCrossProbabilityData::Slice(data),
145 params,
146 }
147 }
148
149 #[inline]
150 pub fn with_default_candles(candles: &'a Candles) -> Self {
151 Self::from_candles(candles, MovingAverageCrossProbabilityParams::default())
152 }
153}
154
155#[derive(Copy, Clone, Debug)]
156pub struct MovingAverageCrossProbabilityBuilder {
157 ma_type: Option<MovingAverageCrossProbabilityMaType>,
158 smoothing_window: Option<usize>,
159 slow_length: Option<usize>,
160 fast_length: Option<usize>,
161 resolution: Option<usize>,
162 kernel: Kernel,
163}
164
165impl Default for MovingAverageCrossProbabilityBuilder {
166 fn default() -> Self {
167 Self {
168 ma_type: None,
169 smoothing_window: None,
170 slow_length: None,
171 fast_length: None,
172 resolution: None,
173 kernel: Kernel::Auto,
174 }
175 }
176}
177
178impl MovingAverageCrossProbabilityBuilder {
179 #[inline(always)]
180 pub fn new() -> Self {
181 Self::default()
182 }
183
184 #[inline(always)]
185 pub fn ma_type(mut self, value: MovingAverageCrossProbabilityMaType) -> Self {
186 self.ma_type = Some(value);
187 self
188 }
189
190 #[inline(always)]
191 pub fn smoothing_window(mut self, value: usize) -> Self {
192 self.smoothing_window = Some(value);
193 self
194 }
195
196 #[inline(always)]
197 pub fn slow_length(mut self, value: usize) -> Self {
198 self.slow_length = Some(value);
199 self
200 }
201
202 #[inline(always)]
203 pub fn fast_length(mut self, value: usize) -> Self {
204 self.fast_length = Some(value);
205 self
206 }
207
208 #[inline(always)]
209 pub fn resolution(mut self, value: usize) -> Self {
210 self.resolution = Some(value);
211 self
212 }
213
214 #[inline(always)]
215 pub fn kernel(mut self, value: Kernel) -> Self {
216 self.kernel = value;
217 self
218 }
219
220 #[inline(always)]
221 pub fn apply(
222 self,
223 candles: &Candles,
224 ) -> Result<MovingAverageCrossProbabilityOutput, MovingAverageCrossProbabilityError> {
225 let input = MovingAverageCrossProbabilityInput::from_candles(
226 candles,
227 MovingAverageCrossProbabilityParams {
228 ma_type: self.ma_type,
229 smoothing_window: self.smoothing_window,
230 slow_length: self.slow_length,
231 fast_length: self.fast_length,
232 resolution: self.resolution,
233 },
234 );
235 moving_average_cross_probability_with_kernel(&input, self.kernel)
236 }
237
238 #[inline(always)]
239 pub fn apply_slice(
240 self,
241 data: &[f64],
242 ) -> Result<MovingAverageCrossProbabilityOutput, MovingAverageCrossProbabilityError> {
243 let input = MovingAverageCrossProbabilityInput::from_slice(
244 data,
245 MovingAverageCrossProbabilityParams {
246 ma_type: self.ma_type,
247 smoothing_window: self.smoothing_window,
248 slow_length: self.slow_length,
249 fast_length: self.fast_length,
250 resolution: self.resolution,
251 },
252 );
253 moving_average_cross_probability_with_kernel(&input, self.kernel)
254 }
255
256 #[inline(always)]
257 pub fn into_stream(
258 self,
259 ) -> Result<MovingAverageCrossProbabilityStream, MovingAverageCrossProbabilityError> {
260 MovingAverageCrossProbabilityStream::try_new(MovingAverageCrossProbabilityParams {
261 ma_type: self.ma_type,
262 smoothing_window: self.smoothing_window,
263 slow_length: self.slow_length,
264 fast_length: self.fast_length,
265 resolution: self.resolution,
266 })
267 }
268}
269
270#[derive(Debug, Error)]
271pub enum MovingAverageCrossProbabilityError {
272 #[error("moving_average_cross_probability: Input data slice is empty.")]
273 EmptyInputData,
274 #[error("moving_average_cross_probability: All values are NaN.")]
275 AllValuesNaN,
276 #[error("moving_average_cross_probability: Invalid smoothing_window: {smoothing_window}")]
277 InvalidSmoothingWindow { smoothing_window: usize },
278 #[error("moving_average_cross_probability: Invalid slow_length: {slow_length}")]
279 InvalidSlowLength { slow_length: usize },
280 #[error("moving_average_cross_probability: Invalid fast_length: {fast_length}")]
281 InvalidFastLength { fast_length: usize },
282 #[error("moving_average_cross_probability: Invalid resolution: {resolution}")]
283 InvalidResolution { resolution: usize },
284 #[error("moving_average_cross_probability: Invalid length order: fast_length={fast_length}, slow_length={slow_length}")]
285 InvalidLengthOrder {
286 fast_length: usize,
287 slow_length: usize,
288 },
289 #[error(
290 "moving_average_cross_probability: Output length mismatch: expected={expected}, got={got}"
291 )]
292 OutputLengthMismatch { expected: usize, got: usize },
293 #[error(
294 "moving_average_cross_probability: Invalid range: start={start}, end={end}, step={step}"
295 )]
296 InvalidRange {
297 start: String,
298 end: String,
299 step: String,
300 },
301 #[error("moving_average_cross_probability: Invalid kernel for batch: {0:?}")]
302 InvalidKernelForBatch(Kernel),
303}
304
305#[derive(Debug, Clone)]
306struct ResolvedParams {
307 ma_type: MovingAverageCrossProbabilityMaType,
308 smoothing_window: usize,
309 slow_length: usize,
310 fast_length: usize,
311 resolution: usize,
312 history_window_len: usize,
313 slow_alpha: f64,
314 slow_beta: f64,
315 fast_alpha: f64,
316 fast_beta: f64,
317 slow_ma_warmup: usize,
318 fast_ma_warmup: usize,
319 direction_warmup: usize,
320 forecast_warmup: usize,
321 probability_warmup: usize,
322}
323
324#[derive(Debug, Clone)]
325enum CurrentMaStream {
326 Ema(EmaStream),
327 Sma(SmaStream),
328}
329
330impl CurrentMaStream {
331 #[inline(always)]
332 fn try_new(
333 ma_type: MovingAverageCrossProbabilityMaType,
334 period: usize,
335 ) -> Result<Self, MovingAverageCrossProbabilityError> {
336 match ma_type {
337 MovingAverageCrossProbabilityMaType::Ema => Ok(Self::Ema(
338 EmaStream::try_new(EmaParams {
339 period: Some(period),
340 })
341 .map_err(|_| {
342 MovingAverageCrossProbabilityError::InvalidSlowLength {
343 slow_length: period,
344 }
345 })?,
346 )),
347 MovingAverageCrossProbabilityMaType::Sma => Ok(Self::Sma(
348 SmaStream::try_new(SmaParams {
349 period: Some(period),
350 })
351 .map_err(|_| {
352 MovingAverageCrossProbabilityError::InvalidSlowLength {
353 slow_length: period,
354 }
355 })?,
356 )),
357 }
358 }
359
360 #[inline(always)]
361 fn update(&mut self, value: f64) -> Option<f64> {
362 match self {
363 Self::Ema(stream) => stream.update(value),
364 Self::Sma(stream) => stream.update(value),
365 }
366 }
367}
368
369#[inline(always)]
370fn resolve_params(
371 params: &MovingAverageCrossProbabilityParams,
372) -> Result<ResolvedParams, MovingAverageCrossProbabilityError> {
373 let ma_type = params.ma_type.unwrap_or(DEFAULT_MA_TYPE);
374 let smoothing_window = params.smoothing_window.unwrap_or(DEFAULT_SMOOTHING_WINDOW);
375 let slow_length = params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH);
376 let fast_length = params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH);
377 let resolution = params.resolution.unwrap_or(DEFAULT_RESOLUTION);
378
379 if smoothing_window < 2 {
380 return Err(MovingAverageCrossProbabilityError::InvalidSmoothingWindow {
381 smoothing_window,
382 });
383 }
384 if slow_length < 2 {
385 return Err(MovingAverageCrossProbabilityError::InvalidSlowLength { slow_length });
386 }
387 if fast_length == 0 {
388 return Err(MovingAverageCrossProbabilityError::InvalidFastLength { fast_length });
389 }
390 if slow_length <= fast_length {
391 return Err(MovingAverageCrossProbabilityError::InvalidLengthOrder {
392 fast_length,
393 slow_length,
394 });
395 }
396 if resolution < 2 {
397 return Err(MovingAverageCrossProbabilityError::InvalidResolution { resolution });
398 }
399
400 let sqrt_len = (smoothing_window as f64).sqrt().floor() as usize;
401 let forecast_warmup = smoothing_window + sqrt_len - 1;
402 let slow_ma_warmup = slow_length - 1;
403 let fast_ma_warmup = fast_length - 1;
404 let direction_warmup = slow_ma_warmup.max(fast_ma_warmup);
405 let probability_warmup = forecast_warmup.max(direction_warmup).max(2 * slow_length);
406
407 Ok(ResolvedParams {
408 ma_type,
409 smoothing_window,
410 slow_length,
411 fast_length,
412 resolution,
413 history_window_len: 2 * slow_length + 1,
414 slow_alpha: 2.0 / (slow_length as f64 + 1.0),
415 slow_beta: 1.0 - 2.0 / (slow_length as f64 + 1.0),
416 fast_alpha: 2.0 / (fast_length as f64 + 1.0),
417 fast_beta: 1.0 - 2.0 / (fast_length as f64 + 1.0),
418 slow_ma_warmup,
419 fast_ma_warmup,
420 direction_warmup,
421 forecast_warmup,
422 probability_warmup,
423 })
424}
425
426#[inline(always)]
427fn extract_slice<'a>(
428 input: &'a MovingAverageCrossProbabilityInput<'a>,
429) -> Result<&'a [f64], MovingAverageCrossProbabilityError> {
430 let data = match &input.data {
431 MovingAverageCrossProbabilityData::Candles { candles } => candles.close.as_slice(),
432 MovingAverageCrossProbabilityData::Slice(values) => *values,
433 };
434 if data.is_empty() {
435 return Err(MovingAverageCrossProbabilityError::EmptyInputData);
436 }
437 if !data.iter().any(|v| v.is_finite()) {
438 return Err(MovingAverageCrossProbabilityError::AllValuesNaN);
439 }
440 Ok(data)
441}
442
443#[inline(always)]
444fn check_output_len(
445 out: &[f64],
446 expected: usize,
447) -> Result<(), MovingAverageCrossProbabilityError> {
448 if out.len() != expected {
449 return Err(MovingAverageCrossProbabilityError::OutputLengthMismatch {
450 expected,
451 got: out.len(),
452 });
453 }
454 Ok(())
455}
456
457#[inline(always)]
458fn truncated_ema_from_window(window: &VecDeque<f64>, alpha: f64, beta: f64) -> Option<f64> {
459 let mut iter = window.iter().rev();
460 let mut ema = *iter.next()?;
461 if !ema.is_finite() {
462 return None;
463 }
464 for value in iter {
465 if !value.is_finite() {
466 return None;
467 }
468 ema = alpha.mul_add(*value, beta * ema);
469 }
470 Some(ema)
471}
472
473#[inline(always)]
474fn probability_from_window(
475 window: &VecDeque<f64>,
476 params: &ResolvedParams,
477 lower: f64,
478 upper: f64,
479 direction: f64,
480) -> Option<f64> {
481 let step = (upper - lower) / (params.resolution - 1) as f64;
482 let mut hits = 0usize;
483
484 match params.ma_type {
485 MovingAverageCrossProbabilityMaType::Ema => {
486 let slow_current =
487 truncated_ema_from_window(window, params.slow_alpha, params.slow_beta)?;
488 let fast_current =
489 truncated_ema_from_window(window, params.fast_alpha, params.fast_beta)?;
490 for idx in 0..params.resolution {
491 let price = lower + step * idx as f64;
492 let slow_future = params
493 .slow_alpha
494 .mul_add(price, params.slow_beta * slow_current);
495 let fast_future = params
496 .fast_alpha
497 .mul_add(price, params.fast_beta * fast_current);
498 let crossed = if direction < 0.0 {
499 slow_future > fast_future
500 } else {
501 slow_future <= fast_future
502 };
503 if crossed {
504 hits += 1;
505 }
506 }
507 }
508 MovingAverageCrossProbabilityMaType::Sma => {
509 let slow_needed = params.slow_length.saturating_sub(1);
510 let fast_needed = params.fast_length.saturating_sub(1);
511 let mut slow_sum = 0.0;
512 let mut fast_sum = 0.0;
513 for (idx, value) in window.iter().enumerate() {
514 if idx < slow_needed {
515 slow_sum += *value;
516 }
517 if idx < fast_needed {
518 fast_sum += *value;
519 }
520 if idx >= slow_needed && idx >= fast_needed {
521 break;
522 }
523 }
524 for idx in 0..params.resolution {
525 let price = lower + step * idx as f64;
526 let slow_future = (price + slow_sum) / params.slow_length as f64;
527 let fast_future = (price + fast_sum) / params.fast_length as f64;
528 let crossed = if direction < 0.0 {
529 slow_future > fast_future
530 } else {
531 slow_future <= fast_future
532 };
533 if crossed {
534 hits += 1;
535 }
536 }
537 }
538 }
539
540 Some(100.0 * hits as f64 / params.resolution as f64)
541}
542
543#[inline(always)]
544fn moving_average_cross_probability_compute_into(
545 data: &[f64],
546 params: &ResolvedParams,
547 out_value: &mut [f64],
548 out_slow_ma: &mut [f64],
549 out_fast_ma: &mut [f64],
550 out_forecast: &mut [f64],
551 out_upper: &mut [f64],
552 out_lower: &mut [f64],
553 out_direction: &mut [f64],
554) -> Result<(), MovingAverageCrossProbabilityError> {
555 let len = data.len();
556 check_output_len(out_value, len)?;
557 check_output_len(out_slow_ma, len)?;
558 check_output_len(out_fast_ma, len)?;
559 check_output_len(out_forecast, len)?;
560 check_output_len(out_upper, len)?;
561 check_output_len(out_lower, len)?;
562 check_output_len(out_direction, len)?;
563
564 out_value.fill(f64::NAN);
565 out_slow_ma.fill(f64::NAN);
566 out_fast_ma.fill(f64::NAN);
567 out_forecast.fill(f64::NAN);
568 out_upper.fill(f64::NAN);
569 out_lower.fill(f64::NAN);
570 out_direction.fill(f64::NAN);
571
572 let mut slow_stream = CurrentMaStream::try_new(params.ma_type, params.slow_length)?;
573 let mut fast_stream = CurrentMaStream::try_new(params.ma_type, params.fast_length)?;
574 let mut hma_stream = HmaStream::try_new(HmaParams {
575 period: Some(params.smoothing_window),
576 })
577 .map_err(
578 |_| MovingAverageCrossProbabilityError::InvalidSmoothingWindow {
579 smoothing_window: params.smoothing_window,
580 },
581 )?;
582 let mut stddev_stream = StdDevStream::try_new(StdDevParams {
583 period: Some(params.smoothing_window),
584 nbdev: Some(4.0),
585 })
586 .map_err(
587 |_| MovingAverageCrossProbabilityError::InvalidSmoothingWindow {
588 smoothing_window: params.smoothing_window,
589 },
590 )?;
591
592 let mut history: VecDeque<f64> = VecDeque::with_capacity(params.history_window_len);
593 let mut invalid_history = 0usize;
594 let mut previous_hma = f64::NAN;
595
596 for (idx, &value) in data.iter().enumerate() {
597 if history.len() == params.history_window_len {
598 if let Some(old) = history.pop_back() {
599 if !old.is_finite() {
600 invalid_history = invalid_history.saturating_sub(1);
601 }
602 }
603 }
604 history.push_front(value);
605 if !value.is_finite() {
606 invalid_history += 1;
607 }
608
609 let slow_ma = slow_stream.update(value).unwrap_or(f64::NAN);
610 let fast_ma = fast_stream.update(value).unwrap_or(f64::NAN);
611 let current_hma = hma_stream.update(value).unwrap_or(f64::NAN);
612 let current_std = stddev_stream.update(value).unwrap_or(f64::NAN);
613
614 out_slow_ma[idx] = slow_ma;
615 out_fast_ma[idx] = fast_ma;
616
617 let direction = if slow_ma.is_finite() && fast_ma.is_finite() {
618 if fast_ma > slow_ma {
619 -1.0
620 } else {
621 1.0
622 }
623 } else {
624 f64::NAN
625 };
626 out_direction[idx] = direction;
627
628 if current_hma.is_finite() && previous_hma.is_finite() && current_std.is_finite() {
629 let forecast = current_hma + (current_hma - previous_hma);
630 out_forecast[idx] = forecast;
631 out_upper[idx] = forecast + current_std;
632 out_lower[idx] = forecast - current_std;
633
634 if direction.is_finite()
635 && history.len() == params.history_window_len
636 && invalid_history == 0
637 && out_upper[idx].is_finite()
638 && out_lower[idx].is_finite()
639 {
640 if let Some(probability) = probability_from_window(
641 &history,
642 params,
643 out_lower[idx],
644 out_upper[idx],
645 direction,
646 ) {
647 out_value[idx] = probability;
648 }
649 }
650 }
651
652 previous_hma = current_hma;
653 }
654
655 Ok(())
656}
657
658#[inline]
659pub fn moving_average_cross_probability(
660 input: &MovingAverageCrossProbabilityInput,
661) -> Result<MovingAverageCrossProbabilityOutput, MovingAverageCrossProbabilityError> {
662 moving_average_cross_probability_with_kernel(input, Kernel::Auto)
663}
664
665pub fn moving_average_cross_probability_with_kernel(
666 input: &MovingAverageCrossProbabilityInput,
667 _kernel: Kernel,
668) -> Result<MovingAverageCrossProbabilityOutput, MovingAverageCrossProbabilityError> {
669 let data = extract_slice(input)?;
670 let params = resolve_params(&input.params)?;
671 let len = data.len();
672
673 let mut value = alloc_with_nan_prefix(len, params.probability_warmup.min(len));
674 let mut slow_ma = alloc_with_nan_prefix(len, params.slow_ma_warmup.min(len));
675 let mut fast_ma = alloc_with_nan_prefix(len, params.fast_ma_warmup.min(len));
676 let mut forecast = alloc_with_nan_prefix(len, params.forecast_warmup.min(len));
677 let mut upper = alloc_with_nan_prefix(len, params.forecast_warmup.min(len));
678 let mut lower = alloc_with_nan_prefix(len, params.forecast_warmup.min(len));
679 let mut direction = alloc_with_nan_prefix(len, params.direction_warmup.min(len));
680
681 moving_average_cross_probability_compute_into(
682 data,
683 ¶ms,
684 &mut value,
685 &mut slow_ma,
686 &mut fast_ma,
687 &mut forecast,
688 &mut upper,
689 &mut lower,
690 &mut direction,
691 )?;
692
693 Ok(MovingAverageCrossProbabilityOutput {
694 value,
695 slow_ma,
696 fast_ma,
697 forecast,
698 upper,
699 lower,
700 direction,
701 })
702}
703
704#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
705pub fn moving_average_cross_probability_into(
706 input: &MovingAverageCrossProbabilityInput,
707 out_value: &mut [f64],
708 out_slow_ma: &mut [f64],
709 out_fast_ma: &mut [f64],
710 out_forecast: &mut [f64],
711 out_upper: &mut [f64],
712 out_lower: &mut [f64],
713 out_direction: &mut [f64],
714) -> Result<(), MovingAverageCrossProbabilityError> {
715 moving_average_cross_probability_into_slice(
716 out_value,
717 out_slow_ma,
718 out_fast_ma,
719 out_forecast,
720 out_upper,
721 out_lower,
722 out_direction,
723 input,
724 Kernel::Auto,
725 )
726}
727
728pub fn moving_average_cross_probability_into_slice(
729 out_value: &mut [f64],
730 out_slow_ma: &mut [f64],
731 out_fast_ma: &mut [f64],
732 out_forecast: &mut [f64],
733 out_upper: &mut [f64],
734 out_lower: &mut [f64],
735 out_direction: &mut [f64],
736 input: &MovingAverageCrossProbabilityInput,
737 _kernel: Kernel,
738) -> Result<(), MovingAverageCrossProbabilityError> {
739 let data = extract_slice(input)?;
740 let params = resolve_params(&input.params)?;
741 moving_average_cross_probability_compute_into(
742 data,
743 ¶ms,
744 out_value,
745 out_slow_ma,
746 out_fast_ma,
747 out_forecast,
748 out_upper,
749 out_lower,
750 out_direction,
751 )
752}
753
754#[derive(Debug)]
755pub struct MovingAverageCrossProbabilityStream {
756 params: ResolvedParams,
757 slow_stream: CurrentMaStream,
758 fast_stream: CurrentMaStream,
759 hma_stream: HmaStream,
760 stddev_stream: StdDevStream,
761 history: VecDeque<f64>,
762 invalid_history: usize,
763 previous_hma: f64,
764}
765
766impl MovingAverageCrossProbabilityStream {
767 pub fn try_new(
768 params: MovingAverageCrossProbabilityParams,
769 ) -> Result<Self, MovingAverageCrossProbabilityError> {
770 let params = resolve_params(¶ms)?;
771 Ok(Self {
772 slow_stream: CurrentMaStream::try_new(params.ma_type, params.slow_length)?,
773 fast_stream: CurrentMaStream::try_new(params.ma_type, params.fast_length)?,
774 hma_stream: HmaStream::try_new(HmaParams {
775 period: Some(params.smoothing_window),
776 })
777 .map_err(|_| {
778 MovingAverageCrossProbabilityError::InvalidSmoothingWindow {
779 smoothing_window: params.smoothing_window,
780 }
781 })?,
782 stddev_stream: StdDevStream::try_new(StdDevParams {
783 period: Some(params.smoothing_window),
784 nbdev: Some(4.0),
785 })
786 .map_err(|_| {
787 MovingAverageCrossProbabilityError::InvalidSmoothingWindow {
788 smoothing_window: params.smoothing_window,
789 }
790 })?,
791 history: VecDeque::with_capacity(params.history_window_len),
792 invalid_history: 0,
793 previous_hma: f64::NAN,
794 params,
795 })
796 }
797
798 #[inline(always)]
799 pub fn update(&mut self, value: f64) -> (f64, f64, f64, f64, f64, f64, f64) {
800 if self.history.len() == self.params.history_window_len {
801 if let Some(old) = self.history.pop_back() {
802 if !old.is_finite() {
803 self.invalid_history = self.invalid_history.saturating_sub(1);
804 }
805 }
806 }
807 self.history.push_front(value);
808 if !value.is_finite() {
809 self.invalid_history += 1;
810 }
811
812 let slow_ma = self.slow_stream.update(value).unwrap_or(f64::NAN);
813 let fast_ma = self.fast_stream.update(value).unwrap_or(f64::NAN);
814 let current_hma = self.hma_stream.update(value).unwrap_or(f64::NAN);
815 let current_std = self.stddev_stream.update(value).unwrap_or(f64::NAN);
816 let direction = if slow_ma.is_finite() && fast_ma.is_finite() {
817 if fast_ma > slow_ma {
818 -1.0
819 } else {
820 1.0
821 }
822 } else {
823 f64::NAN
824 };
825
826 let mut forecast = f64::NAN;
827 let mut upper = f64::NAN;
828 let mut lower = f64::NAN;
829 let mut probability = f64::NAN;
830 if current_hma.is_finite() && self.previous_hma.is_finite() && current_std.is_finite() {
831 forecast = current_hma + (current_hma - self.previous_hma);
832 upper = forecast + current_std;
833 lower = forecast - current_std;
834 if direction.is_finite()
835 && self.history.len() == self.params.history_window_len
836 && self.invalid_history == 0
837 {
838 probability =
839 probability_from_window(&self.history, &self.params, lower, upper, direction)
840 .unwrap_or(f64::NAN);
841 }
842 }
843 self.previous_hma = current_hma;
844
845 (
846 probability,
847 slow_ma,
848 fast_ma,
849 forecast,
850 upper,
851 lower,
852 direction,
853 )
854 }
855}
856
857#[derive(Clone, Debug)]
858pub struct MovingAverageCrossProbabilityBatchRange {
859 pub smoothing_window: (usize, usize, usize),
860 pub slow_length: (usize, usize, usize),
861 pub fast_length: (usize, usize, usize),
862 pub resolution: (usize, usize, usize),
863 pub ma_type: MovingAverageCrossProbabilityMaType,
864}
865
866impl Default for MovingAverageCrossProbabilityBatchRange {
867 fn default() -> Self {
868 Self {
869 smoothing_window: (DEFAULT_SMOOTHING_WINDOW, DEFAULT_SMOOTHING_WINDOW, 0),
870 slow_length: (DEFAULT_SLOW_LENGTH, DEFAULT_SLOW_LENGTH, 0),
871 fast_length: (DEFAULT_FAST_LENGTH, DEFAULT_FAST_LENGTH, 0),
872 resolution: (DEFAULT_RESOLUTION, DEFAULT_RESOLUTION, 0),
873 ma_type: DEFAULT_MA_TYPE,
874 }
875 }
876}
877
878#[derive(Clone, Debug, Default)]
879pub struct MovingAverageCrossProbabilityBatchBuilder {
880 range: MovingAverageCrossProbabilityBatchRange,
881 kernel: Kernel,
882}
883
884impl MovingAverageCrossProbabilityBatchBuilder {
885 pub fn new() -> Self {
886 Self::default()
887 }
888
889 pub fn kernel(mut self, kernel: Kernel) -> Self {
890 self.kernel = kernel;
891 self
892 }
893
894 #[inline(always)]
895 pub fn smoothing_window_range(mut self, start: usize, end: usize, step: usize) -> Self {
896 self.range.smoothing_window = (start, end, step);
897 self
898 }
899
900 #[inline(always)]
901 pub fn slow_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
902 self.range.slow_length = (start, end, step);
903 self
904 }
905
906 #[inline(always)]
907 pub fn fast_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
908 self.range.fast_length = (start, end, step);
909 self
910 }
911
912 #[inline(always)]
913 pub fn resolution_range(mut self, start: usize, end: usize, step: usize) -> Self {
914 self.range.resolution = (start, end, step);
915 self
916 }
917
918 #[inline(always)]
919 pub fn ma_type(mut self, value: MovingAverageCrossProbabilityMaType) -> Self {
920 self.range.ma_type = value;
921 self
922 }
923
924 pub fn apply_slice(
925 self,
926 data: &[f64],
927 ) -> Result<MovingAverageCrossProbabilityBatchOutput, MovingAverageCrossProbabilityError> {
928 moving_average_cross_probability_batch_with_kernel(data, &self.range, self.kernel)
929 }
930
931 pub fn apply(
932 self,
933 candles: &Candles,
934 ) -> Result<MovingAverageCrossProbabilityBatchOutput, MovingAverageCrossProbabilityError> {
935 self.apply_slice(candles.close.as_slice())
936 }
937}
938
939#[derive(Clone, Debug)]
940pub struct MovingAverageCrossProbabilityBatchOutput {
941 pub value: Vec<f64>,
942 pub slow_ma: Vec<f64>,
943 pub fast_ma: Vec<f64>,
944 pub forecast: Vec<f64>,
945 pub upper: Vec<f64>,
946 pub lower: Vec<f64>,
947 pub direction: Vec<f64>,
948 pub combos: Vec<MovingAverageCrossProbabilityParams>,
949 pub rows: usize,
950 pub cols: usize,
951}
952
953fn axis_usize(
954 (start, end, step): (usize, usize, usize),
955) -> Result<Vec<usize>, MovingAverageCrossProbabilityError> {
956 if step == 0 || start == end {
957 return Ok(vec![start]);
958 }
959 let mut values = Vec::new();
960 if start < end {
961 let mut current = start;
962 while current <= end {
963 values.push(current);
964 let next = current.saturating_add(step);
965 if next <= current {
966 break;
967 }
968 current = next;
969 }
970 } else {
971 let mut current = start;
972 while current >= end {
973 values.push(current);
974 let next = current.saturating_sub(step);
975 if next == current {
976 break;
977 }
978 current = next;
979 if current == 0 && end > 0 {
980 break;
981 }
982 }
983 }
984 if values.is_empty() {
985 return Err(MovingAverageCrossProbabilityError::InvalidRange {
986 start: start.to_string(),
987 end: end.to_string(),
988 step: step.to_string(),
989 });
990 }
991 Ok(values)
992}
993
994pub fn moving_average_cross_probability_expand_grid(
995 range: &MovingAverageCrossProbabilityBatchRange,
996) -> Result<Vec<MovingAverageCrossProbabilityParams>, MovingAverageCrossProbabilityError> {
997 let smoothing_windows = axis_usize(range.smoothing_window)?;
998 let slow_lengths = axis_usize(range.slow_length)?;
999 let fast_lengths = axis_usize(range.fast_length)?;
1000 let resolutions = axis_usize(range.resolution)?;
1001
1002 let cap = smoothing_windows
1003 .len()
1004 .checked_mul(slow_lengths.len())
1005 .and_then(|v| v.checked_mul(fast_lengths.len()))
1006 .and_then(|v| v.checked_mul(resolutions.len()))
1007 .ok_or_else(|| MovingAverageCrossProbabilityError::InvalidRange {
1008 start: "grid".to_string(),
1009 end: "overflow".to_string(),
1010 step: "n/a".to_string(),
1011 })?;
1012
1013 let mut out = Vec::with_capacity(cap);
1014 for &smoothing_window in &smoothing_windows {
1015 for &slow_length in &slow_lengths {
1016 for &fast_length in &fast_lengths {
1017 for &resolution in &resolutions {
1018 out.push(MovingAverageCrossProbabilityParams {
1019 ma_type: Some(range.ma_type),
1020 smoothing_window: Some(smoothing_window),
1021 slow_length: Some(slow_length),
1022 fast_length: Some(fast_length),
1023 resolution: Some(resolution),
1024 });
1025 }
1026 }
1027 }
1028 }
1029 Ok(out)
1030}
1031
1032#[inline(always)]
1033fn batch_shape(rows: usize, cols: usize) -> Result<usize, MovingAverageCrossProbabilityError> {
1034 rows.checked_mul(cols)
1035 .ok_or_else(|| MovingAverageCrossProbabilityError::InvalidRange {
1036 start: rows.to_string(),
1037 end: cols.to_string(),
1038 step: "overflow".to_string(),
1039 })
1040}
1041
1042#[inline(always)]
1043pub fn moving_average_cross_probability_batch_slice(
1044 data: &[f64],
1045 range: &MovingAverageCrossProbabilityBatchRange,
1046 _kernel: Kernel,
1047) -> Result<MovingAverageCrossProbabilityBatchOutput, MovingAverageCrossProbabilityError> {
1048 moving_average_cross_probability_batch_inner(data, range, false)
1049}
1050
1051#[inline(always)]
1052pub fn moving_average_cross_probability_batch_par_slice(
1053 data: &[f64],
1054 range: &MovingAverageCrossProbabilityBatchRange,
1055 _kernel: Kernel,
1056) -> Result<MovingAverageCrossProbabilityBatchOutput, MovingAverageCrossProbabilityError> {
1057 moving_average_cross_probability_batch_inner(data, range, true)
1058}
1059
1060pub fn moving_average_cross_probability_batch_with_kernel(
1061 data: &[f64],
1062 range: &MovingAverageCrossProbabilityBatchRange,
1063 kernel: Kernel,
1064) -> Result<MovingAverageCrossProbabilityBatchOutput, MovingAverageCrossProbabilityError> {
1065 let batch_kernel = match kernel {
1066 Kernel::Auto => detect_best_batch_kernel(),
1067 other if other.is_batch() => other,
1068 other => {
1069 return Err(MovingAverageCrossProbabilityError::InvalidKernelForBatch(
1070 other,
1071 ))
1072 }
1073 };
1074 moving_average_cross_probability_batch_inner(data, range, batch_kernel.is_batch())
1075}
1076
1077fn moving_average_cross_probability_batch_inner(
1078 data: &[f64],
1079 range: &MovingAverageCrossProbabilityBatchRange,
1080 parallel: bool,
1081) -> Result<MovingAverageCrossProbabilityBatchOutput, MovingAverageCrossProbabilityError> {
1082 let combos = moving_average_cross_probability_expand_grid(range)?;
1083 let rows = combos.len();
1084 let cols = data.len();
1085 let total = batch_shape(rows, cols)?;
1086
1087 let mut value_buf = make_uninit_matrix(rows, cols);
1088 let mut slow_buf = make_uninit_matrix(rows, cols);
1089 let mut fast_buf = make_uninit_matrix(rows, cols);
1090 let mut forecast_buf = make_uninit_matrix(rows, cols);
1091 let mut upper_buf = make_uninit_matrix(rows, cols);
1092 let mut lower_buf = make_uninit_matrix(rows, cols);
1093 let mut direction_buf = make_uninit_matrix(rows, cols);
1094
1095 let mut value_warmups = Vec::with_capacity(rows);
1096 let mut slow_warmups = Vec::with_capacity(rows);
1097 let mut fast_warmups = Vec::with_capacity(rows);
1098 let mut forecast_warmups = Vec::with_capacity(rows);
1099 let mut direction_warmups = Vec::with_capacity(rows);
1100 for combo in &combos {
1101 let resolved = resolve_params(combo)?;
1102 value_warmups.push(resolved.probability_warmup);
1103 slow_warmups.push(resolved.slow_ma_warmup);
1104 fast_warmups.push(resolved.fast_ma_warmup);
1105 forecast_warmups.push(resolved.forecast_warmup);
1106 direction_warmups.push(resolved.direction_warmup);
1107 }
1108
1109 init_matrix_prefixes(&mut value_buf, cols, &value_warmups);
1110 init_matrix_prefixes(&mut slow_buf, cols, &slow_warmups);
1111 init_matrix_prefixes(&mut fast_buf, cols, &fast_warmups);
1112 init_matrix_prefixes(&mut forecast_buf, cols, &forecast_warmups);
1113 init_matrix_prefixes(&mut upper_buf, cols, &forecast_warmups);
1114 init_matrix_prefixes(&mut lower_buf, cols, &forecast_warmups);
1115 init_matrix_prefixes(&mut direction_buf, cols, &direction_warmups);
1116
1117 let mut value = unsafe {
1118 Vec::from_raw_parts(
1119 value_buf.as_mut_ptr() as *mut f64,
1120 total,
1121 value_buf.capacity(),
1122 )
1123 };
1124 let mut slow_ma = unsafe {
1125 Vec::from_raw_parts(
1126 slow_buf.as_mut_ptr() as *mut f64,
1127 total,
1128 slow_buf.capacity(),
1129 )
1130 };
1131 let mut fast_ma = unsafe {
1132 Vec::from_raw_parts(
1133 fast_buf.as_mut_ptr() as *mut f64,
1134 total,
1135 fast_buf.capacity(),
1136 )
1137 };
1138 let mut forecast = unsafe {
1139 Vec::from_raw_parts(
1140 forecast_buf.as_mut_ptr() as *mut f64,
1141 total,
1142 forecast_buf.capacity(),
1143 )
1144 };
1145 let mut upper = unsafe {
1146 Vec::from_raw_parts(
1147 upper_buf.as_mut_ptr() as *mut f64,
1148 total,
1149 upper_buf.capacity(),
1150 )
1151 };
1152 let mut lower = unsafe {
1153 Vec::from_raw_parts(
1154 lower_buf.as_mut_ptr() as *mut f64,
1155 total,
1156 lower_buf.capacity(),
1157 )
1158 };
1159 let mut direction = unsafe {
1160 Vec::from_raw_parts(
1161 direction_buf.as_mut_ptr() as *mut f64,
1162 total,
1163 direction_buf.capacity(),
1164 )
1165 };
1166 std::mem::forget(value_buf);
1167 std::mem::forget(slow_buf);
1168 std::mem::forget(fast_buf);
1169 std::mem::forget(forecast_buf);
1170 std::mem::forget(upper_buf);
1171 std::mem::forget(lower_buf);
1172 std::mem::forget(direction_buf);
1173
1174 moving_average_cross_probability_batch_inner_into(
1175 data,
1176 range,
1177 parallel,
1178 &mut value,
1179 &mut slow_ma,
1180 &mut fast_ma,
1181 &mut forecast,
1182 &mut upper,
1183 &mut lower,
1184 &mut direction,
1185 )?;
1186
1187 Ok(MovingAverageCrossProbabilityBatchOutput {
1188 value,
1189 slow_ma,
1190 fast_ma,
1191 forecast,
1192 upper,
1193 lower,
1194 direction,
1195 combos,
1196 rows,
1197 cols,
1198 })
1199}
1200
1201pub fn moving_average_cross_probability_batch_into_slice(
1202 out_value: &mut [f64],
1203 out_slow_ma: &mut [f64],
1204 out_fast_ma: &mut [f64],
1205 out_forecast: &mut [f64],
1206 out_upper: &mut [f64],
1207 out_lower: &mut [f64],
1208 out_direction: &mut [f64],
1209 data: &[f64],
1210 range: &MovingAverageCrossProbabilityBatchRange,
1211 kernel: Kernel,
1212) -> Result<(), MovingAverageCrossProbabilityError> {
1213 let batch_kernel = match kernel {
1214 Kernel::Auto => detect_best_batch_kernel(),
1215 other if other.is_batch() => other,
1216 other => {
1217 return Err(MovingAverageCrossProbabilityError::InvalidKernelForBatch(
1218 other,
1219 ))
1220 }
1221 };
1222 moving_average_cross_probability_batch_inner_into(
1223 data,
1224 range,
1225 batch_kernel.is_batch(),
1226 out_value,
1227 out_slow_ma,
1228 out_fast_ma,
1229 out_forecast,
1230 out_upper,
1231 out_lower,
1232 out_direction,
1233 )?;
1234 Ok(())
1235}
1236
1237fn moving_average_cross_probability_batch_inner_into(
1238 data: &[f64],
1239 range: &MovingAverageCrossProbabilityBatchRange,
1240 parallel: bool,
1241 out_value: &mut [f64],
1242 out_slow_ma: &mut [f64],
1243 out_fast_ma: &mut [f64],
1244 out_forecast: &mut [f64],
1245 out_upper: &mut [f64],
1246 out_lower: &mut [f64],
1247 out_direction: &mut [f64],
1248) -> Result<Vec<MovingAverageCrossProbabilityParams>, MovingAverageCrossProbabilityError> {
1249 if data.is_empty() {
1250 return Err(MovingAverageCrossProbabilityError::EmptyInputData);
1251 }
1252 if !data.iter().any(|value| value.is_finite()) {
1253 return Err(MovingAverageCrossProbabilityError::AllValuesNaN);
1254 }
1255 let combos = moving_average_cross_probability_expand_grid(range)?;
1256 let rows = combos.len();
1257 let cols = data.len();
1258 let total = batch_shape(rows, cols)?;
1259 check_output_len(out_value, total)?;
1260 check_output_len(out_slow_ma, total)?;
1261 check_output_len(out_fast_ma, total)?;
1262 check_output_len(out_forecast, total)?;
1263 check_output_len(out_upper, total)?;
1264 check_output_len(out_lower, total)?;
1265 check_output_len(out_direction, total)?;
1266
1267 #[cfg(not(target_arch = "wasm32"))]
1268 if parallel {
1269 let results: Vec<Result<(), MovingAverageCrossProbabilityError>> = out_value
1270 .par_chunks_mut(cols)
1271 .zip(out_slow_ma.par_chunks_mut(cols))
1272 .zip(out_fast_ma.par_chunks_mut(cols))
1273 .zip(out_forecast.par_chunks_mut(cols))
1274 .zip(out_upper.par_chunks_mut(cols))
1275 .zip(out_lower.par_chunks_mut(cols))
1276 .zip(out_direction.par_chunks_mut(cols))
1277 .zip(combos.par_iter())
1278 .map(
1279 |(
1280 (
1281 (((((value_row, slow_row), fast_row), forecast_row), upper_row), lower_row),
1282 direction_row,
1283 ),
1284 combo,
1285 )| {
1286 let params = resolve_params(combo)?;
1287 moving_average_cross_probability_compute_into(
1288 data,
1289 ¶ms,
1290 value_row,
1291 slow_row,
1292 fast_row,
1293 forecast_row,
1294 upper_row,
1295 lower_row,
1296 direction_row,
1297 )
1298 },
1299 )
1300 .collect();
1301 for result in results {
1302 result?;
1303 }
1304 }
1305
1306 if !parallel || cfg!(target_arch = "wasm32") {
1307 for (row, combo) in combos.iter().enumerate() {
1308 let start = row * cols;
1309 let end = start + cols;
1310 let params = resolve_params(combo)?;
1311 moving_average_cross_probability_compute_into(
1312 data,
1313 ¶ms,
1314 &mut out_value[start..end],
1315 &mut out_slow_ma[start..end],
1316 &mut out_fast_ma[start..end],
1317 &mut out_forecast[start..end],
1318 &mut out_upper[start..end],
1319 &mut out_lower[start..end],
1320 &mut out_direction[start..end],
1321 )?;
1322 }
1323 }
1324
1325 Ok(combos)
1326}
1327
1328#[cfg(feature = "python")]
1329#[pyfunction(name = "moving_average_cross_probability")]
1330#[pyo3(signature = (
1331 data,
1332 ma_type="ema",
1333 smoothing_window=7,
1334 slow_length=30,
1335 fast_length=14,
1336 resolution=50,
1337 kernel=None
1338))]
1339pub fn moving_average_cross_probability_py<'py>(
1340 py: Python<'py>,
1341 data: PyReadonlyArray1<'py, f64>,
1342 ma_type: &str,
1343 smoothing_window: usize,
1344 slow_length: usize,
1345 fast_length: usize,
1346 resolution: usize,
1347 kernel: Option<&str>,
1348) -> PyResult<Bound<'py, PyDict>> {
1349 let data = data.as_slice()?;
1350 let input = MovingAverageCrossProbabilityInput::from_slice(
1351 data,
1352 MovingAverageCrossProbabilityParams {
1353 ma_type: Some(
1354 MovingAverageCrossProbabilityMaType::from_str(ma_type)
1355 .map_err(|e| PyValueError::new_err(e.to_string()))?,
1356 ),
1357 smoothing_window: Some(smoothing_window),
1358 slow_length: Some(slow_length),
1359 fast_length: Some(fast_length),
1360 resolution: Some(resolution),
1361 },
1362 );
1363 let kernel = validate_kernel(kernel, false)?;
1364 let out = py
1365 .allow_threads(|| moving_average_cross_probability_with_kernel(&input, kernel))
1366 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1367 let dict = PyDict::new(py);
1368 dict.set_item("value", out.value.into_pyarray(py))?;
1369 dict.set_item("slow_ma", out.slow_ma.into_pyarray(py))?;
1370 dict.set_item("fast_ma", out.fast_ma.into_pyarray(py))?;
1371 dict.set_item("forecast", out.forecast.into_pyarray(py))?;
1372 dict.set_item("upper", out.upper.into_pyarray(py))?;
1373 dict.set_item("lower", out.lower.into_pyarray(py))?;
1374 dict.set_item("direction", out.direction.into_pyarray(py))?;
1375 Ok(dict)
1376}
1377
1378#[cfg(feature = "python")]
1379#[pyclass(name = "MovingAverageCrossProbabilityStream")]
1380pub struct MovingAverageCrossProbabilityStreamPy {
1381 stream: MovingAverageCrossProbabilityStream,
1382}
1383
1384#[cfg(feature = "python")]
1385#[pymethods]
1386impl MovingAverageCrossProbabilityStreamPy {
1387 #[new]
1388 #[pyo3(signature = (
1389 ma_type="ema",
1390 smoothing_window=7,
1391 slow_length=30,
1392 fast_length=14,
1393 resolution=50
1394 ))]
1395 fn new(
1396 ma_type: &str,
1397 smoothing_window: usize,
1398 slow_length: usize,
1399 fast_length: usize,
1400 resolution: usize,
1401 ) -> PyResult<Self> {
1402 let stream =
1403 MovingAverageCrossProbabilityStream::try_new(MovingAverageCrossProbabilityParams {
1404 ma_type: Some(
1405 MovingAverageCrossProbabilityMaType::from_str(ma_type)
1406 .map_err(|e| PyValueError::new_err(e.to_string()))?,
1407 ),
1408 smoothing_window: Some(smoothing_window),
1409 slow_length: Some(slow_length),
1410 fast_length: Some(fast_length),
1411 resolution: Some(resolution),
1412 })
1413 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1414 Ok(Self { stream })
1415 }
1416
1417 fn update(&mut self, value: f64) -> (f64, f64, f64, f64, f64, f64, f64) {
1418 self.stream.update(value)
1419 }
1420}
1421
1422#[cfg(feature = "python")]
1423#[pyfunction(name = "moving_average_cross_probability_batch")]
1424#[pyo3(signature = (
1425 data,
1426 smoothing_window_range=(7,7,0),
1427 slow_length_range=(30,30,0),
1428 fast_length_range=(14,14,0),
1429 resolution_range=(50,50,0),
1430 ma_type="ema",
1431 kernel=None
1432))]
1433pub fn moving_average_cross_probability_batch_py<'py>(
1434 py: Python<'py>,
1435 data: PyReadonlyArray1<'py, f64>,
1436 smoothing_window_range: (usize, usize, usize),
1437 slow_length_range: (usize, usize, usize),
1438 fast_length_range: (usize, usize, usize),
1439 resolution_range: (usize, usize, usize),
1440 ma_type: &str,
1441 kernel: Option<&str>,
1442) -> PyResult<Bound<'py, PyDict>> {
1443 let data = data.as_slice()?;
1444 let sweep = MovingAverageCrossProbabilityBatchRange {
1445 smoothing_window: smoothing_window_range,
1446 slow_length: slow_length_range,
1447 fast_length: fast_length_range,
1448 resolution: resolution_range,
1449 ma_type: MovingAverageCrossProbabilityMaType::from_str(ma_type)
1450 .map_err(|e| PyValueError::new_err(e.to_string()))?,
1451 };
1452 let combos = moving_average_cross_probability_expand_grid(&sweep)
1453 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1454 let rows = combos.len();
1455 let cols = data.len();
1456 let total = rows
1457 .checked_mul(cols)
1458 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1459
1460 let out_value = unsafe { PyArray1::<f64>::new(py, [total], false) };
1461 let out_slow_ma = unsafe { PyArray1::<f64>::new(py, [total], false) };
1462 let out_fast_ma = unsafe { PyArray1::<f64>::new(py, [total], false) };
1463 let out_forecast = unsafe { PyArray1::<f64>::new(py, [total], false) };
1464 let out_upper = unsafe { PyArray1::<f64>::new(py, [total], false) };
1465 let out_lower = unsafe { PyArray1::<f64>::new(py, [total], false) };
1466 let out_direction = unsafe { PyArray1::<f64>::new(py, [total], false) };
1467
1468 let value_slice = unsafe { out_value.as_slice_mut()? };
1469 let slow_slice = unsafe { out_slow_ma.as_slice_mut()? };
1470 let fast_slice = unsafe { out_fast_ma.as_slice_mut()? };
1471 let forecast_slice = unsafe { out_forecast.as_slice_mut()? };
1472 let upper_slice = unsafe { out_upper.as_slice_mut()? };
1473 let lower_slice = unsafe { out_lower.as_slice_mut()? };
1474 let direction_slice = unsafe { out_direction.as_slice_mut()? };
1475 let kernel = validate_kernel(kernel, true)?;
1476
1477 py.allow_threads(|| {
1478 let batch_kernel = match kernel {
1479 Kernel::Auto => detect_best_batch_kernel(),
1480 other => other,
1481 };
1482 moving_average_cross_probability_batch_inner_into(
1483 data,
1484 &sweep,
1485 batch_kernel.is_batch(),
1486 value_slice,
1487 slow_slice,
1488 fast_slice,
1489 forecast_slice,
1490 upper_slice,
1491 lower_slice,
1492 direction_slice,
1493 )
1494 })
1495 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1496
1497 let dict = PyDict::new(py);
1498 dict.set_item("value", out_value.reshape((rows, cols))?)?;
1499 dict.set_item("slow_ma", out_slow_ma.reshape((rows, cols))?)?;
1500 dict.set_item("fast_ma", out_fast_ma.reshape((rows, cols))?)?;
1501 dict.set_item("forecast", out_forecast.reshape((rows, cols))?)?;
1502 dict.set_item("upper", out_upper.reshape((rows, cols))?)?;
1503 dict.set_item("lower", out_lower.reshape((rows, cols))?)?;
1504 dict.set_item("direction", out_direction.reshape((rows, cols))?)?;
1505 dict.set_item(
1506 "smoothing_windows",
1507 combos
1508 .iter()
1509 .map(|combo| combo.smoothing_window.unwrap_or(DEFAULT_SMOOTHING_WINDOW))
1510 .collect::<Vec<_>>()
1511 .into_pyarray(py),
1512 )?;
1513 dict.set_item(
1514 "slow_lengths",
1515 combos
1516 .iter()
1517 .map(|combo| combo.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH))
1518 .collect::<Vec<_>>()
1519 .into_pyarray(py),
1520 )?;
1521 dict.set_item(
1522 "fast_lengths",
1523 combos
1524 .iter()
1525 .map(|combo| combo.fast_length.unwrap_or(DEFAULT_FAST_LENGTH))
1526 .collect::<Vec<_>>()
1527 .into_pyarray(py),
1528 )?;
1529 dict.set_item(
1530 "resolutions",
1531 combos
1532 .iter()
1533 .map(|combo| combo.resolution.unwrap_or(DEFAULT_RESOLUTION))
1534 .collect::<Vec<_>>()
1535 .into_pyarray(py),
1536 )?;
1537 dict.set_item("rows", rows)?;
1538 dict.set_item("cols", cols)?;
1539 Ok(dict)
1540}
1541
1542#[cfg(feature = "python")]
1543pub fn register_moving_average_cross_probability_module(
1544 m: &Bound<'_, pyo3::types::PyModule>,
1545) -> PyResult<()> {
1546 m.add_function(wrap_pyfunction!(moving_average_cross_probability_py, m)?)?;
1547 m.add_function(wrap_pyfunction!(
1548 moving_average_cross_probability_batch_py,
1549 m
1550 )?)?;
1551 m.add_class::<MovingAverageCrossProbabilityStreamPy>()?;
1552 Ok(())
1553}
1554
1555#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1556#[derive(Serialize, Deserialize)]
1557pub struct MovingAverageCrossProbabilityJsOutput {
1558 pub value: Vec<f64>,
1559 pub slow_ma: Vec<f64>,
1560 pub fast_ma: Vec<f64>,
1561 pub forecast: Vec<f64>,
1562 pub upper: Vec<f64>,
1563 pub lower: Vec<f64>,
1564 pub direction: Vec<f64>,
1565}
1566
1567#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1568fn js_vec3_to_usize(name: &str, values: &[f64]) -> Result<(usize, usize, usize), JsValue> {
1569 if values.len() != 3 {
1570 return Err(JsValue::from_str(&format!(
1571 "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1572 )));
1573 }
1574 let mut out = [0usize; 3];
1575 for (idx, value) in values.iter().enumerate() {
1576 if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 {
1577 return Err(JsValue::from_str(&format!(
1578 "Invalid config: {name} values must be non-negative integers"
1579 )));
1580 }
1581 out[idx] = *value as usize;
1582 }
1583 Ok((out[0], out[1], out[2]))
1584}
1585
1586#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1587#[wasm_bindgen(js_name = "moving_average_cross_probability_js")]
1588pub fn moving_average_cross_probability_js(
1589 data: &[f64],
1590 ma_type: String,
1591 smoothing_window: usize,
1592 slow_length: usize,
1593 fast_length: usize,
1594 resolution: usize,
1595) -> Result<JsValue, JsValue> {
1596 let input = MovingAverageCrossProbabilityInput::from_slice(
1597 data,
1598 MovingAverageCrossProbabilityParams {
1599 ma_type: Some(
1600 MovingAverageCrossProbabilityMaType::from_str(&ma_type)
1601 .map_err(|e| JsValue::from_str(&e))?,
1602 ),
1603 smoothing_window: Some(smoothing_window),
1604 slow_length: Some(slow_length),
1605 fast_length: Some(fast_length),
1606 resolution: Some(resolution),
1607 },
1608 );
1609 let out = moving_average_cross_probability_with_kernel(&input, Kernel::Auto)
1610 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1611 serde_wasm_bindgen::to_value(&MovingAverageCrossProbabilityJsOutput {
1612 value: out.value,
1613 slow_ma: out.slow_ma,
1614 fast_ma: out.fast_ma,
1615 forecast: out.forecast,
1616 upper: out.upper,
1617 lower: out.lower,
1618 direction: out.direction,
1619 })
1620 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1621}
1622
1623#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1624#[derive(Serialize, Deserialize)]
1625pub struct MovingAverageCrossProbabilityBatchConfig {
1626 pub smoothing_window_range: Vec<f64>,
1627 pub slow_length_range: Vec<f64>,
1628 pub fast_length_range: Vec<f64>,
1629 pub resolution_range: Vec<f64>,
1630 pub ma_type: Option<String>,
1631}
1632
1633#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1634#[derive(Serialize, Deserialize)]
1635pub struct MovingAverageCrossProbabilityBatchJsOutput {
1636 pub value: Vec<f64>,
1637 pub slow_ma: Vec<f64>,
1638 pub fast_ma: Vec<f64>,
1639 pub forecast: Vec<f64>,
1640 pub upper: Vec<f64>,
1641 pub lower: Vec<f64>,
1642 pub direction: Vec<f64>,
1643 pub smoothing_windows: Vec<usize>,
1644 pub slow_lengths: Vec<usize>,
1645 pub fast_lengths: Vec<usize>,
1646 pub resolutions: Vec<usize>,
1647 pub rows: usize,
1648 pub cols: usize,
1649}
1650
1651#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1652#[wasm_bindgen(js_name = "moving_average_cross_probability_batch_js")]
1653pub fn moving_average_cross_probability_batch_js(
1654 data: &[f64],
1655 config: JsValue,
1656) -> Result<JsValue, JsValue> {
1657 let config: MovingAverageCrossProbabilityBatchConfig =
1658 serde_wasm_bindgen::from_value(config)
1659 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1660 let ma_type = config
1661 .ma_type
1662 .as_deref()
1663 .map(MovingAverageCrossProbabilityMaType::from_str)
1664 .transpose()
1665 .map_err(|e| JsValue::from_str(&e))?
1666 .unwrap_or(DEFAULT_MA_TYPE);
1667 let sweep = MovingAverageCrossProbabilityBatchRange {
1668 smoothing_window: js_vec3_to_usize(
1669 "smoothing_window_range",
1670 &config.smoothing_window_range,
1671 )?,
1672 slow_length: js_vec3_to_usize("slow_length_range", &config.slow_length_range)?,
1673 fast_length: js_vec3_to_usize("fast_length_range", &config.fast_length_range)?,
1674 resolution: js_vec3_to_usize("resolution_range", &config.resolution_range)?,
1675 ma_type,
1676 };
1677 let out = moving_average_cross_probability_batch_with_kernel(data, &sweep, Kernel::Auto)
1678 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1679 serde_wasm_bindgen::to_value(&MovingAverageCrossProbabilityBatchJsOutput {
1680 value: out.value,
1681 slow_ma: out.slow_ma,
1682 fast_ma: out.fast_ma,
1683 forecast: out.forecast,
1684 upper: out.upper,
1685 lower: out.lower,
1686 direction: out.direction,
1687 smoothing_windows: out
1688 .combos
1689 .iter()
1690 .map(|combo| combo.smoothing_window.unwrap_or(DEFAULT_SMOOTHING_WINDOW))
1691 .collect(),
1692 slow_lengths: out
1693 .combos
1694 .iter()
1695 .map(|combo| combo.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH))
1696 .collect(),
1697 fast_lengths: out
1698 .combos
1699 .iter()
1700 .map(|combo| combo.fast_length.unwrap_or(DEFAULT_FAST_LENGTH))
1701 .collect(),
1702 resolutions: out
1703 .combos
1704 .iter()
1705 .map(|combo| combo.resolution.unwrap_or(DEFAULT_RESOLUTION))
1706 .collect(),
1707 rows: out.rows,
1708 cols: out.cols,
1709 })
1710 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1711}
1712
1713#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1714#[wasm_bindgen]
1715pub fn moving_average_cross_probability_alloc(len: usize) -> *mut f64 {
1716 let mut vec = Vec::<f64>::with_capacity(len);
1717 let ptr = vec.as_mut_ptr();
1718 std::mem::forget(vec);
1719 ptr
1720}
1721
1722#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1723#[wasm_bindgen]
1724pub fn moving_average_cross_probability_free(ptr: *mut f64, len: usize) {
1725 if !ptr.is_null() {
1726 unsafe {
1727 let _ = Vec::from_raw_parts(ptr, len, len);
1728 }
1729 }
1730}
1731
1732#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1733#[wasm_bindgen]
1734pub fn moving_average_cross_probability_into(
1735 in_ptr: *const f64,
1736 out_value_ptr: *mut f64,
1737 out_slow_ma_ptr: *mut f64,
1738 out_fast_ma_ptr: *mut f64,
1739 out_forecast_ptr: *mut f64,
1740 out_upper_ptr: *mut f64,
1741 out_lower_ptr: *mut f64,
1742 out_direction_ptr: *mut f64,
1743 len: usize,
1744 ma_type: String,
1745 smoothing_window: usize,
1746 slow_length: usize,
1747 fast_length: usize,
1748 resolution: usize,
1749) -> Result<(), JsValue> {
1750 if in_ptr.is_null()
1751 || out_value_ptr.is_null()
1752 || out_slow_ma_ptr.is_null()
1753 || out_fast_ma_ptr.is_null()
1754 || out_forecast_ptr.is_null()
1755 || out_upper_ptr.is_null()
1756 || out_lower_ptr.is_null()
1757 || out_direction_ptr.is_null()
1758 {
1759 return Err(JsValue::from_str(
1760 "null pointer passed to moving_average_cross_probability_into",
1761 ));
1762 }
1763 unsafe {
1764 let data = std::slice::from_raw_parts(in_ptr, len);
1765 let out_value = std::slice::from_raw_parts_mut(out_value_ptr, len);
1766 let out_slow_ma = std::slice::from_raw_parts_mut(out_slow_ma_ptr, len);
1767 let out_fast_ma = std::slice::from_raw_parts_mut(out_fast_ma_ptr, len);
1768 let out_forecast = std::slice::from_raw_parts_mut(out_forecast_ptr, len);
1769 let out_upper = std::slice::from_raw_parts_mut(out_upper_ptr, len);
1770 let out_lower = std::slice::from_raw_parts_mut(out_lower_ptr, len);
1771 let out_direction = std::slice::from_raw_parts_mut(out_direction_ptr, len);
1772 let input = MovingAverageCrossProbabilityInput::from_slice(
1773 data,
1774 MovingAverageCrossProbabilityParams {
1775 ma_type: Some(
1776 MovingAverageCrossProbabilityMaType::from_str(&ma_type)
1777 .map_err(|e| JsValue::from_str(&e))?,
1778 ),
1779 smoothing_window: Some(smoothing_window),
1780 slow_length: Some(slow_length),
1781 fast_length: Some(fast_length),
1782 resolution: Some(resolution),
1783 },
1784 );
1785 moving_average_cross_probability_into_slice(
1786 out_value,
1787 out_slow_ma,
1788 out_fast_ma,
1789 out_forecast,
1790 out_upper,
1791 out_lower,
1792 out_direction,
1793 &input,
1794 Kernel::Auto,
1795 )
1796 .map_err(|e| JsValue::from_str(&e.to_string()))
1797 }
1798}
1799
1800#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1801#[wasm_bindgen]
1802pub fn moving_average_cross_probability_batch_into(
1803 in_ptr: *const f64,
1804 out_value_ptr: *mut f64,
1805 out_slow_ma_ptr: *mut f64,
1806 out_fast_ma_ptr: *mut f64,
1807 out_forecast_ptr: *mut f64,
1808 out_upper_ptr: *mut f64,
1809 out_lower_ptr: *mut f64,
1810 out_direction_ptr: *mut f64,
1811 len: usize,
1812 smoothing_window_start: usize,
1813 smoothing_window_end: usize,
1814 smoothing_window_step: usize,
1815 slow_length_start: usize,
1816 slow_length_end: usize,
1817 slow_length_step: usize,
1818 fast_length_start: usize,
1819 fast_length_end: usize,
1820 fast_length_step: usize,
1821 resolution_start: usize,
1822 resolution_end: usize,
1823 resolution_step: usize,
1824 ma_type: String,
1825) -> Result<usize, JsValue> {
1826 if in_ptr.is_null()
1827 || out_value_ptr.is_null()
1828 || out_slow_ma_ptr.is_null()
1829 || out_fast_ma_ptr.is_null()
1830 || out_forecast_ptr.is_null()
1831 || out_upper_ptr.is_null()
1832 || out_lower_ptr.is_null()
1833 || out_direction_ptr.is_null()
1834 {
1835 return Err(JsValue::from_str(
1836 "null pointer passed to moving_average_cross_probability_batch_into",
1837 ));
1838 }
1839 unsafe {
1840 let data = std::slice::from_raw_parts(in_ptr, len);
1841 let sweep = MovingAverageCrossProbabilityBatchRange {
1842 smoothing_window: (
1843 smoothing_window_start,
1844 smoothing_window_end,
1845 smoothing_window_step,
1846 ),
1847 slow_length: (slow_length_start, slow_length_end, slow_length_step),
1848 fast_length: (fast_length_start, fast_length_end, fast_length_step),
1849 resolution: (resolution_start, resolution_end, resolution_step),
1850 ma_type: MovingAverageCrossProbabilityMaType::from_str(&ma_type)
1851 .map_err(|e| JsValue::from_str(&e))?,
1852 };
1853 let combos = moving_average_cross_probability_expand_grid(&sweep)
1854 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1855 let rows = combos.len();
1856 let total = rows.checked_mul(len).ok_or_else(|| {
1857 JsValue::from_str("rows*cols overflow in moving_average_cross_probability_batch_into")
1858 })?;
1859 let out_value = std::slice::from_raw_parts_mut(out_value_ptr, total);
1860 let out_slow_ma = std::slice::from_raw_parts_mut(out_slow_ma_ptr, total);
1861 let out_fast_ma = std::slice::from_raw_parts_mut(out_fast_ma_ptr, total);
1862 let out_forecast = std::slice::from_raw_parts_mut(out_forecast_ptr, total);
1863 let out_upper = std::slice::from_raw_parts_mut(out_upper_ptr, total);
1864 let out_lower = std::slice::from_raw_parts_mut(out_lower_ptr, total);
1865 let out_direction = std::slice::from_raw_parts_mut(out_direction_ptr, total);
1866 moving_average_cross_probability_batch_into_slice(
1867 out_value,
1868 out_slow_ma,
1869 out_fast_ma,
1870 out_forecast,
1871 out_upper,
1872 out_lower,
1873 out_direction,
1874 data,
1875 &sweep,
1876 Kernel::Auto,
1877 )
1878 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1879 Ok(rows)
1880 }
1881}
1882
1883#[cfg(test)]
1884mod tests {
1885 use super::*;
1886
1887 fn sample_data(len: usize) -> Vec<f64> {
1888 (0..len)
1889 .map(|i| {
1890 let x = i as f64;
1891 100.0 + x * 0.13 + (x * 0.11).sin() * 2.4 + (x * 0.03).cos() * 0.7
1892 })
1893 .collect()
1894 }
1895
1896 fn sample_candles(len: usize) -> Candles {
1897 let close = sample_data(len);
1898 let open = close.iter().map(|v| v - 0.3).collect::<Vec<_>>();
1899 let high = close.iter().map(|v| v + 0.8).collect::<Vec<_>>();
1900 let low = close.iter().map(|v| v - 0.9).collect::<Vec<_>>();
1901 let volume = vec![1_000.0; len];
1902 let timestamp = (0..len as i64).collect::<Vec<_>>();
1903 Candles::new(timestamp, open, high, low, close, volume)
1904 }
1905
1906 fn assert_vec_close(lhs: &[f64], rhs: &[f64]) {
1907 assert_eq!(lhs.len(), rhs.len());
1908 for (idx, (a, b)) in lhs.iter().zip(rhs.iter()).enumerate() {
1909 if a.is_nan() && b.is_nan() {
1910 continue;
1911 }
1912 let diff = (a - b).abs();
1913 assert!(diff <= 1e-9, "mismatch at {idx}: {a} vs {b}");
1914 }
1915 }
1916
1917 fn sma(data: &[f64], period: usize) -> Vec<f64> {
1918 let mut out = vec![f64::NAN; data.len()];
1919 let mut sum = 0.0;
1920 for i in 0..data.len() {
1921 sum += data[i];
1922 if i + 1 >= period {
1923 if i + 1 > period {
1924 sum -= data[i - period];
1925 }
1926 out[i] = sum / period as f64;
1927 }
1928 }
1929 out
1930 }
1931
1932 fn ema(data: &[f64], period: usize) -> Vec<f64> {
1933 let mut out = vec![f64::NAN; data.len()];
1934 let alpha = 2.0 / (period as f64 + 1.0);
1935 let mut seed_sum = 0.0;
1936 let mut current = f64::NAN;
1937 for i in 0..data.len() {
1938 seed_sum += data[i];
1939 if i + 1 < period {
1940 continue;
1941 }
1942 if i + 1 == period {
1943 current = seed_sum / period as f64;
1944 } else {
1945 current = alpha.mul_add(data[i], (1.0 - alpha) * current);
1946 }
1947 out[i] = current;
1948 }
1949 out
1950 }
1951
1952 fn wma_window(data: &[f64]) -> f64 {
1953 let mut num = 0.0;
1954 let mut den = 0.0;
1955 for (idx, value) in data.iter().enumerate() {
1956 let w = (idx + 1) as f64;
1957 num += *value * w;
1958 den += w;
1959 }
1960 num / den
1961 }
1962
1963 fn hma(data: &[f64], period: usize) -> Vec<f64> {
1964 let n = data.len();
1965 let half = period / 2;
1966 let sqrt_len = (period as f64).sqrt().floor() as usize;
1967 let mut diff = vec![f64::NAN; n];
1968 let mut out = vec![f64::NAN; n];
1969 for i in 0..n {
1970 if i + 1 >= period && i + 1 >= half {
1971 let full = wma_window(&data[i + 1 - period..=i]);
1972 let half_val = wma_window(&data[i + 1 - half..=i]);
1973 diff[i] = 2.0 * half_val - full;
1974 }
1975 if i + 1 >= period + sqrt_len - 1 {
1976 let start = i + 1 - sqrt_len;
1977 let window = diff[start..=i].iter().copied().collect::<Vec<_>>();
1978 if window.iter().all(|v| v.is_finite()) {
1979 out[i] = wma_window(&window);
1980 }
1981 }
1982 }
1983 out
1984 }
1985
1986 fn stddev4(data: &[f64], period: usize) -> Vec<f64> {
1987 let n = data.len();
1988 let mut out = vec![f64::NAN; n];
1989 for i in period.saturating_sub(1)..n {
1990 let window = &data[i + 1 - period..=i];
1991 let mean = window.iter().sum::<f64>() / period as f64;
1992 let var = window
1993 .iter()
1994 .map(|v| {
1995 let d = *v - mean;
1996 d * d
1997 })
1998 .sum::<f64>()
1999 / period as f64;
2000 out[i] = var.sqrt() * 4.0;
2001 }
2002 out
2003 }
2004
2005 fn array_sma(temp: &[f64], period: usize) -> f64 {
2006 temp[..period].iter().sum::<f64>() / period as f64
2007 }
2008
2009 fn array_ema(temp: &[f64], period: usize) -> f64 {
2010 let alpha = 2.0 / (period as f64 + 1.0);
2011 let mut ema = *temp.last().unwrap();
2012 for idx in (0..temp.len() - 1).rev() {
2013 ema = alpha.mul_add(temp[idx], (1.0 - alpha) * ema);
2014 }
2015 ema
2016 }
2017
2018 fn manual_reference(
2019 data: &[f64],
2020 ma_type: MovingAverageCrossProbabilityMaType,
2021 smoothing_window: usize,
2022 slow_length: usize,
2023 fast_length: usize,
2024 resolution: usize,
2025 ) -> MovingAverageCrossProbabilityOutput {
2026 let n = data.len();
2027 let mut value = vec![f64::NAN; n];
2028 let slow_ma = match ma_type {
2029 MovingAverageCrossProbabilityMaType::Ema => ema(data, slow_length),
2030 MovingAverageCrossProbabilityMaType::Sma => sma(data, slow_length),
2031 };
2032 let fast_ma = match ma_type {
2033 MovingAverageCrossProbabilityMaType::Ema => ema(data, fast_length),
2034 MovingAverageCrossProbabilityMaType::Sma => sma(data, fast_length),
2035 };
2036 let price = hma(data, smoothing_window);
2037 let stdev = stddev4(data, smoothing_window);
2038 let mut forecast = vec![f64::NAN; n];
2039 let mut upper = vec![f64::NAN; n];
2040 let mut lower = vec![f64::NAN; n];
2041 let mut direction = vec![f64::NAN; n];
2042
2043 for i in 0..n {
2044 if slow_ma[i].is_finite() && fast_ma[i].is_finite() {
2045 direction[i] = if fast_ma[i] > slow_ma[i] { -1.0 } else { 1.0 };
2046 }
2047 if i > 0 && price[i].is_finite() && price[i - 1].is_finite() && stdev[i].is_finite() {
2048 forecast[i] = price[i] + (price[i] - price[i - 1]);
2049 upper[i] = forecast[i] + stdev[i];
2050 lower[i] = forecast[i] - stdev[i];
2051 }
2052 if i < 2 * slow_length
2053 || !direction[i].is_finite()
2054 || !upper[i].is_finite()
2055 || !lower[i].is_finite()
2056 {
2057 continue;
2058 }
2059 let mut memory = Vec::with_capacity(2 * slow_length + 1);
2060 for j in 0..=2 * slow_length {
2061 memory.push(data[i - j]);
2062 }
2063 let seg = (upper[i] - lower[i]) / (resolution - 1) as f64;
2064 let mut hits = 0usize;
2065 for k in 0..resolution {
2066 let possibility = lower[i] + seg * k as f64;
2067 let mut temp = Vec::with_capacity(memory.len() + 1);
2068 temp.push(possibility);
2069 temp.extend_from_slice(&memory);
2070 let slow_future = match ma_type {
2071 MovingAverageCrossProbabilityMaType::Ema => array_ema(&temp, slow_length),
2072 MovingAverageCrossProbabilityMaType::Sma => array_sma(&temp, slow_length),
2073 };
2074 let fast_future = match ma_type {
2075 MovingAverageCrossProbabilityMaType::Ema => array_ema(&temp, fast_length),
2076 MovingAverageCrossProbabilityMaType::Sma => array_sma(&temp, fast_length),
2077 };
2078 let crossed = if direction[i] < 0.0 {
2079 slow_future > fast_future
2080 } else {
2081 slow_future <= fast_future
2082 };
2083 if crossed {
2084 hits += 1;
2085 }
2086 }
2087 value[i] = 100.0 * hits as f64 / resolution as f64;
2088 }
2089
2090 MovingAverageCrossProbabilityOutput {
2091 value,
2092 slow_ma,
2093 fast_ma,
2094 forecast,
2095 upper,
2096 lower,
2097 direction,
2098 }
2099 }
2100
2101 #[test]
2102 fn manual_reference_matches_ema_single() {
2103 let data = sample_data(220);
2104 let input = MovingAverageCrossProbabilityInput::from_slice(
2105 &data,
2106 MovingAverageCrossProbabilityParams::default(),
2107 );
2108 let out = moving_average_cross_probability(&input).unwrap();
2109 let expected = manual_reference(&data, DEFAULT_MA_TYPE, 7, 30, 14, 50);
2110 assert_vec_close(&out.value, &expected.value);
2111 assert_vec_close(&out.slow_ma, &expected.slow_ma);
2112 assert_vec_close(&out.fast_ma, &expected.fast_ma);
2113 assert_vec_close(&out.forecast, &expected.forecast);
2114 assert_vec_close(&out.upper, &expected.upper);
2115 assert_vec_close(&out.lower, &expected.lower);
2116 assert_vec_close(&out.direction, &expected.direction);
2117 }
2118
2119 #[test]
2120 fn manual_reference_matches_sma_single() {
2121 let data = sample_data(220);
2122 let params = MovingAverageCrossProbabilityParams {
2123 ma_type: Some(MovingAverageCrossProbabilityMaType::Sma),
2124 ..MovingAverageCrossProbabilityParams::default()
2125 };
2126 let input = MovingAverageCrossProbabilityInput::from_slice(&data, params);
2127 let out = moving_average_cross_probability(&input).unwrap();
2128 let expected = manual_reference(
2129 &data,
2130 MovingAverageCrossProbabilityMaType::Sma,
2131 7,
2132 30,
2133 14,
2134 50,
2135 );
2136 assert_vec_close(&out.value, &expected.value);
2137 }
2138
2139 #[test]
2140 fn stream_matches_batch() {
2141 let data = sample_data(200);
2142 let params = MovingAverageCrossProbabilityParams {
2143 ma_type: Some(MovingAverageCrossProbabilityMaType::Sma),
2144 smoothing_window: Some(8),
2145 slow_length: Some(26),
2146 fast_length: Some(11),
2147 resolution: Some(40),
2148 };
2149 let input = MovingAverageCrossProbabilityInput::from_slice(&data, params.clone());
2150 let batch = moving_average_cross_probability(&input).unwrap();
2151 let mut stream = MovingAverageCrossProbabilityStream::try_new(params).unwrap();
2152 let mut value = Vec::with_capacity(data.len());
2153 let mut slow_ma = Vec::with_capacity(data.len());
2154 let mut fast_ma = Vec::with_capacity(data.len());
2155 let mut forecast = Vec::with_capacity(data.len());
2156 let mut upper = Vec::with_capacity(data.len());
2157 let mut lower = Vec::with_capacity(data.len());
2158 let mut direction = Vec::with_capacity(data.len());
2159 for item in data {
2160 let (v, s, f, fc, u, l, d) = stream.update(item);
2161 value.push(v);
2162 slow_ma.push(s);
2163 fast_ma.push(f);
2164 forecast.push(fc);
2165 upper.push(u);
2166 lower.push(l);
2167 direction.push(d);
2168 }
2169 assert_vec_close(&value, &batch.value);
2170 assert_vec_close(&slow_ma, &batch.slow_ma);
2171 assert_vec_close(&fast_ma, &batch.fast_ma);
2172 assert_vec_close(&forecast, &batch.forecast);
2173 assert_vec_close(&upper, &batch.upper);
2174 assert_vec_close(&lower, &batch.lower);
2175 assert_vec_close(&direction, &batch.direction);
2176 }
2177
2178 #[test]
2179 fn batch_first_row_matches_single() {
2180 let data = sample_data(180);
2181 let sweep = MovingAverageCrossProbabilityBatchRange {
2182 smoothing_window: (7, 8, 1),
2183 slow_length: (30, 30, 0),
2184 fast_length: (14, 14, 0),
2185 resolution: (50, 50, 0),
2186 ma_type: MovingAverageCrossProbabilityMaType::Ema,
2187 };
2188 let batch = moving_average_cross_probability_batch_with_kernel(&data, &sweep, Kernel::Auto)
2189 .unwrap();
2190 let input = MovingAverageCrossProbabilityInput::from_slice(
2191 &data,
2192 MovingAverageCrossProbabilityParams::default(),
2193 );
2194 let single = moving_average_cross_probability(&input).unwrap();
2195 assert_eq!(batch.rows, 2);
2196 assert_eq!(batch.cols, data.len());
2197 assert_vec_close(&batch.value[..data.len()], &single.value);
2198 assert_vec_close(&batch.slow_ma[..data.len()], &single.slow_ma);
2199 assert_vec_close(&batch.fast_ma[..data.len()], &single.fast_ma);
2200 }
2201
2202 #[test]
2203 fn invalid_length_order_fails() {
2204 let data = sample_data(96);
2205 let input = MovingAverageCrossProbabilityInput::from_slice(
2206 &data,
2207 MovingAverageCrossProbabilityParams {
2208 slow_length: Some(10),
2209 fast_length: Some(14),
2210 ..MovingAverageCrossProbabilityParams::default()
2211 },
2212 );
2213 let err = moving_average_cross_probability(&input).unwrap_err();
2214 assert!(matches!(
2215 err,
2216 MovingAverageCrossProbabilityError::InvalidLengthOrder { .. }
2217 ));
2218 }
2219
2220 #[test]
2221 fn cpu_dispatch_matches_direct() {
2222 let candles = sample_candles(180);
2223 let combos = [IndicatorParamSet {
2224 params: &[
2225 ParamKV {
2226 key: "ma_type",
2227 value: ParamValue::EnumString("ema"),
2228 },
2229 ParamKV {
2230 key: "smoothing_window",
2231 value: ParamValue::Int(7),
2232 },
2233 ParamKV {
2234 key: "slow_length",
2235 value: ParamValue::Int(30),
2236 },
2237 ParamKV {
2238 key: "fast_length",
2239 value: ParamValue::Int(14),
2240 },
2241 ParamKV {
2242 key: "resolution",
2243 value: ParamValue::Int(50),
2244 },
2245 ],
2246 }];
2247 let dispatched = compute_cpu_batch(IndicatorBatchRequest {
2248 indicator_id: "moving_average_cross_probability",
2249 output_id: Some("value"),
2250 data: IndicatorDataRef::Candles {
2251 candles: &candles,
2252 source: Some("close"),
2253 },
2254 combos: &combos,
2255 kernel: Kernel::Auto,
2256 })
2257 .unwrap();
2258 let direct =
2259 moving_average_cross_probability(&MovingAverageCrossProbabilityInput::from_candles(
2260 &candles,
2261 MovingAverageCrossProbabilityParams::default(),
2262 ))
2263 .unwrap();
2264 assert_vec_close(dispatched.values_f64.as_ref().unwrap(), &direct.value);
2265 }
2266}