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