Skip to main content

vector_ta/indicators/
adjustable_ma_alternating_extremities.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
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}