Skip to main content

vector_ta/indicators/
standardized_psar_oscillator.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, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24#[cfg(test)]
25use std::error::Error as StdError;
26use std::mem::ManuallyDrop;
27use thiserror::Error;
28
29const DEFAULT_START: f64 = 0.02;
30const DEFAULT_INCREMENT: f64 = 0.0005;
31const DEFAULT_MAXIMUM: f64 = 0.2;
32const DEFAULT_STANDARDIZATION_LENGTH: usize = 21;
33const DEFAULT_WMA_LENGTH: usize = 40;
34const DEFAULT_WMA_LAG: usize = 3;
35const DEFAULT_PIVOT_LEFT: usize = 15;
36const DEFAULT_PIVOT_RIGHT: usize = 1;
37const DEFAULT_PLOT_BULLISH: bool = true;
38const DEFAULT_PLOT_BEARISH: bool = true;
39const REVERSAL_LEVEL: f64 = 600.0;
40const REVERSAL_MARKER: f64 = 900.0;
41const MAX_PIVOT_BARS: usize = 80;
42
43#[inline(always)]
44fn high_source(candles: &Candles) -> &[f64] {
45    &candles.high
46}
47
48#[inline(always)]
49fn low_source(candles: &Candles) -> &[f64] {
50    &candles.low
51}
52
53#[inline(always)]
54fn close_source(candles: &Candles) -> &[f64] {
55    &candles.close
56}
57
58#[derive(Debug, Clone)]
59pub enum StandardizedPsarOscillatorData<'a> {
60    Candles {
61        candles: &'a Candles,
62    },
63    Slices {
64        high: &'a [f64],
65        low: &'a [f64],
66        close: &'a [f64],
67    },
68}
69
70#[derive(Debug, Clone)]
71#[cfg_attr(
72    all(target_arch = "wasm32", feature = "wasm"),
73    derive(Serialize, Deserialize)
74)]
75pub struct StandardizedPsarOscillatorOutput {
76    pub oscillator: Vec<f64>,
77    pub ma: Vec<f64>,
78    pub bullish_reversal: Vec<f64>,
79    pub bearish_reversal: Vec<f64>,
80    pub regular_bullish: Vec<f64>,
81    pub regular_bearish: Vec<f64>,
82    pub bullish_weakening: Vec<f64>,
83    pub bearish_weakening: Vec<f64>,
84}
85
86#[derive(Debug, Clone, PartialEq)]
87#[cfg_attr(
88    all(target_arch = "wasm32", feature = "wasm"),
89    derive(Serialize, Deserialize)
90)]
91pub struct StandardizedPsarOscillatorParams {
92    pub start: Option<f64>,
93    pub increment: Option<f64>,
94    pub maximum: Option<f64>,
95    pub standardization_length: Option<usize>,
96    pub wma_length: Option<usize>,
97    pub wma_lag: Option<usize>,
98    pub pivot_left: Option<usize>,
99    pub pivot_right: Option<usize>,
100    pub plot_bullish: Option<bool>,
101    pub plot_bearish: Option<bool>,
102}
103
104impl Default for StandardizedPsarOscillatorParams {
105    fn default() -> Self {
106        Self {
107            start: Some(DEFAULT_START),
108            increment: Some(DEFAULT_INCREMENT),
109            maximum: Some(DEFAULT_MAXIMUM),
110            standardization_length: Some(DEFAULT_STANDARDIZATION_LENGTH),
111            wma_length: Some(DEFAULT_WMA_LENGTH),
112            wma_lag: Some(DEFAULT_WMA_LAG),
113            pivot_left: Some(DEFAULT_PIVOT_LEFT),
114            pivot_right: Some(DEFAULT_PIVOT_RIGHT),
115            plot_bullish: Some(DEFAULT_PLOT_BULLISH),
116            plot_bearish: Some(DEFAULT_PLOT_BEARISH),
117        }
118    }
119}
120
121#[derive(Debug, Clone)]
122pub struct StandardizedPsarOscillatorInput<'a> {
123    pub data: StandardizedPsarOscillatorData<'a>,
124    pub params: StandardizedPsarOscillatorParams,
125}
126
127impl<'a> StandardizedPsarOscillatorInput<'a> {
128    #[inline(always)]
129    pub fn from_candles(candles: &'a Candles, params: StandardizedPsarOscillatorParams) -> Self {
130        Self {
131            data: StandardizedPsarOscillatorData::Candles { candles },
132            params,
133        }
134    }
135
136    #[inline(always)]
137    pub fn from_slices(
138        high: &'a [f64],
139        low: &'a [f64],
140        close: &'a [f64],
141        params: StandardizedPsarOscillatorParams,
142    ) -> Self {
143        Self {
144            data: StandardizedPsarOscillatorData::Slices { high, low, close },
145            params,
146        }
147    }
148
149    #[inline(always)]
150    pub fn with_default_candles(candles: &'a Candles) -> Self {
151        Self::from_candles(candles, StandardizedPsarOscillatorParams::default())
152    }
153
154    #[inline(always)]
155    pub fn get_start(&self) -> f64 {
156        self.params.start.unwrap_or(DEFAULT_START)
157    }
158
159    #[inline(always)]
160    pub fn get_increment(&self) -> f64 {
161        self.params.increment.unwrap_or(DEFAULT_INCREMENT)
162    }
163
164    #[inline(always)]
165    pub fn get_maximum(&self) -> f64 {
166        self.params.maximum.unwrap_or(DEFAULT_MAXIMUM)
167    }
168
169    #[inline(always)]
170    pub fn get_standardization_length(&self) -> usize {
171        self.params
172            .standardization_length
173            .unwrap_or(DEFAULT_STANDARDIZATION_LENGTH)
174    }
175
176    #[inline(always)]
177    pub fn get_wma_length(&self) -> usize {
178        self.params.wma_length.unwrap_or(DEFAULT_WMA_LENGTH)
179    }
180
181    #[inline(always)]
182    pub fn get_wma_lag(&self) -> usize {
183        self.params.wma_lag.unwrap_or(DEFAULT_WMA_LAG)
184    }
185
186    #[inline(always)]
187    pub fn get_pivot_left(&self) -> usize {
188        self.params.pivot_left.unwrap_or(DEFAULT_PIVOT_LEFT)
189    }
190
191    #[inline(always)]
192    pub fn get_pivot_right(&self) -> usize {
193        self.params.pivot_right.unwrap_or(DEFAULT_PIVOT_RIGHT)
194    }
195
196    #[inline(always)]
197    pub fn get_plot_bullish(&self) -> bool {
198        self.params.plot_bullish.unwrap_or(DEFAULT_PLOT_BULLISH)
199    }
200
201    #[inline(always)]
202    pub fn get_plot_bearish(&self) -> bool {
203        self.params.plot_bearish.unwrap_or(DEFAULT_PLOT_BEARISH)
204    }
205
206    #[inline(always)]
207    fn as_hlc(&self) -> (&'a [f64], &'a [f64], &'a [f64]) {
208        match &self.data {
209            StandardizedPsarOscillatorData::Candles { candles } => (
210                high_source(candles),
211                low_source(candles),
212                close_source(candles),
213            ),
214            StandardizedPsarOscillatorData::Slices { high, low, close } => (*high, *low, *close),
215        }
216    }
217}
218
219impl<'a> AsRef<[f64]> for StandardizedPsarOscillatorInput<'a> {
220    #[inline(always)]
221    fn as_ref(&self) -> &[f64] {
222        self.as_hlc().2
223    }
224}
225
226#[derive(Clone, Debug)]
227pub struct StandardizedPsarOscillatorBuilder {
228    start: Option<f64>,
229    increment: Option<f64>,
230    maximum: Option<f64>,
231    standardization_length: Option<usize>,
232    wma_length: Option<usize>,
233    wma_lag: Option<usize>,
234    pivot_left: Option<usize>,
235    pivot_right: Option<usize>,
236    plot_bullish: Option<bool>,
237    plot_bearish: Option<bool>,
238    kernel: Kernel,
239}
240
241impl Default for StandardizedPsarOscillatorBuilder {
242    fn default() -> Self {
243        Self {
244            start: None,
245            increment: None,
246            maximum: None,
247            standardization_length: None,
248            wma_length: None,
249            wma_lag: None,
250            pivot_left: None,
251            pivot_right: None,
252            plot_bullish: None,
253            plot_bearish: None,
254            kernel: Kernel::Auto,
255        }
256    }
257}
258
259impl StandardizedPsarOscillatorBuilder {
260    #[inline(always)]
261    pub fn new() -> Self {
262        Self::default()
263    }
264
265    #[inline(always)]
266    pub fn start(mut self, value: f64) -> Self {
267        self.start = Some(value);
268        self
269    }
270
271    #[inline(always)]
272    pub fn increment(mut self, value: f64) -> Self {
273        self.increment = Some(value);
274        self
275    }
276
277    #[inline(always)]
278    pub fn maximum(mut self, value: f64) -> Self {
279        self.maximum = Some(value);
280        self
281    }
282
283    #[inline(always)]
284    pub fn standardization_length(mut self, value: usize) -> Self {
285        self.standardization_length = Some(value);
286        self
287    }
288
289    #[inline(always)]
290    pub fn wma_length(mut self, value: usize) -> Self {
291        self.wma_length = Some(value);
292        self
293    }
294
295    #[inline(always)]
296    pub fn wma_lag(mut self, value: usize) -> Self {
297        self.wma_lag = Some(value);
298        self
299    }
300
301    #[inline(always)]
302    pub fn pivot_left(mut self, value: usize) -> Self {
303        self.pivot_left = Some(value);
304        self
305    }
306
307    #[inline(always)]
308    pub fn pivot_right(mut self, value: usize) -> Self {
309        self.pivot_right = Some(value);
310        self
311    }
312
313    #[inline(always)]
314    pub fn plot_bullish(mut self, value: bool) -> Self {
315        self.plot_bullish = Some(value);
316        self
317    }
318
319    #[inline(always)]
320    pub fn plot_bearish(mut self, value: bool) -> Self {
321        self.plot_bearish = Some(value);
322        self
323    }
324
325    #[inline(always)]
326    pub fn kernel(mut self, kernel: Kernel) -> Self {
327        self.kernel = kernel;
328        self
329    }
330
331    #[inline(always)]
332    fn params(self) -> StandardizedPsarOscillatorParams {
333        StandardizedPsarOscillatorParams {
334            start: self.start,
335            increment: self.increment,
336            maximum: self.maximum,
337            standardization_length: self.standardization_length,
338            wma_length: self.wma_length,
339            wma_lag: self.wma_lag,
340            pivot_left: self.pivot_left,
341            pivot_right: self.pivot_right,
342            plot_bullish: self.plot_bullish,
343            plot_bearish: self.plot_bearish,
344        }
345    }
346
347    #[inline(always)]
348    pub fn apply(
349        self,
350        candles: &Candles,
351    ) -> Result<StandardizedPsarOscillatorOutput, StandardizedPsarOscillatorError> {
352        let kernel = self.kernel;
353        let params = self.params();
354        standardized_psar_oscillator_with_kernel(
355            &StandardizedPsarOscillatorInput::from_candles(candles, params),
356            kernel,
357        )
358    }
359
360    #[inline(always)]
361    pub fn apply_slices(
362        self,
363        high: &[f64],
364        low: &[f64],
365        close: &[f64],
366    ) -> Result<StandardizedPsarOscillatorOutput, StandardizedPsarOscillatorError> {
367        let kernel = self.kernel;
368        let params = self.params();
369        standardized_psar_oscillator_with_kernel(
370            &StandardizedPsarOscillatorInput::from_slices(high, low, close, params),
371            kernel,
372        )
373    }
374
375    #[inline(always)]
376    pub fn into_stream(
377        self,
378    ) -> Result<StandardizedPsarOscillatorStream, StandardizedPsarOscillatorError> {
379        StandardizedPsarOscillatorStream::try_new(self.params())
380    }
381}
382
383#[derive(Debug, Error)]
384pub enum StandardizedPsarOscillatorError {
385    #[error("standardized_psar_oscillator: input data slice is empty.")]
386    EmptyInputData,
387    #[error("standardized_psar_oscillator: all values are NaN.")]
388    AllValuesNaN,
389    #[error(
390        "standardized_psar_oscillator: inconsistent data lengths - high = {high_len}, low = {low_len}, close = {close_len}"
391    )]
392    DataLengthMismatch {
393        high_len: usize,
394        low_len: usize,
395        close_len: usize,
396    },
397    #[error("standardized_psar_oscillator: invalid start: {start}")]
398    InvalidStart { start: f64 },
399    #[error("standardized_psar_oscillator: invalid increment: {increment}")]
400    InvalidIncrement { increment: f64 },
401    #[error("standardized_psar_oscillator: invalid maximum: {maximum}")]
402    InvalidMaximum { maximum: f64 },
403    #[error(
404        "standardized_psar_oscillator: invalid standardization_length: {standardization_length}, data length = {data_len}"
405    )]
406    InvalidStandardizationLength {
407        standardization_length: usize,
408        data_len: usize,
409    },
410    #[error(
411        "standardized_psar_oscillator: invalid wma_length: {wma_length}, data length = {data_len}"
412    )]
413    InvalidWmaLength { wma_length: usize, data_len: usize },
414    #[error("standardized_psar_oscillator: invalid wma_lag: {wma_lag}")]
415    InvalidWmaLag { wma_lag: usize },
416    #[error(
417        "standardized_psar_oscillator: invalid pivot_left: {pivot_left}, data length = {data_len}"
418    )]
419    InvalidPivotLeft { pivot_left: usize, data_len: usize },
420    #[error(
421        "standardized_psar_oscillator: invalid pivot_right: {pivot_right}, data length = {data_len}"
422    )]
423    InvalidPivotRight { pivot_right: usize, data_len: usize },
424    #[error(
425        "standardized_psar_oscillator: not enough valid data: needed = {needed}, valid = {valid}"
426    )]
427    NotEnoughValidData { needed: usize, valid: usize },
428    #[error(
429        "standardized_psar_oscillator: output length mismatch: expected = {expected}, got = {got}"
430    )]
431    OutputLengthMismatch { expected: usize, got: usize },
432    #[error(
433        "standardized_psar_oscillator: invalid range for {axis}: start = {start}, end = {end}, step = {step}"
434    )]
435    InvalidRange {
436        axis: &'static str,
437        start: String,
438        end: String,
439        step: String,
440    },
441    #[error("standardized_psar_oscillator: invalid kernel for batch: {0:?}")]
442    InvalidKernelForBatch(Kernel),
443}
444
445#[derive(Clone, Copy, Debug)]
446struct PreparedInput<'a> {
447    high: &'a [f64],
448    low: &'a [f64],
449    close: &'a [f64],
450    start: f64,
451    increment: f64,
452    maximum: f64,
453    standardization_length: usize,
454    wma_length: usize,
455    wma_lag: usize,
456    pivot_left: usize,
457    pivot_right: usize,
458    plot_bullish: bool,
459    plot_bearish: bool,
460    warmup: usize,
461}
462
463#[inline(always)]
464fn normalize_single_kernel(_kernel: Kernel) -> Kernel {
465    Kernel::Scalar
466}
467
468#[inline(always)]
469fn validate_params(
470    start: f64,
471    increment: f64,
472    maximum: f64,
473    standardization_length: usize,
474    wma_length: usize,
475    wma_lag: usize,
476    pivot_left: usize,
477    pivot_right: usize,
478    data_len: usize,
479) -> Result<(), StandardizedPsarOscillatorError> {
480    if !start.is_finite() || start <= 0.0 {
481        return Err(StandardizedPsarOscillatorError::InvalidStart { start });
482    }
483    if !increment.is_finite() || increment <= 0.0 {
484        return Err(StandardizedPsarOscillatorError::InvalidIncrement { increment });
485    }
486    if !maximum.is_finite() || maximum <= 0.0 || maximum < start {
487        return Err(StandardizedPsarOscillatorError::InvalidMaximum { maximum });
488    }
489    if standardization_length == 0 || standardization_length > data_len {
490        return Err(
491            StandardizedPsarOscillatorError::InvalidStandardizationLength {
492                standardization_length,
493                data_len,
494            },
495        );
496    }
497    if wma_length == 0 || wma_length > data_len {
498        return Err(StandardizedPsarOscillatorError::InvalidWmaLength {
499            wma_length,
500            data_len,
501        });
502    }
503    if wma_lag > data_len {
504        return Err(StandardizedPsarOscillatorError::InvalidWmaLag { wma_lag });
505    }
506    if pivot_left == 0 || pivot_left > data_len {
507        return Err(StandardizedPsarOscillatorError::InvalidPivotLeft {
508            pivot_left,
509            data_len,
510        });
511    }
512    if pivot_right > data_len {
513        return Err(StandardizedPsarOscillatorError::InvalidPivotRight {
514            pivot_right,
515            data_len,
516        });
517    }
518    Ok(())
519}
520
521#[inline(always)]
522fn analyze_valid_segments(
523    high: &[f64],
524    low: &[f64],
525    close: &[f64],
526) -> Result<(usize, usize), StandardizedPsarOscillatorError> {
527    if high.is_empty() || low.is_empty() || close.is_empty() {
528        return Err(StandardizedPsarOscillatorError::EmptyInputData);
529    }
530    if high.len() != low.len() || high.len() != close.len() {
531        return Err(StandardizedPsarOscillatorError::DataLengthMismatch {
532            high_len: high.len(),
533            low_len: low.len(),
534            close_len: close.len(),
535        });
536    }
537
538    let mut first_valid = None;
539    let mut max_run = 0usize;
540    let mut run = 0usize;
541
542    for i in 0..close.len() {
543        let valid = high[i].is_finite() && low[i].is_finite() && close[i].is_finite();
544        if valid {
545            if first_valid.is_none() {
546                first_valid = Some(i);
547            }
548            run += 1;
549            if run > max_run {
550                max_run = run;
551            }
552        } else {
553            run = 0;
554        }
555    }
556
557    match first_valid {
558        Some(first) => Ok((first, max_run)),
559        None => Err(StandardizedPsarOscillatorError::AllValuesNaN),
560    }
561}
562
563#[inline(always)]
564fn required_valid_bars(standardization_length: usize, wma_length: usize) -> usize {
565    standardization_length.max(2) + wma_length - 1
566}
567
568#[inline(always)]
569fn prepare_input<'a>(
570    input: &'a StandardizedPsarOscillatorInput<'a>,
571    kernel: Kernel,
572) -> Result<PreparedInput<'a>, StandardizedPsarOscillatorError> {
573    let _chosen = normalize_single_kernel(kernel);
574    let (high, low, close) = input.as_hlc();
575    let start = input.get_start();
576    let increment = input.get_increment();
577    let maximum = input.get_maximum();
578    let standardization_length = input.get_standardization_length();
579    let wma_length = input.get_wma_length();
580    let wma_lag = input.get_wma_lag();
581    let pivot_left = input.get_pivot_left();
582    let pivot_right = input.get_pivot_right();
583    validate_params(
584        start,
585        increment,
586        maximum,
587        standardization_length,
588        wma_length,
589        wma_lag,
590        pivot_left,
591        pivot_right,
592        close.len(),
593    )?;
594    let (first_valid, max_run) = analyze_valid_segments(high, low, close)?;
595    let needed = required_valid_bars(standardization_length, wma_length);
596    if max_run < needed {
597        return Err(StandardizedPsarOscillatorError::NotEnoughValidData {
598            needed,
599            valid: max_run,
600        });
601    }
602    Ok(PreparedInput {
603        high,
604        low,
605        close,
606        start,
607        increment,
608        maximum,
609        standardization_length,
610        wma_length,
611        wma_lag,
612        pivot_left,
613        pivot_right,
614        plot_bullish: input.get_plot_bullish(),
615        plot_bearish: input.get_plot_bearish(),
616        warmup: first_valid + needed - 1,
617    })
618}
619
620#[derive(Clone, Debug)]
621struct EmaState {
622    period: usize,
623    alpha: f64,
624    beta: f64,
625    count: usize,
626    mean: f64,
627}
628
629impl EmaState {
630    #[inline(always)]
631    fn new(period: usize) -> Self {
632        let alpha = 2.0 / (period as f64 + 1.0);
633        Self {
634            period,
635            alpha,
636            beta: 1.0 - alpha,
637            count: 0,
638            mean: f64::NAN,
639        }
640    }
641
642    #[inline(always)]
643    fn reset(&mut self) {
644        self.count = 0;
645        self.mean = f64::NAN;
646    }
647
648    #[inline(always)]
649    fn update(&mut self, value: f64) -> Option<f64> {
650        self.count += 1;
651        if self.count == 1 {
652            self.mean = value;
653        } else if self.count <= self.period {
654            let inv = 1.0 / self.count as f64;
655            self.mean = (value - self.mean).mul_add(inv, self.mean);
656        } else {
657            self.mean = self.beta.mul_add(self.mean, self.alpha * value);
658        }
659        if self.count >= self.period {
660            Some(self.mean)
661        } else {
662            None
663        }
664    }
665}
666
667#[derive(Clone, Debug)]
668struct WmaState {
669    buffer: Vec<f64>,
670    head: usize,
671    count: usize,
672    sum: f64,
673    weighted_sum: f64,
674    denominator: f64,
675}
676
677impl WmaState {
678    #[inline(always)]
679    fn new(period: usize) -> Self {
680        Self {
681            buffer: vec![0.0; period],
682            head: 0,
683            count: 0,
684            sum: 0.0,
685            weighted_sum: 0.0,
686            denominator: (period * (period + 1) / 2) as f64,
687        }
688    }
689
690    #[inline(always)]
691    fn reset(&mut self) {
692        self.head = 0;
693        self.count = 0;
694        self.sum = 0.0;
695        self.weighted_sum = 0.0;
696    }
697
698    #[inline(always)]
699    fn update(&mut self, value: f64) -> Option<f64> {
700        let len = self.buffer.len();
701        if self.count < len {
702            self.count += 1;
703            self.buffer[self.head] = value;
704            self.head += 1;
705            if self.head == len {
706                self.head = 0;
707            }
708            self.sum += value;
709            self.weighted_sum += value * self.count as f64;
710            if self.count == len {
711                Some(self.weighted_sum / self.denominator)
712            } else {
713                None
714            }
715        } else {
716            let oldest = self.buffer[self.head];
717            let old_sum = self.sum;
718            self.weighted_sum = self.weighted_sum - old_sum + value * len as f64;
719            self.sum = old_sum - oldest + value;
720            self.buffer[self.head] = value;
721            self.head += 1;
722            if self.head == len {
723                self.head = 0;
724            }
725            Some(self.weighted_sum / self.denominator)
726        }
727    }
728}
729
730#[derive(Clone, Debug)]
731struct PsarTrendState {
732    trend_up: bool,
733    sar: f64,
734    ep: f64,
735    acc: f64,
736    prev_high: f64,
737    prev_high2: f64,
738    prev_low: f64,
739    prev_low2: f64,
740}
741
742#[derive(Clone, Debug)]
743struct PsarState {
744    start: f64,
745    increment: f64,
746    maximum: f64,
747    state: Option<PsarTrendState>,
748    idx: usize,
749}
750
751impl PsarState {
752    #[inline(always)]
753    fn new(start: f64, increment: f64, maximum: f64) -> Self {
754        Self {
755            start,
756            increment,
757            maximum,
758            state: None,
759            idx: 0,
760        }
761    }
762
763    #[inline(always)]
764    fn reset(&mut self) {
765        self.state = None;
766        self.idx = 0;
767    }
768
769    #[inline(always)]
770    fn update(&mut self, high: f64, low: f64) -> Option<f64> {
771        match self.state.as_mut() {
772            None => {
773                self.state = Some(PsarTrendState {
774                    trend_up: false,
775                    sar: f64::NAN,
776                    ep: f64::NAN,
777                    acc: self.start,
778                    prev_high: high,
779                    prev_high2: high,
780                    prev_low: low,
781                    prev_low2: low,
782                });
783                self.idx = 1;
784                None
785            }
786            Some(st) if self.idx == 1 => {
787                let trend_up = high > st.prev_high;
788                let sar = if trend_up { st.prev_low } else { st.prev_high };
789                let ep = if trend_up { high } else { low };
790
791                st.prev_high2 = st.prev_high;
792                st.prev_low2 = st.prev_low;
793                st.prev_high = high;
794                st.prev_low = low;
795                st.trend_up = trend_up;
796                st.sar = sar;
797                st.ep = ep;
798                st.acc = self.start;
799                self.idx = 2;
800                Some(sar)
801            }
802            Some(st) => {
803                let mut next_sar = st.acc.mul_add(st.ep - st.sar, st.sar);
804
805                if st.trend_up {
806                    if low < next_sar {
807                        st.trend_up = false;
808                        next_sar = st.ep;
809                        st.ep = low;
810                        st.acc = self.start;
811                    } else {
812                        if high > st.ep {
813                            st.ep = high;
814                            st.acc = (st.acc + self.increment).min(self.maximum);
815                        }
816                        next_sar = next_sar.min(st.prev_low.min(st.prev_low2));
817                    }
818                } else if high > next_sar {
819                    st.trend_up = true;
820                    next_sar = st.ep;
821                    st.ep = high;
822                    st.acc = self.start;
823                } else {
824                    if low < st.ep {
825                        st.ep = low;
826                        st.acc = (st.acc + self.increment).min(self.maximum);
827                    }
828                    next_sar = next_sar.max(st.prev_high.max(st.prev_high2));
829                }
830
831                st.prev_high2 = st.prev_high;
832                st.prev_low2 = st.prev_low;
833                st.prev_high = high;
834                st.prev_low = low;
835                st.sar = next_sar;
836                self.idx += 1;
837                Some(next_sar)
838            }
839        }
840    }
841}
842
843#[derive(Clone, Copy, Debug)]
844struct PivotEvent {
845    confirm_index: usize,
846    oscillator: f64,
847    price: f64,
848}
849
850#[derive(Clone, Debug)]
851struct StandardizedPsarOscillatorState {
852    psar: PsarState,
853    range_ema: EmaState,
854    wma: WmaState,
855    wma_lag: usize,
856    pivot_left: usize,
857    pivot_right: usize,
858    plot_bullish: bool,
859    plot_bearish: bool,
860    oscillator_history: Vec<f64>,
861    ma_history: Vec<f64>,
862    low_history: Vec<f64>,
863    high_history: Vec<f64>,
864    previous_low_pivot: Option<PivotEvent>,
865    previous_high_pivot: Option<PivotEvent>,
866    previous_oscillator: f64,
867}
868
869impl StandardizedPsarOscillatorState {
870    #[inline(always)]
871    fn new(
872        start: f64,
873        increment: f64,
874        maximum: f64,
875        standardization_length: usize,
876        wma_length: usize,
877        wma_lag: usize,
878        pivot_left: usize,
879        pivot_right: usize,
880        plot_bullish: bool,
881        plot_bearish: bool,
882    ) -> Self {
883        Self {
884            psar: PsarState::new(start, increment, maximum),
885            range_ema: EmaState::new(standardization_length),
886            wma: WmaState::new(wma_length),
887            wma_lag,
888            pivot_left,
889            pivot_right,
890            plot_bullish,
891            plot_bearish,
892            oscillator_history: Vec::new(),
893            ma_history: Vec::new(),
894            low_history: Vec::new(),
895            high_history: Vec::new(),
896            previous_low_pivot: None,
897            previous_high_pivot: None,
898            previous_oscillator: f64::NAN,
899        }
900    }
901
902    #[inline(always)]
903    fn reset(&mut self) {
904        self.psar.reset();
905        self.range_ema.reset();
906        self.wma.reset();
907        self.oscillator_history.clear();
908        self.ma_history.clear();
909        self.low_history.clear();
910        self.high_history.clear();
911        self.previous_low_pivot = None;
912        self.previous_high_pivot = None;
913        self.previous_oscillator = f64::NAN;
914    }
915
916    #[inline(always)]
917    fn update(
918        &mut self,
919        high: f64,
920        low: f64,
921        close: f64,
922    ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64)> {
923        if !high.is_finite() || !low.is_finite() || !close.is_finite() {
924            self.reset();
925            return None;
926        }
927
928        let psar = self.psar.update(high, low);
929        let range = self.range_ema.update(high - low);
930        let oscillator = match (psar, range) {
931            (Some(sar), Some(ema_range)) if ema_range.is_finite() && ema_range != 0.0 => {
932                (close - sar) / ema_range * 100.0
933            }
934            _ => f64::NAN,
935        };
936
937        let ma = if oscillator.is_finite() {
938            self.wma.update(oscillator).unwrap_or(f64::NAN)
939        } else {
940            f64::NAN
941        };
942
943        let bearish_reversal = if self.previous_oscillator.is_finite()
944            && oscillator.is_finite()
945            && self.previous_oscillator >= REVERSAL_LEVEL
946            && oscillator < REVERSAL_LEVEL
947        {
948            REVERSAL_MARKER
949        } else {
950            f64::NAN
951        };
952
953        let bullish_reversal = if self.previous_oscillator.is_finite()
954            && oscillator.is_finite()
955            && self.previous_oscillator <= -REVERSAL_LEVEL
956            && oscillator > -REVERSAL_LEVEL
957        {
958            -REVERSAL_MARKER
959        } else {
960            f64::NAN
961        };
962
963        let lag_ma = if self.wma_lag == 0 || self.ma_history.len() < self.wma_lag {
964            f64::NAN
965        } else {
966            self.ma_history[self.ma_history.len() - self.wma_lag]
967        };
968
969        let bullish_weakening = if ma.is_finite() && lag_ma.is_finite() {
970            if oscillator > 0.0 && ma < lag_ma {
971                1.0
972            } else {
973                0.0
974            }
975        } else {
976            f64::NAN
977        };
978
979        let bearish_weakening = if ma.is_finite() && lag_ma.is_finite() {
980            if oscillator < 0.0 && ma > lag_ma {
981                1.0
982            } else {
983                0.0
984            }
985        } else {
986            f64::NAN
987        };
988
989        self.previous_oscillator = oscillator;
990        self.oscillator_history.push(oscillator);
991        self.ma_history.push(ma);
992        self.low_history.push(low);
993        self.high_history.push(high);
994
995        let mut regular_bullish = f64::NAN;
996        let mut regular_bearish = f64::NAN;
997        let len = self.oscillator_history.len();
998        let needed = self.pivot_left + self.pivot_right + 1;
999
1000        if len >= needed {
1001            let center = len - 1 - self.pivot_right;
1002            let start = center - self.pivot_left;
1003            let end = center + self.pivot_right;
1004            let center_oscillator = self.oscillator_history[center];
1005
1006            if center_oscillator.is_finite() {
1007                let mut pivot_low = true;
1008                let mut pivot_high = true;
1009
1010                for idx in start..=end {
1011                    let value = self.oscillator_history[idx];
1012                    if !value.is_finite() {
1013                        pivot_low = false;
1014                        pivot_high = false;
1015                        break;
1016                    }
1017                    if idx != center {
1018                        if value < center_oscillator {
1019                            pivot_low = false;
1020                        }
1021                        if value > center_oscillator {
1022                            pivot_high = false;
1023                        }
1024                    }
1025                    if !pivot_low && !pivot_high {
1026                        break;
1027                    }
1028                }
1029
1030                let confirm_index = len - 1;
1031
1032                if pivot_low {
1033                    let event = PivotEvent {
1034                        confirm_index,
1035                        oscillator: center_oscillator,
1036                        price: self.low_history[center],
1037                    };
1038                    if self.plot_bullish {
1039                        if let Some(previous) = self.previous_low_pivot {
1040                            let bars = event.confirm_index.saturating_sub(previous.confirm_index);
1041                            if (1..=MAX_PIVOT_BARS).contains(&bars)
1042                                && event.oscillator > previous.oscillator
1043                                && event.price < previous.price
1044                            {
1045                                regular_bullish = event.oscillator;
1046                            }
1047                        }
1048                    }
1049                    self.previous_low_pivot = Some(event);
1050                }
1051
1052                if pivot_high {
1053                    let event = PivotEvent {
1054                        confirm_index,
1055                        oscillator: center_oscillator,
1056                        price: self.high_history[center],
1057                    };
1058                    if self.plot_bearish {
1059                        if let Some(previous) = self.previous_high_pivot {
1060                            let bars = event.confirm_index.saturating_sub(previous.confirm_index);
1061                            if (1..=MAX_PIVOT_BARS).contains(&bars)
1062                                && event.oscillator < previous.oscillator
1063                                && event.price > previous.price
1064                            {
1065                                regular_bearish = event.oscillator;
1066                            }
1067                        }
1068                    }
1069                    self.previous_high_pivot = Some(event);
1070                }
1071            }
1072        }
1073
1074        if oscillator.is_finite() {
1075            Some((
1076                oscillator,
1077                ma,
1078                bullish_reversal,
1079                bearish_reversal,
1080                regular_bullish,
1081                regular_bearish,
1082                bullish_weakening,
1083                bearish_weakening,
1084            ))
1085        } else {
1086            None
1087        }
1088    }
1089}
1090
1091#[derive(Clone, Debug)]
1092pub struct StandardizedPsarOscillatorStream {
1093    params: StandardizedPsarOscillatorParams,
1094    state: StandardizedPsarOscillatorState,
1095}
1096
1097impl StandardizedPsarOscillatorStream {
1098    #[inline(always)]
1099    pub fn try_new(
1100        params: StandardizedPsarOscillatorParams,
1101    ) -> Result<Self, StandardizedPsarOscillatorError> {
1102        let start = params.start.unwrap_or(DEFAULT_START);
1103        let increment = params.increment.unwrap_or(DEFAULT_INCREMENT);
1104        let maximum = params.maximum.unwrap_or(DEFAULT_MAXIMUM);
1105        let standardization_length = params
1106            .standardization_length
1107            .unwrap_or(DEFAULT_STANDARDIZATION_LENGTH);
1108        let wma_length = params.wma_length.unwrap_or(DEFAULT_WMA_LENGTH);
1109        let wma_lag = params.wma_lag.unwrap_or(DEFAULT_WMA_LAG);
1110        let pivot_left = params.pivot_left.unwrap_or(DEFAULT_PIVOT_LEFT);
1111        let pivot_right = params.pivot_right.unwrap_or(DEFAULT_PIVOT_RIGHT);
1112        validate_params(
1113            start,
1114            increment,
1115            maximum,
1116            standardization_length,
1117            wma_length,
1118            wma_lag,
1119            pivot_left,
1120            pivot_right,
1121            usize::MAX,
1122        )?;
1123        Ok(Self {
1124            state: StandardizedPsarOscillatorState::new(
1125                start,
1126                increment,
1127                maximum,
1128                standardization_length,
1129                wma_length,
1130                wma_lag,
1131                pivot_left,
1132                pivot_right,
1133                params.plot_bullish.unwrap_or(DEFAULT_PLOT_BULLISH),
1134                params.plot_bearish.unwrap_or(DEFAULT_PLOT_BEARISH),
1135            ),
1136            params,
1137        })
1138    }
1139
1140    #[inline(always)]
1141    pub fn update(
1142        &mut self,
1143        high: f64,
1144        low: f64,
1145        close: f64,
1146    ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64)> {
1147        self.state.update(high, low, close)
1148    }
1149
1150    #[inline(always)]
1151    pub fn params(&self) -> &StandardizedPsarOscillatorParams {
1152        &self.params
1153    }
1154}
1155
1156#[derive(Clone, Debug)]
1157pub struct StandardizedPsarOscillatorBatchRange {
1158    pub start: (f64, f64, f64),
1159    pub increment: (f64, f64, f64),
1160    pub maximum: (f64, f64, f64),
1161    pub standardization_length: (usize, usize, usize),
1162    pub wma_length: (usize, usize, usize),
1163    pub wma_lag: (usize, usize, usize),
1164    pub pivot_left: (usize, usize, usize),
1165    pub pivot_right: (usize, usize, usize),
1166    pub plot_bullish: bool,
1167    pub plot_bearish: bool,
1168}
1169
1170impl Default for StandardizedPsarOscillatorBatchRange {
1171    fn default() -> Self {
1172        Self {
1173            start: (DEFAULT_START, DEFAULT_START, 0.0),
1174            increment: (DEFAULT_INCREMENT, DEFAULT_INCREMENT, 0.0),
1175            maximum: (DEFAULT_MAXIMUM, DEFAULT_MAXIMUM, 0.0),
1176            standardization_length: (
1177                DEFAULT_STANDARDIZATION_LENGTH,
1178                DEFAULT_STANDARDIZATION_LENGTH,
1179                0,
1180            ),
1181            wma_length: (DEFAULT_WMA_LENGTH, DEFAULT_WMA_LENGTH, 0),
1182            wma_lag: (DEFAULT_WMA_LAG, DEFAULT_WMA_LAG, 0),
1183            pivot_left: (DEFAULT_PIVOT_LEFT, DEFAULT_PIVOT_LEFT, 0),
1184            pivot_right: (DEFAULT_PIVOT_RIGHT, DEFAULT_PIVOT_RIGHT, 0),
1185            plot_bullish: DEFAULT_PLOT_BULLISH,
1186            plot_bearish: DEFAULT_PLOT_BEARISH,
1187        }
1188    }
1189}
1190
1191#[derive(Clone, Debug, Default)]
1192pub struct StandardizedPsarOscillatorBatchBuilder {
1193    range: StandardizedPsarOscillatorBatchRange,
1194    kernel: Kernel,
1195}
1196
1197impl StandardizedPsarOscillatorBatchBuilder {
1198    #[inline(always)]
1199    pub fn new() -> Self {
1200        Self::default()
1201    }
1202
1203    #[inline(always)]
1204    pub fn kernel(mut self, kernel: Kernel) -> Self {
1205        self.kernel = kernel;
1206        self
1207    }
1208
1209    #[inline(always)]
1210    pub fn start_range(mut self, start: f64, end: f64, step: f64) -> Self {
1211        self.range.start = (start, end, step);
1212        self
1213    }
1214
1215    #[inline(always)]
1216    pub fn increment_range(mut self, start: f64, end: f64, step: f64) -> Self {
1217        self.range.increment = (start, end, step);
1218        self
1219    }
1220
1221    #[inline(always)]
1222    pub fn maximum_range(mut self, start: f64, end: f64, step: f64) -> Self {
1223        self.range.maximum = (start, end, step);
1224        self
1225    }
1226
1227    #[inline(always)]
1228    pub fn standardization_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1229        self.range.standardization_length = (start, end, step);
1230        self
1231    }
1232
1233    #[inline(always)]
1234    pub fn wma_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1235        self.range.wma_length = (start, end, step);
1236        self
1237    }
1238
1239    #[inline(always)]
1240    pub fn wma_lag_range(mut self, start: usize, end: usize, step: usize) -> Self {
1241        self.range.wma_lag = (start, end, step);
1242        self
1243    }
1244
1245    #[inline(always)]
1246    pub fn pivot_left_range(mut self, start: usize, end: usize, step: usize) -> Self {
1247        self.range.pivot_left = (start, end, step);
1248        self
1249    }
1250
1251    #[inline(always)]
1252    pub fn pivot_right_range(mut self, start: usize, end: usize, step: usize) -> Self {
1253        self.range.pivot_right = (start, end, step);
1254        self
1255    }
1256
1257    #[inline(always)]
1258    pub fn plot_bullish(mut self, value: bool) -> Self {
1259        self.range.plot_bullish = value;
1260        self
1261    }
1262
1263    #[inline(always)]
1264    pub fn plot_bearish(mut self, value: bool) -> Self {
1265        self.range.plot_bearish = value;
1266        self
1267    }
1268
1269    #[inline(always)]
1270    pub fn apply_slices(
1271        self,
1272        high: &[f64],
1273        low: &[f64],
1274        close: &[f64],
1275    ) -> Result<StandardizedPsarOscillatorBatchOutput, StandardizedPsarOscillatorError> {
1276        standardized_psar_oscillator_batch_with_kernel(high, low, close, &self.range, self.kernel)
1277    }
1278
1279    #[inline(always)]
1280    pub fn apply(
1281        self,
1282        candles: &Candles,
1283    ) -> Result<StandardizedPsarOscillatorBatchOutput, StandardizedPsarOscillatorError> {
1284        self.apply_slices(&candles.high, &candles.low, &candles.close)
1285    }
1286}
1287
1288#[derive(Clone, Debug)]
1289pub struct StandardizedPsarOscillatorBatchOutput {
1290    pub oscillator: Vec<f64>,
1291    pub ma: Vec<f64>,
1292    pub bullish_reversal: Vec<f64>,
1293    pub bearish_reversal: Vec<f64>,
1294    pub regular_bullish: Vec<f64>,
1295    pub regular_bearish: Vec<f64>,
1296    pub bullish_weakening: Vec<f64>,
1297    pub bearish_weakening: Vec<f64>,
1298    pub combos: Vec<StandardizedPsarOscillatorParams>,
1299    pub rows: usize,
1300    pub cols: usize,
1301}
1302
1303#[inline(always)]
1304fn axis_usize(
1305    axis: &'static str,
1306    (start, end, step): (usize, usize, usize),
1307) -> Result<Vec<usize>, StandardizedPsarOscillatorError> {
1308    if step == 0 || start == end {
1309        return Ok(vec![start]);
1310    }
1311
1312    let mut out = Vec::new();
1313    if start < end {
1314        let mut value = start;
1315        while value <= end {
1316            out.push(value);
1317            match value.checked_add(step) {
1318                Some(next) if next > value => value = next,
1319                _ => break,
1320            }
1321        }
1322    } else {
1323        let mut value = start;
1324        while value >= end {
1325            out.push(value);
1326            if value == end {
1327                break;
1328            }
1329            match value.checked_sub(step) {
1330                Some(next) if next < value => value = next,
1331                _ => break,
1332            }
1333        }
1334    }
1335
1336    if out.is_empty() || !out.last().is_some_and(|value| *value == end) {
1337        return Err(StandardizedPsarOscillatorError::InvalidRange {
1338            axis,
1339            start: start.to_string(),
1340            end: end.to_string(),
1341            step: step.to_string(),
1342        });
1343    }
1344    Ok(out)
1345}
1346
1347#[inline(always)]
1348fn axis_float(
1349    axis: &'static str,
1350    (start, end, step): (f64, f64, f64),
1351) -> Result<Vec<f64>, StandardizedPsarOscillatorError> {
1352    if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1353        return Err(StandardizedPsarOscillatorError::InvalidRange {
1354            axis,
1355            start: start.to_string(),
1356            end: end.to_string(),
1357            step: step.to_string(),
1358        });
1359    }
1360    if step == 0.0 || start == end {
1361        return Ok(vec![start]);
1362    }
1363    if step < 0.0 {
1364        return Err(StandardizedPsarOscillatorError::InvalidRange {
1365            axis,
1366            start: start.to_string(),
1367            end: end.to_string(),
1368            step: step.to_string(),
1369        });
1370    }
1371
1372    let eps = step.abs() * 1e-9 + 1e-12;
1373    let mut out = Vec::new();
1374    if start < end {
1375        let mut value = start;
1376        while value <= end + eps {
1377            out.push(value);
1378            value += step;
1379        }
1380    } else {
1381        let mut value = start;
1382        while value + eps >= end {
1383            out.push(value);
1384            value -= step;
1385        }
1386    }
1387
1388    if out.is_empty() {
1389        return Err(StandardizedPsarOscillatorError::InvalidRange {
1390            axis,
1391            start: start.to_string(),
1392            end: end.to_string(),
1393            step: step.to_string(),
1394        });
1395    }
1396    Ok(out)
1397}
1398
1399pub fn expand_grid_standardized_psar_oscillator(
1400    sweep: &StandardizedPsarOscillatorBatchRange,
1401) -> Result<Vec<StandardizedPsarOscillatorParams>, StandardizedPsarOscillatorError> {
1402    let starts = axis_float("start", sweep.start)?;
1403    let increments = axis_float("increment", sweep.increment)?;
1404    let maximums = axis_float("maximum", sweep.maximum)?;
1405    let standardization_lengths =
1406        axis_usize("standardization_length", sweep.standardization_length)?;
1407    let wma_lengths = axis_usize("wma_length", sweep.wma_length)?;
1408    let wma_lags = axis_usize("wma_lag", sweep.wma_lag)?;
1409    let pivot_lefts = axis_usize("pivot_left", sweep.pivot_left)?;
1410    let pivot_rights = axis_usize("pivot_right", sweep.pivot_right)?;
1411
1412    let total = starts
1413        .len()
1414        .checked_mul(increments.len())
1415        .and_then(|value| value.checked_mul(maximums.len()))
1416        .and_then(|value| value.checked_mul(standardization_lengths.len()))
1417        .and_then(|value| value.checked_mul(wma_lengths.len()))
1418        .and_then(|value| value.checked_mul(wma_lags.len()))
1419        .and_then(|value| value.checked_mul(pivot_lefts.len()))
1420        .and_then(|value| value.checked_mul(pivot_rights.len()))
1421        .ok_or(StandardizedPsarOscillatorError::InvalidRange {
1422            axis: "grid",
1423            start: "overflow".to_string(),
1424            end: "overflow".to_string(),
1425            step: "overflow".to_string(),
1426        })?;
1427
1428    let mut out = Vec::with_capacity(total);
1429    for &start in &starts {
1430        for &increment in &increments {
1431            for &maximum in &maximums {
1432                for &standardization_length in &standardization_lengths {
1433                    for &wma_length in &wma_lengths {
1434                        for &wma_lag in &wma_lags {
1435                            for &pivot_left in &pivot_lefts {
1436                                for &pivot_right in &pivot_rights {
1437                                    out.push(StandardizedPsarOscillatorParams {
1438                                        start: Some(start),
1439                                        increment: Some(increment),
1440                                        maximum: Some(maximum),
1441                                        standardization_length: Some(standardization_length),
1442                                        wma_length: Some(wma_length),
1443                                        wma_lag: Some(wma_lag),
1444                                        pivot_left: Some(pivot_left),
1445                                        pivot_right: Some(pivot_right),
1446                                        plot_bullish: Some(sweep.plot_bullish),
1447                                        plot_bearish: Some(sweep.plot_bearish),
1448                                    });
1449                                }
1450                            }
1451                        }
1452                    }
1453                }
1454            }
1455        }
1456    }
1457    Ok(out)
1458}
1459
1460fn compute_row(
1461    high: &[f64],
1462    low: &[f64],
1463    close: &[f64],
1464    params: &StandardizedPsarOscillatorParams,
1465    oscillator_out: &mut [f64],
1466    ma_out: &mut [f64],
1467    bullish_reversal_out: &mut [f64],
1468    bearish_reversal_out: &mut [f64],
1469    regular_bullish_out: &mut [f64],
1470    regular_bearish_out: &mut [f64],
1471    bullish_weakening_out: &mut [f64],
1472    bearish_weakening_out: &mut [f64],
1473) -> Result<(), StandardizedPsarOscillatorError> {
1474    let len = close.len();
1475    for out in [
1476        &mut *oscillator_out,
1477        &mut *ma_out,
1478        &mut *bullish_reversal_out,
1479        &mut *bearish_reversal_out,
1480        &mut *regular_bullish_out,
1481        &mut *regular_bearish_out,
1482        &mut *bullish_weakening_out,
1483        &mut *bearish_weakening_out,
1484    ] {
1485        if out.len() != len {
1486            return Err(StandardizedPsarOscillatorError::OutputLengthMismatch {
1487                expected: len,
1488                got: out.len(),
1489            });
1490        }
1491    }
1492
1493    let mut state = StandardizedPsarOscillatorState::new(
1494        params.start.unwrap_or(DEFAULT_START),
1495        params.increment.unwrap_or(DEFAULT_INCREMENT),
1496        params.maximum.unwrap_or(DEFAULT_MAXIMUM),
1497        params
1498            .standardization_length
1499            .unwrap_or(DEFAULT_STANDARDIZATION_LENGTH),
1500        params.wma_length.unwrap_or(DEFAULT_WMA_LENGTH),
1501        params.wma_lag.unwrap_or(DEFAULT_WMA_LAG),
1502        params.pivot_left.unwrap_or(DEFAULT_PIVOT_LEFT),
1503        params.pivot_right.unwrap_or(DEFAULT_PIVOT_RIGHT),
1504        params.plot_bullish.unwrap_or(DEFAULT_PLOT_BULLISH),
1505        params.plot_bearish.unwrap_or(DEFAULT_PLOT_BEARISH),
1506    );
1507
1508    for i in 0..len {
1509        if let Some((
1510            oscillator,
1511            ma,
1512            bullish_reversal,
1513            bearish_reversal,
1514            regular_bullish,
1515            regular_bearish,
1516            bullish_weakening,
1517            bearish_weakening,
1518        )) = state.update(high[i], low[i], close[i])
1519        {
1520            oscillator_out[i] = oscillator;
1521            ma_out[i] = ma;
1522            bullish_reversal_out[i] = bullish_reversal;
1523            bearish_reversal_out[i] = bearish_reversal;
1524            regular_bullish_out[i] = regular_bullish;
1525            regular_bearish_out[i] = regular_bearish;
1526            bullish_weakening_out[i] = bullish_weakening;
1527            bearish_weakening_out[i] = bearish_weakening;
1528        } else {
1529            oscillator_out[i] = f64::NAN;
1530            ma_out[i] = f64::NAN;
1531            bullish_reversal_out[i] = f64::NAN;
1532            bearish_reversal_out[i] = f64::NAN;
1533            regular_bullish_out[i] = f64::NAN;
1534            regular_bearish_out[i] = f64::NAN;
1535            bullish_weakening_out[i] = f64::NAN;
1536            bearish_weakening_out[i] = f64::NAN;
1537        }
1538    }
1539
1540    Ok(())
1541}
1542
1543#[inline]
1544pub fn standardized_psar_oscillator(
1545    input: &StandardizedPsarOscillatorInput,
1546) -> Result<StandardizedPsarOscillatorOutput, StandardizedPsarOscillatorError> {
1547    standardized_psar_oscillator_with_kernel(input, Kernel::Auto)
1548}
1549
1550pub fn standardized_psar_oscillator_with_kernel(
1551    input: &StandardizedPsarOscillatorInput,
1552    kernel: Kernel,
1553) -> Result<StandardizedPsarOscillatorOutput, StandardizedPsarOscillatorError> {
1554    let prepared = prepare_input(input, kernel)?;
1555    let len = prepared.close.len();
1556
1557    let mut oscillator = alloc_with_nan_prefix(len, prepared.warmup);
1558    let mut ma = alloc_with_nan_prefix(len, prepared.warmup);
1559    let mut bullish_reversal = alloc_with_nan_prefix(len, prepared.warmup);
1560    let mut bearish_reversal = alloc_with_nan_prefix(len, prepared.warmup);
1561    let mut regular_bullish = alloc_with_nan_prefix(len, prepared.warmup);
1562    let mut regular_bearish = alloc_with_nan_prefix(len, prepared.warmup);
1563    let mut bullish_weakening = alloc_with_nan_prefix(len, prepared.warmup);
1564    let mut bearish_weakening = alloc_with_nan_prefix(len, prepared.warmup);
1565
1566    compute_row(
1567        prepared.high,
1568        prepared.low,
1569        prepared.close,
1570        &StandardizedPsarOscillatorParams {
1571            start: Some(prepared.start),
1572            increment: Some(prepared.increment),
1573            maximum: Some(prepared.maximum),
1574            standardization_length: Some(prepared.standardization_length),
1575            wma_length: Some(prepared.wma_length),
1576            wma_lag: Some(prepared.wma_lag),
1577            pivot_left: Some(prepared.pivot_left),
1578            pivot_right: Some(prepared.pivot_right),
1579            plot_bullish: Some(prepared.plot_bullish),
1580            plot_bearish: Some(prepared.plot_bearish),
1581        },
1582        &mut oscillator,
1583        &mut ma,
1584        &mut bullish_reversal,
1585        &mut bearish_reversal,
1586        &mut regular_bullish,
1587        &mut regular_bearish,
1588        &mut bullish_weakening,
1589        &mut bearish_weakening,
1590    )?;
1591
1592    Ok(StandardizedPsarOscillatorOutput {
1593        oscillator,
1594        ma,
1595        bullish_reversal,
1596        bearish_reversal,
1597        regular_bullish,
1598        regular_bearish,
1599        bullish_weakening,
1600        bearish_weakening,
1601    })
1602}
1603
1604#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1605pub fn standardized_psar_oscillator_into(
1606    oscillator_out: &mut [f64],
1607    ma_out: &mut [f64],
1608    bullish_reversal_out: &mut [f64],
1609    bearish_reversal_out: &mut [f64],
1610    regular_bullish_out: &mut [f64],
1611    regular_bearish_out: &mut [f64],
1612    bullish_weakening_out: &mut [f64],
1613    bearish_weakening_out: &mut [f64],
1614    input: &StandardizedPsarOscillatorInput,
1615) -> Result<(), StandardizedPsarOscillatorError> {
1616    standardized_psar_oscillator_into_slice(
1617        oscillator_out,
1618        ma_out,
1619        bullish_reversal_out,
1620        bearish_reversal_out,
1621        regular_bullish_out,
1622        regular_bearish_out,
1623        bullish_weakening_out,
1624        bearish_weakening_out,
1625        input,
1626        Kernel::Auto,
1627    )
1628}
1629
1630pub fn standardized_psar_oscillator_into_slice(
1631    oscillator_out: &mut [f64],
1632    ma_out: &mut [f64],
1633    bullish_reversal_out: &mut [f64],
1634    bearish_reversal_out: &mut [f64],
1635    regular_bullish_out: &mut [f64],
1636    regular_bearish_out: &mut [f64],
1637    bullish_weakening_out: &mut [f64],
1638    bearish_weakening_out: &mut [f64],
1639    input: &StandardizedPsarOscillatorInput,
1640    kernel: Kernel,
1641) -> Result<(), StandardizedPsarOscillatorError> {
1642    let prepared = prepare_input(input, kernel)?;
1643    compute_row(
1644        prepared.high,
1645        prepared.low,
1646        prepared.close,
1647        &StandardizedPsarOscillatorParams {
1648            start: Some(prepared.start),
1649            increment: Some(prepared.increment),
1650            maximum: Some(prepared.maximum),
1651            standardization_length: Some(prepared.standardization_length),
1652            wma_length: Some(prepared.wma_length),
1653            wma_lag: Some(prepared.wma_lag),
1654            pivot_left: Some(prepared.pivot_left),
1655            pivot_right: Some(prepared.pivot_right),
1656            plot_bullish: Some(prepared.plot_bullish),
1657            plot_bearish: Some(prepared.plot_bearish),
1658        },
1659        oscillator_out,
1660        ma_out,
1661        bullish_reversal_out,
1662        bearish_reversal_out,
1663        regular_bullish_out,
1664        regular_bearish_out,
1665        bullish_weakening_out,
1666        bearish_weakening_out,
1667    )
1668}
1669
1670fn standardized_psar_oscillator_batch_inner_into(
1671    high: &[f64],
1672    low: &[f64],
1673    close: &[f64],
1674    sweep: &StandardizedPsarOscillatorBatchRange,
1675    parallel: bool,
1676    oscillator_out: &mut [f64],
1677    ma_out: &mut [f64],
1678    bullish_reversal_out: &mut [f64],
1679    bearish_reversal_out: &mut [f64],
1680    regular_bullish_out: &mut [f64],
1681    regular_bearish_out: &mut [f64],
1682    bullish_weakening_out: &mut [f64],
1683    bearish_weakening_out: &mut [f64],
1684) -> Result<Vec<StandardizedPsarOscillatorParams>, StandardizedPsarOscillatorError> {
1685    let (_, max_run) = analyze_valid_segments(high, low, close)?;
1686    let combos = expand_grid_standardized_psar_oscillator(sweep)?;
1687    let rows = combos.len();
1688    let cols = close.len();
1689    let expected =
1690        rows.checked_mul(cols)
1691            .ok_or(StandardizedPsarOscillatorError::OutputLengthMismatch {
1692                expected: usize::MAX,
1693                got: oscillator_out.len(),
1694            })?;
1695
1696    for out in [
1697        &mut *oscillator_out,
1698        &mut *ma_out,
1699        &mut *bullish_reversal_out,
1700        &mut *bearish_reversal_out,
1701        &mut *regular_bullish_out,
1702        &mut *regular_bearish_out,
1703        &mut *bullish_weakening_out,
1704        &mut *bearish_weakening_out,
1705    ] {
1706        if out.len() != expected {
1707            return Err(StandardizedPsarOscillatorError::OutputLengthMismatch {
1708                expected,
1709                got: out.len(),
1710            });
1711        }
1712    }
1713
1714    for params in &combos {
1715        let standardization_length = params
1716            .standardization_length
1717            .unwrap_or(DEFAULT_STANDARDIZATION_LENGTH);
1718        let wma_length = params.wma_length.unwrap_or(DEFAULT_WMA_LENGTH);
1719        let needed = required_valid_bars(standardization_length, wma_length);
1720        if max_run < needed {
1721            return Err(StandardizedPsarOscillatorError::NotEnoughValidData {
1722                needed,
1723                valid: max_run,
1724            });
1725        }
1726        validate_params(
1727            params.start.unwrap_or(DEFAULT_START),
1728            params.increment.unwrap_or(DEFAULT_INCREMENT),
1729            params.maximum.unwrap_or(DEFAULT_MAXIMUM),
1730            standardization_length,
1731            wma_length,
1732            params.wma_lag.unwrap_or(DEFAULT_WMA_LAG),
1733            params.pivot_left.unwrap_or(DEFAULT_PIVOT_LEFT),
1734            params.pivot_right.unwrap_or(DEFAULT_PIVOT_RIGHT),
1735            cols,
1736        )?;
1737    }
1738
1739    let do_row = |row: usize,
1740                  oscillator_row: &mut [f64],
1741                  ma_row: &mut [f64],
1742                  bullish_reversal_row: &mut [f64],
1743                  bearish_reversal_row: &mut [f64],
1744                  regular_bullish_row: &mut [f64],
1745                  regular_bearish_row: &mut [f64],
1746                  bullish_weakening_row: &mut [f64],
1747                  bearish_weakening_row: &mut [f64]| {
1748        compute_row(
1749            high,
1750            low,
1751            close,
1752            &combos[row],
1753            oscillator_row,
1754            ma_row,
1755            bullish_reversal_row,
1756            bearish_reversal_row,
1757            regular_bullish_row,
1758            regular_bearish_row,
1759            bullish_weakening_row,
1760            bearish_weakening_row,
1761        )
1762    };
1763
1764    if parallel {
1765        #[cfg(not(target_arch = "wasm32"))]
1766        {
1767            oscillator_out
1768                .par_chunks_mut(cols)
1769                .zip(ma_out.par_chunks_mut(cols))
1770                .zip(bullish_reversal_out.par_chunks_mut(cols))
1771                .zip(bearish_reversal_out.par_chunks_mut(cols))
1772                .zip(regular_bullish_out.par_chunks_mut(cols))
1773                .zip(regular_bearish_out.par_chunks_mut(cols))
1774                .zip(bullish_weakening_out.par_chunks_mut(cols))
1775                .zip(bearish_weakening_out.par_chunks_mut(cols))
1776                .enumerate()
1777                .try_for_each(
1778                    |(
1779                        row,
1780                        (
1781                            (
1782                                (
1783                                    (
1784                                        (
1785                                            ((oscillator_row, ma_row), bullish_reversal_row),
1786                                            bearish_reversal_row,
1787                                        ),
1788                                        regular_bullish_row,
1789                                    ),
1790                                    regular_bearish_row,
1791                                ),
1792                                bullish_weakening_row,
1793                            ),
1794                            bearish_weakening_row,
1795                        ),
1796                    )| {
1797                        do_row(
1798                            row,
1799                            oscillator_row,
1800                            ma_row,
1801                            bullish_reversal_row,
1802                            bearish_reversal_row,
1803                            regular_bullish_row,
1804                            regular_bearish_row,
1805                            bullish_weakening_row,
1806                            bearish_weakening_row,
1807                        )
1808                    },
1809                )?;
1810        }
1811        #[cfg(target_arch = "wasm32")]
1812        {
1813            for row in 0..rows {
1814                let start = row * cols;
1815                let end = start + cols;
1816                do_row(
1817                    row,
1818                    &mut oscillator_out[start..end],
1819                    &mut ma_out[start..end],
1820                    &mut bullish_reversal_out[start..end],
1821                    &mut bearish_reversal_out[start..end],
1822                    &mut regular_bullish_out[start..end],
1823                    &mut regular_bearish_out[start..end],
1824                    &mut bullish_weakening_out[start..end],
1825                    &mut bearish_weakening_out[start..end],
1826                )?;
1827            }
1828        }
1829    } else {
1830        for row in 0..rows {
1831            let start = row * cols;
1832            let end = start + cols;
1833            do_row(
1834                row,
1835                &mut oscillator_out[start..end],
1836                &mut ma_out[start..end],
1837                &mut bullish_reversal_out[start..end],
1838                &mut bearish_reversal_out[start..end],
1839                &mut regular_bullish_out[start..end],
1840                &mut regular_bearish_out[start..end],
1841                &mut bullish_weakening_out[start..end],
1842                &mut bearish_weakening_out[start..end],
1843            )?;
1844        }
1845    }
1846
1847    Ok(combos)
1848}
1849
1850pub fn standardized_psar_oscillator_batch_with_kernel(
1851    high: &[f64],
1852    low: &[f64],
1853    close: &[f64],
1854    sweep: &StandardizedPsarOscillatorBatchRange,
1855    kernel: Kernel,
1856) -> Result<StandardizedPsarOscillatorBatchOutput, StandardizedPsarOscillatorError> {
1857    match kernel {
1858        Kernel::Auto => {
1859            let _ = detect_best_batch_kernel();
1860        }
1861        k if !k.is_batch() => {
1862            return Err(StandardizedPsarOscillatorError::InvalidKernelForBatch(k));
1863        }
1864        _ => {}
1865    }
1866    standardized_psar_oscillator_batch_par_slice(high, low, close, sweep, Kernel::ScalarBatch)
1867}
1868
1869pub fn standardized_psar_oscillator_batch_slice(
1870    high: &[f64],
1871    low: &[f64],
1872    close: &[f64],
1873    sweep: &StandardizedPsarOscillatorBatchRange,
1874    _kernel: Kernel,
1875) -> Result<StandardizedPsarOscillatorBatchOutput, StandardizedPsarOscillatorError> {
1876    standardized_psar_oscillator_batch_impl(high, low, close, sweep, false)
1877}
1878
1879pub fn standardized_psar_oscillator_batch_par_slice(
1880    high: &[f64],
1881    low: &[f64],
1882    close: &[f64],
1883    sweep: &StandardizedPsarOscillatorBatchRange,
1884    _kernel: Kernel,
1885) -> Result<StandardizedPsarOscillatorBatchOutput, StandardizedPsarOscillatorError> {
1886    standardized_psar_oscillator_batch_impl(high, low, close, sweep, true)
1887}
1888
1889fn standardized_psar_oscillator_batch_impl(
1890    high: &[f64],
1891    low: &[f64],
1892    close: &[f64],
1893    sweep: &StandardizedPsarOscillatorBatchRange,
1894    parallel: bool,
1895) -> Result<StandardizedPsarOscillatorBatchOutput, StandardizedPsarOscillatorError> {
1896    let rows = expand_grid_standardized_psar_oscillator(sweep)?.len();
1897    let cols = close.len();
1898
1899    let oscillator_mu = make_uninit_matrix(rows, cols);
1900    let ma_mu = make_uninit_matrix(rows, cols);
1901    let bullish_reversal_mu = make_uninit_matrix(rows, cols);
1902    let bearish_reversal_mu = make_uninit_matrix(rows, cols);
1903    let regular_bullish_mu = make_uninit_matrix(rows, cols);
1904    let regular_bearish_mu = make_uninit_matrix(rows, cols);
1905    let bullish_weakening_mu = make_uninit_matrix(rows, cols);
1906    let bearish_weakening_mu = make_uninit_matrix(rows, cols);
1907
1908    let mut oscillator_guard = ManuallyDrop::new(oscillator_mu);
1909    let mut ma_guard = ManuallyDrop::new(ma_mu);
1910    let mut bullish_reversal_guard = ManuallyDrop::new(bullish_reversal_mu);
1911    let mut bearish_reversal_guard = ManuallyDrop::new(bearish_reversal_mu);
1912    let mut regular_bullish_guard = ManuallyDrop::new(regular_bullish_mu);
1913    let mut regular_bearish_guard = ManuallyDrop::new(regular_bearish_mu);
1914    let mut bullish_weakening_guard = ManuallyDrop::new(bullish_weakening_mu);
1915    let mut bearish_weakening_guard = ManuallyDrop::new(bearish_weakening_mu);
1916
1917    let oscillator_out: &mut [f64] = unsafe {
1918        core::slice::from_raw_parts_mut(
1919            oscillator_guard.as_mut_ptr() as *mut f64,
1920            oscillator_guard.len(),
1921        )
1922    };
1923    let ma_out: &mut [f64] = unsafe {
1924        core::slice::from_raw_parts_mut(ma_guard.as_mut_ptr() as *mut f64, ma_guard.len())
1925    };
1926    let bullish_reversal_out: &mut [f64] = unsafe {
1927        core::slice::from_raw_parts_mut(
1928            bullish_reversal_guard.as_mut_ptr() as *mut f64,
1929            bullish_reversal_guard.len(),
1930        )
1931    };
1932    let bearish_reversal_out: &mut [f64] = unsafe {
1933        core::slice::from_raw_parts_mut(
1934            bearish_reversal_guard.as_mut_ptr() as *mut f64,
1935            bearish_reversal_guard.len(),
1936        )
1937    };
1938    let regular_bullish_out: &mut [f64] = unsafe {
1939        core::slice::from_raw_parts_mut(
1940            regular_bullish_guard.as_mut_ptr() as *mut f64,
1941            regular_bullish_guard.len(),
1942        )
1943    };
1944    let regular_bearish_out: &mut [f64] = unsafe {
1945        core::slice::from_raw_parts_mut(
1946            regular_bearish_guard.as_mut_ptr() as *mut f64,
1947            regular_bearish_guard.len(),
1948        )
1949    };
1950    let bullish_weakening_out: &mut [f64] = unsafe {
1951        core::slice::from_raw_parts_mut(
1952            bullish_weakening_guard.as_mut_ptr() as *mut f64,
1953            bullish_weakening_guard.len(),
1954        )
1955    };
1956    let bearish_weakening_out: &mut [f64] = unsafe {
1957        core::slice::from_raw_parts_mut(
1958            bearish_weakening_guard.as_mut_ptr() as *mut f64,
1959            bearish_weakening_guard.len(),
1960        )
1961    };
1962
1963    let combos = standardized_psar_oscillator_batch_inner_into(
1964        high,
1965        low,
1966        close,
1967        sweep,
1968        parallel,
1969        oscillator_out,
1970        ma_out,
1971        bullish_reversal_out,
1972        bearish_reversal_out,
1973        regular_bullish_out,
1974        regular_bearish_out,
1975        bullish_weakening_out,
1976        bearish_weakening_out,
1977    )?;
1978
1979    let oscillator = unsafe {
1980        Vec::from_raw_parts(
1981            oscillator_guard.as_mut_ptr() as *mut f64,
1982            oscillator_guard.len(),
1983            oscillator_guard.capacity(),
1984        )
1985    };
1986    let ma = unsafe {
1987        Vec::from_raw_parts(
1988            ma_guard.as_mut_ptr() as *mut f64,
1989            ma_guard.len(),
1990            ma_guard.capacity(),
1991        )
1992    };
1993    let bullish_reversal = unsafe {
1994        Vec::from_raw_parts(
1995            bullish_reversal_guard.as_mut_ptr() as *mut f64,
1996            bullish_reversal_guard.len(),
1997            bullish_reversal_guard.capacity(),
1998        )
1999    };
2000    let bearish_reversal = unsafe {
2001        Vec::from_raw_parts(
2002            bearish_reversal_guard.as_mut_ptr() as *mut f64,
2003            bearish_reversal_guard.len(),
2004            bearish_reversal_guard.capacity(),
2005        )
2006    };
2007    let regular_bullish = unsafe {
2008        Vec::from_raw_parts(
2009            regular_bullish_guard.as_mut_ptr() as *mut f64,
2010            regular_bullish_guard.len(),
2011            regular_bullish_guard.capacity(),
2012        )
2013    };
2014    let regular_bearish = unsafe {
2015        Vec::from_raw_parts(
2016            regular_bearish_guard.as_mut_ptr() as *mut f64,
2017            regular_bearish_guard.len(),
2018            regular_bearish_guard.capacity(),
2019        )
2020    };
2021    let bullish_weakening = unsafe {
2022        Vec::from_raw_parts(
2023            bullish_weakening_guard.as_mut_ptr() as *mut f64,
2024            bullish_weakening_guard.len(),
2025            bullish_weakening_guard.capacity(),
2026        )
2027    };
2028    let bearish_weakening = unsafe {
2029        Vec::from_raw_parts(
2030            bearish_weakening_guard.as_mut_ptr() as *mut f64,
2031            bearish_weakening_guard.len(),
2032            bearish_weakening_guard.capacity(),
2033        )
2034    };
2035
2036    Ok(StandardizedPsarOscillatorBatchOutput {
2037        oscillator,
2038        ma,
2039        bullish_reversal,
2040        bearish_reversal,
2041        regular_bullish,
2042        regular_bearish,
2043        bullish_weakening,
2044        bearish_weakening,
2045        combos,
2046        rows,
2047        cols,
2048    })
2049}
2050
2051#[cfg(feature = "python")]
2052#[pyfunction(name = "standardized_psar_oscillator")]
2053#[pyo3(signature = (high, low, close, start=DEFAULT_START, increment=DEFAULT_INCREMENT, maximum=DEFAULT_MAXIMUM, standardization_length=DEFAULT_STANDARDIZATION_LENGTH, wma_length=DEFAULT_WMA_LENGTH, wma_lag=DEFAULT_WMA_LAG, pivot_left=DEFAULT_PIVOT_LEFT, pivot_right=DEFAULT_PIVOT_RIGHT, plot_bullish=DEFAULT_PLOT_BULLISH, plot_bearish=DEFAULT_PLOT_BEARISH, kernel=None))]
2054pub fn standardized_psar_oscillator_py<'py>(
2055    py: Python<'py>,
2056    high: PyReadonlyArray1<'py, f64>,
2057    low: PyReadonlyArray1<'py, f64>,
2058    close: PyReadonlyArray1<'py, f64>,
2059    start: f64,
2060    increment: f64,
2061    maximum: f64,
2062    standardization_length: usize,
2063    wma_length: usize,
2064    wma_lag: usize,
2065    pivot_left: usize,
2066    pivot_right: usize,
2067    plot_bullish: bool,
2068    plot_bearish: bool,
2069    kernel: Option<&str>,
2070) -> PyResult<Bound<'py, PyDict>> {
2071    let high_slice = high.as_slice()?;
2072    let low_slice = low.as_slice()?;
2073    let close_slice = close.as_slice()?;
2074    let kernel = validate_kernel(kernel, false)?;
2075    let input = StandardizedPsarOscillatorInput::from_slices(
2076        high_slice,
2077        low_slice,
2078        close_slice,
2079        StandardizedPsarOscillatorParams {
2080            start: Some(start),
2081            increment: Some(increment),
2082            maximum: Some(maximum),
2083            standardization_length: Some(standardization_length),
2084            wma_length: Some(wma_length),
2085            wma_lag: Some(wma_lag),
2086            pivot_left: Some(pivot_left),
2087            pivot_right: Some(pivot_right),
2088            plot_bullish: Some(plot_bullish),
2089            plot_bearish: Some(plot_bearish),
2090        },
2091    );
2092
2093    let out = py
2094        .allow_threads(|| standardized_psar_oscillator_with_kernel(&input, kernel))
2095        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2096
2097    let dict = PyDict::new(py);
2098    dict.set_item("oscillator", out.oscillator.into_pyarray(py))?;
2099    dict.set_item("ma", out.ma.into_pyarray(py))?;
2100    dict.set_item("bullish_reversal", out.bullish_reversal.into_pyarray(py))?;
2101    dict.set_item("bearish_reversal", out.bearish_reversal.into_pyarray(py))?;
2102    dict.set_item("regular_bullish", out.regular_bullish.into_pyarray(py))?;
2103    dict.set_item("regular_bearish", out.regular_bearish.into_pyarray(py))?;
2104    dict.set_item("bullish_weakening", out.bullish_weakening.into_pyarray(py))?;
2105    dict.set_item("bearish_weakening", out.bearish_weakening.into_pyarray(py))?;
2106    Ok(dict)
2107}
2108
2109#[cfg(feature = "python")]
2110#[pyfunction(name = "standardized_psar_oscillator_batch")]
2111#[pyo3(signature = (high, low, close, start_range=(DEFAULT_START, DEFAULT_START, 0.0), increment_range=(DEFAULT_INCREMENT, DEFAULT_INCREMENT, 0.0), maximum_range=(DEFAULT_MAXIMUM, DEFAULT_MAXIMUM, 0.0), standardization_length_range=(DEFAULT_STANDARDIZATION_LENGTH, DEFAULT_STANDARDIZATION_LENGTH, 0), wma_length_range=(DEFAULT_WMA_LENGTH, DEFAULT_WMA_LENGTH, 0), wma_lag_range=(DEFAULT_WMA_LAG, DEFAULT_WMA_LAG, 0), pivot_left_range=(DEFAULT_PIVOT_LEFT, DEFAULT_PIVOT_LEFT, 0), pivot_right_range=(DEFAULT_PIVOT_RIGHT, DEFAULT_PIVOT_RIGHT, 0), plot_bullish=DEFAULT_PLOT_BULLISH, plot_bearish=DEFAULT_PLOT_BEARISH, kernel=None))]
2112pub fn standardized_psar_oscillator_batch_py<'py>(
2113    py: Python<'py>,
2114    high: PyReadonlyArray1<'py, f64>,
2115    low: PyReadonlyArray1<'py, f64>,
2116    close: PyReadonlyArray1<'py, f64>,
2117    start_range: (f64, f64, f64),
2118    increment_range: (f64, f64, f64),
2119    maximum_range: (f64, f64, f64),
2120    standardization_length_range: (usize, usize, usize),
2121    wma_length_range: (usize, usize, usize),
2122    wma_lag_range: (usize, usize, usize),
2123    pivot_left_range: (usize, usize, usize),
2124    pivot_right_range: (usize, usize, usize),
2125    plot_bullish: bool,
2126    plot_bearish: bool,
2127    kernel: Option<&str>,
2128) -> PyResult<Bound<'py, PyDict>> {
2129    let high_slice = high.as_slice()?;
2130    let low_slice = low.as_slice()?;
2131    let close_slice = close.as_slice()?;
2132    let kernel = validate_kernel(kernel, true)?;
2133    let sweep = StandardizedPsarOscillatorBatchRange {
2134        start: start_range,
2135        increment: increment_range,
2136        maximum: maximum_range,
2137        standardization_length: standardization_length_range,
2138        wma_length: wma_length_range,
2139        wma_lag: wma_lag_range,
2140        pivot_left: pivot_left_range,
2141        pivot_right: pivot_right_range,
2142        plot_bullish,
2143        plot_bearish,
2144    };
2145    let out = py
2146        .allow_threads(|| {
2147            standardized_psar_oscillator_batch_with_kernel(
2148                high_slice,
2149                low_slice,
2150                close_slice,
2151                &sweep,
2152                kernel,
2153            )
2154        })
2155        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2156
2157    let rows = out.rows;
2158    let cols = out.cols;
2159    let dict = PyDict::new(py);
2160    let oscillator_arr = out.oscillator.into_pyarray(py);
2161    let ma_arr = out.ma.into_pyarray(py);
2162    let bullish_reversal_arr = out.bullish_reversal.into_pyarray(py);
2163    let bearish_reversal_arr = out.bearish_reversal.into_pyarray(py);
2164    let regular_bullish_arr = out.regular_bullish.into_pyarray(py);
2165    let regular_bearish_arr = out.regular_bearish.into_pyarray(py);
2166    let bullish_weakening_arr = out.bullish_weakening.into_pyarray(py);
2167    let bearish_weakening_arr = out.bearish_weakening.into_pyarray(py);
2168    let combos = out.combos;
2169    dict.set_item("oscillator", oscillator_arr.reshape((rows, cols))?)?;
2170    dict.set_item("ma", ma_arr.reshape((rows, cols))?)?;
2171    dict.set_item(
2172        "bullish_reversal",
2173        bullish_reversal_arr.reshape((rows, cols))?,
2174    )?;
2175    dict.set_item(
2176        "bearish_reversal",
2177        bearish_reversal_arr.reshape((rows, cols))?,
2178    )?;
2179    dict.set_item(
2180        "regular_bullish",
2181        regular_bullish_arr.reshape((rows, cols))?,
2182    )?;
2183    dict.set_item(
2184        "regular_bearish",
2185        regular_bearish_arr.reshape((rows, cols))?,
2186    )?;
2187    dict.set_item(
2188        "bullish_weakening",
2189        bullish_weakening_arr.reshape((rows, cols))?,
2190    )?;
2191    dict.set_item(
2192        "bearish_weakening",
2193        bearish_weakening_arr.reshape((rows, cols))?,
2194    )?;
2195    dict.set_item(
2196        "starts",
2197        combos
2198            .iter()
2199            .map(|p| p.start.unwrap_or(DEFAULT_START))
2200            .collect::<Vec<_>>()
2201            .into_pyarray(py),
2202    )?;
2203    dict.set_item(
2204        "increments",
2205        combos
2206            .iter()
2207            .map(|p| p.increment.unwrap_or(DEFAULT_INCREMENT))
2208            .collect::<Vec<_>>()
2209            .into_pyarray(py),
2210    )?;
2211    dict.set_item(
2212        "maximums",
2213        combos
2214            .iter()
2215            .map(|p| p.maximum.unwrap_or(DEFAULT_MAXIMUM))
2216            .collect::<Vec<_>>()
2217            .into_pyarray(py),
2218    )?;
2219    dict.set_item(
2220        "standardization_lengths",
2221        combos
2222            .iter()
2223            .map(|p| {
2224                p.standardization_length
2225                    .unwrap_or(DEFAULT_STANDARDIZATION_LENGTH) as u64
2226            })
2227            .collect::<Vec<_>>()
2228            .into_pyarray(py),
2229    )?;
2230    dict.set_item(
2231        "wma_lengths",
2232        combos
2233            .iter()
2234            .map(|p| p.wma_length.unwrap_or(DEFAULT_WMA_LENGTH) as u64)
2235            .collect::<Vec<_>>()
2236            .into_pyarray(py),
2237    )?;
2238    dict.set_item(
2239        "wma_lags",
2240        combos
2241            .iter()
2242            .map(|p| p.wma_lag.unwrap_or(DEFAULT_WMA_LAG) as u64)
2243            .collect::<Vec<_>>()
2244            .into_pyarray(py),
2245    )?;
2246    dict.set_item(
2247        "pivot_lefts",
2248        combos
2249            .iter()
2250            .map(|p| p.pivot_left.unwrap_or(DEFAULT_PIVOT_LEFT) as u64)
2251            .collect::<Vec<_>>()
2252            .into_pyarray(py),
2253    )?;
2254    dict.set_item(
2255        "pivot_rights",
2256        combos
2257            .iter()
2258            .map(|p| p.pivot_right.unwrap_or(DEFAULT_PIVOT_RIGHT) as u64)
2259            .collect::<Vec<_>>()
2260            .into_pyarray(py),
2261    )?;
2262    dict.set_item(
2263        "plot_bullish",
2264        combos
2265            .iter()
2266            .map(|p| p.plot_bullish.unwrap_or(DEFAULT_PLOT_BULLISH))
2267            .collect::<Vec<_>>()
2268            .into_pyarray(py),
2269    )?;
2270    dict.set_item(
2271        "plot_bearish",
2272        combos
2273            .iter()
2274            .map(|p| p.plot_bearish.unwrap_or(DEFAULT_PLOT_BEARISH))
2275            .collect::<Vec<_>>()
2276            .into_pyarray(py),
2277    )?;
2278    dict.set_item("rows", rows)?;
2279    dict.set_item("cols", cols)?;
2280    Ok(dict)
2281}
2282
2283#[cfg(feature = "python")]
2284#[pyclass(name = "StandardizedPsarOscillatorStream")]
2285pub struct StandardizedPsarOscillatorStreamPy {
2286    inner: StandardizedPsarOscillatorStream,
2287}
2288
2289#[cfg(feature = "python")]
2290#[pymethods]
2291impl StandardizedPsarOscillatorStreamPy {
2292    #[new]
2293    #[pyo3(signature = (start=DEFAULT_START, increment=DEFAULT_INCREMENT, maximum=DEFAULT_MAXIMUM, standardization_length=DEFAULT_STANDARDIZATION_LENGTH, wma_length=DEFAULT_WMA_LENGTH, wma_lag=DEFAULT_WMA_LAG, pivot_left=DEFAULT_PIVOT_LEFT, pivot_right=DEFAULT_PIVOT_RIGHT, plot_bullish=DEFAULT_PLOT_BULLISH, plot_bearish=DEFAULT_PLOT_BEARISH))]
2294    pub fn new(
2295        start: f64,
2296        increment: f64,
2297        maximum: f64,
2298        standardization_length: usize,
2299        wma_length: usize,
2300        wma_lag: usize,
2301        pivot_left: usize,
2302        pivot_right: usize,
2303        plot_bullish: bool,
2304        plot_bearish: bool,
2305    ) -> PyResult<Self> {
2306        let inner = StandardizedPsarOscillatorStream::try_new(StandardizedPsarOscillatorParams {
2307            start: Some(start),
2308            increment: Some(increment),
2309            maximum: Some(maximum),
2310            standardization_length: Some(standardization_length),
2311            wma_length: Some(wma_length),
2312            wma_lag: Some(wma_lag),
2313            pivot_left: Some(pivot_left),
2314            pivot_right: Some(pivot_right),
2315            plot_bullish: Some(plot_bullish),
2316            plot_bearish: Some(plot_bearish),
2317        })
2318        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2319        Ok(Self { inner })
2320    }
2321
2322    pub fn update(
2323        &mut self,
2324        high: f64,
2325        low: f64,
2326        close: f64,
2327    ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64)> {
2328        self.inner.update(high, low, close)
2329    }
2330}
2331
2332#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2333#[derive(Serialize, Deserialize)]
2334pub struct StandardizedPsarOscillatorBatchConfig {
2335    pub start_range: (f64, f64, f64),
2336    pub increment_range: (f64, f64, f64),
2337    pub maximum_range: (f64, f64, f64),
2338    pub standardization_length_range: (usize, usize, usize),
2339    pub wma_length_range: (usize, usize, usize),
2340    pub wma_lag_range: (usize, usize, usize),
2341    pub pivot_left_range: (usize, usize, usize),
2342    pub pivot_right_range: (usize, usize, usize),
2343    pub plot_bullish: bool,
2344    pub plot_bearish: bool,
2345}
2346
2347#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2348#[derive(Serialize, Deserialize)]
2349pub struct StandardizedPsarOscillatorBatchJsOutput {
2350    pub oscillator: Vec<f64>,
2351    pub ma: Vec<f64>,
2352    pub bullish_reversal: Vec<f64>,
2353    pub bearish_reversal: Vec<f64>,
2354    pub regular_bullish: Vec<f64>,
2355    pub regular_bearish: Vec<f64>,
2356    pub bullish_weakening: Vec<f64>,
2357    pub bearish_weakening: Vec<f64>,
2358    pub combos: Vec<StandardizedPsarOscillatorParams>,
2359    pub rows: usize,
2360    pub cols: usize,
2361}
2362
2363#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2364#[wasm_bindgen]
2365pub fn standardized_psar_oscillator_js(
2366    high: &[f64],
2367    low: &[f64],
2368    close: &[f64],
2369    start: f64,
2370    increment: f64,
2371    maximum: f64,
2372    standardization_length: usize,
2373    wma_length: usize,
2374    wma_lag: usize,
2375    pivot_left: usize,
2376    pivot_right: usize,
2377    plot_bullish: bool,
2378    plot_bearish: bool,
2379) -> Result<JsValue, JsValue> {
2380    let input = StandardizedPsarOscillatorInput::from_slices(
2381        high,
2382        low,
2383        close,
2384        StandardizedPsarOscillatorParams {
2385            start: Some(start),
2386            increment: Some(increment),
2387            maximum: Some(maximum),
2388            standardization_length: Some(standardization_length),
2389            wma_length: Some(wma_length),
2390            wma_lag: Some(wma_lag),
2391            pivot_left: Some(pivot_left),
2392            pivot_right: Some(pivot_right),
2393            plot_bullish: Some(plot_bullish),
2394            plot_bearish: Some(plot_bearish),
2395        },
2396    );
2397    let output = standardized_psar_oscillator_with_kernel(&input, Kernel::Auto)
2398        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2399    serde_wasm_bindgen::to_value(&output)
2400        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2401}
2402
2403#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2404#[wasm_bindgen]
2405pub fn standardized_psar_oscillator_alloc(len: usize) -> *mut f64 {
2406    let mut vec = Vec::<f64>::with_capacity(len);
2407    let ptr = vec.as_mut_ptr();
2408    std::mem::forget(vec);
2409    ptr
2410}
2411
2412#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2413#[wasm_bindgen]
2414pub fn standardized_psar_oscillator_free(ptr: *mut f64, len: usize) {
2415    if !ptr.is_null() {
2416        unsafe {
2417            let _ = Vec::from_raw_parts(ptr, len, len);
2418        }
2419    }
2420}
2421
2422#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2423#[wasm_bindgen]
2424pub fn standardized_psar_oscillator_into(
2425    high_ptr: *const f64,
2426    low_ptr: *const f64,
2427    close_ptr: *const f64,
2428    oscillator_ptr: *mut f64,
2429    ma_ptr: *mut f64,
2430    bullish_reversal_ptr: *mut f64,
2431    bearish_reversal_ptr: *mut f64,
2432    regular_bullish_ptr: *mut f64,
2433    regular_bearish_ptr: *mut f64,
2434    bullish_weakening_ptr: *mut f64,
2435    bearish_weakening_ptr: *mut f64,
2436    len: usize,
2437    start: f64,
2438    increment: f64,
2439    maximum: f64,
2440    standardization_length: usize,
2441    wma_length: usize,
2442    wma_lag: usize,
2443    pivot_left: usize,
2444    pivot_right: usize,
2445    plot_bullish: bool,
2446    plot_bearish: bool,
2447) -> Result<(), JsValue> {
2448    if high_ptr.is_null()
2449        || low_ptr.is_null()
2450        || close_ptr.is_null()
2451        || oscillator_ptr.is_null()
2452        || ma_ptr.is_null()
2453        || bullish_reversal_ptr.is_null()
2454        || bearish_reversal_ptr.is_null()
2455        || regular_bullish_ptr.is_null()
2456        || regular_bearish_ptr.is_null()
2457        || bullish_weakening_ptr.is_null()
2458        || bearish_weakening_ptr.is_null()
2459    {
2460        return Err(JsValue::from_str("Null pointer provided"));
2461    }
2462
2463    unsafe {
2464        let high = std::slice::from_raw_parts(high_ptr, len);
2465        let low = std::slice::from_raw_parts(low_ptr, len);
2466        let close = std::slice::from_raw_parts(close_ptr, len);
2467        let input = StandardizedPsarOscillatorInput::from_slices(
2468            high,
2469            low,
2470            close,
2471            StandardizedPsarOscillatorParams {
2472                start: Some(start),
2473                increment: Some(increment),
2474                maximum: Some(maximum),
2475                standardization_length: Some(standardization_length),
2476                wma_length: Some(wma_length),
2477                wma_lag: Some(wma_lag),
2478                pivot_left: Some(pivot_left),
2479                pivot_right: Some(pivot_right),
2480                plot_bullish: Some(plot_bullish),
2481                plot_bearish: Some(plot_bearish),
2482            },
2483        );
2484        let output = standardized_psar_oscillator_with_kernel(&input, Kernel::Auto)
2485            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2486        std::slice::from_raw_parts_mut(oscillator_ptr, len).copy_from_slice(&output.oscillator);
2487        std::slice::from_raw_parts_mut(ma_ptr, len).copy_from_slice(&output.ma);
2488        std::slice::from_raw_parts_mut(bullish_reversal_ptr, len)
2489            .copy_from_slice(&output.bullish_reversal);
2490        std::slice::from_raw_parts_mut(bearish_reversal_ptr, len)
2491            .copy_from_slice(&output.bearish_reversal);
2492        std::slice::from_raw_parts_mut(regular_bullish_ptr, len)
2493            .copy_from_slice(&output.regular_bullish);
2494        std::slice::from_raw_parts_mut(regular_bearish_ptr, len)
2495            .copy_from_slice(&output.regular_bearish);
2496        std::slice::from_raw_parts_mut(bullish_weakening_ptr, len)
2497            .copy_from_slice(&output.bullish_weakening);
2498        std::slice::from_raw_parts_mut(bearish_weakening_ptr, len)
2499            .copy_from_slice(&output.bearish_weakening);
2500    }
2501    Ok(())
2502}
2503
2504#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2505#[wasm_bindgen(js_name = standardized_psar_oscillator_batch)]
2506pub fn standardized_psar_oscillator_batch_unified_js(
2507    high: &[f64],
2508    low: &[f64],
2509    close: &[f64],
2510    config: JsValue,
2511) -> Result<JsValue, JsValue> {
2512    let config: StandardizedPsarOscillatorBatchConfig = serde_wasm_bindgen::from_value(config)
2513        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2514    let sweep = StandardizedPsarOscillatorBatchRange {
2515        start: config.start_range,
2516        increment: config.increment_range,
2517        maximum: config.maximum_range,
2518        standardization_length: config.standardization_length_range,
2519        wma_length: config.wma_length_range,
2520        wma_lag: config.wma_lag_range,
2521        pivot_left: config.pivot_left_range,
2522        pivot_right: config.pivot_right_range,
2523        plot_bullish: config.plot_bullish,
2524        plot_bearish: config.plot_bearish,
2525    };
2526    let output =
2527        standardized_psar_oscillator_batch_with_kernel(high, low, close, &sweep, Kernel::Auto)
2528            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2529    serde_wasm_bindgen::to_value(&StandardizedPsarOscillatorBatchJsOutput {
2530        oscillator: output.oscillator,
2531        ma: output.ma,
2532        bullish_reversal: output.bullish_reversal,
2533        bearish_reversal: output.bearish_reversal,
2534        regular_bullish: output.regular_bullish,
2535        regular_bearish: output.regular_bearish,
2536        bullish_weakening: output.bullish_weakening,
2537        bearish_weakening: output.bearish_weakening,
2538        combos: output.combos,
2539        rows: output.rows,
2540        cols: output.cols,
2541    })
2542    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2543}
2544
2545#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2546#[wasm_bindgen]
2547pub fn standardized_psar_oscillator_batch_into(
2548    high_ptr: *const f64,
2549    low_ptr: *const f64,
2550    close_ptr: *const f64,
2551    oscillator_ptr: *mut f64,
2552    ma_ptr: *mut f64,
2553    bullish_reversal_ptr: *mut f64,
2554    bearish_reversal_ptr: *mut f64,
2555    regular_bullish_ptr: *mut f64,
2556    regular_bearish_ptr: *mut f64,
2557    bullish_weakening_ptr: *mut f64,
2558    bearish_weakening_ptr: *mut f64,
2559    len: usize,
2560    start_start: f64,
2561    start_end: f64,
2562    start_step: f64,
2563    increment_start: f64,
2564    increment_end: f64,
2565    increment_step: f64,
2566    maximum_start: f64,
2567    maximum_end: f64,
2568    maximum_step: f64,
2569    standardization_length_start: usize,
2570    standardization_length_end: usize,
2571    standardization_length_step: usize,
2572    wma_length_start: usize,
2573    wma_length_end: usize,
2574    wma_length_step: usize,
2575    wma_lag_start: usize,
2576    wma_lag_end: usize,
2577    wma_lag_step: usize,
2578    pivot_left_start: usize,
2579    pivot_left_end: usize,
2580    pivot_left_step: usize,
2581    pivot_right_start: usize,
2582    pivot_right_end: usize,
2583    pivot_right_step: usize,
2584    plot_bullish: bool,
2585    plot_bearish: bool,
2586) -> Result<usize, JsValue> {
2587    if high_ptr.is_null()
2588        || low_ptr.is_null()
2589        || close_ptr.is_null()
2590        || oscillator_ptr.is_null()
2591        || ma_ptr.is_null()
2592        || bullish_reversal_ptr.is_null()
2593        || bearish_reversal_ptr.is_null()
2594        || regular_bullish_ptr.is_null()
2595        || regular_bearish_ptr.is_null()
2596        || bullish_weakening_ptr.is_null()
2597        || bearish_weakening_ptr.is_null()
2598    {
2599        return Err(JsValue::from_str("Null pointer provided"));
2600    }
2601
2602    let sweep = StandardizedPsarOscillatorBatchRange {
2603        start: (start_start, start_end, start_step),
2604        increment: (increment_start, increment_end, increment_step),
2605        maximum: (maximum_start, maximum_end, maximum_step),
2606        standardization_length: (
2607            standardization_length_start,
2608            standardization_length_end,
2609            standardization_length_step,
2610        ),
2611        wma_length: (wma_length_start, wma_length_end, wma_length_step),
2612        wma_lag: (wma_lag_start, wma_lag_end, wma_lag_step),
2613        pivot_left: (pivot_left_start, pivot_left_end, pivot_left_step),
2614        pivot_right: (pivot_right_start, pivot_right_end, pivot_right_step),
2615        plot_bullish,
2616        plot_bearish,
2617    };
2618    let rows = expand_grid_standardized_psar_oscillator(&sweep)
2619        .map_err(|e| JsValue::from_str(&e.to_string()))?
2620        .len();
2621    let total = rows
2622        .checked_mul(len)
2623        .ok_or_else(|| JsValue::from_str("rows*len overflow"))?;
2624
2625    unsafe {
2626        standardized_psar_oscillator_batch_inner_into(
2627            std::slice::from_raw_parts(high_ptr, len),
2628            std::slice::from_raw_parts(low_ptr, len),
2629            std::slice::from_raw_parts(close_ptr, len),
2630            &sweep,
2631            false,
2632            std::slice::from_raw_parts_mut(oscillator_ptr, total),
2633            std::slice::from_raw_parts_mut(ma_ptr, total),
2634            std::slice::from_raw_parts_mut(bullish_reversal_ptr, total),
2635            std::slice::from_raw_parts_mut(bearish_reversal_ptr, total),
2636            std::slice::from_raw_parts_mut(regular_bullish_ptr, total),
2637            std::slice::from_raw_parts_mut(regular_bearish_ptr, total),
2638            std::slice::from_raw_parts_mut(bullish_weakening_ptr, total),
2639            std::slice::from_raw_parts_mut(bearish_weakening_ptr, total),
2640        )
2641        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2642    }
2643    Ok(rows)
2644}
2645
2646#[cfg(test)]
2647mod tests {
2648    use super::*;
2649
2650    fn mixed_data(size: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
2651        let mut high = vec![0.0; size];
2652        let mut low = vec![0.0; size];
2653        let mut close = vec![0.0; size];
2654        for i in 0..size {
2655            let x = i as f64;
2656            let c = 100.0 + 0.18 * x + (x * 0.23).sin() * 5.5 + (x * 0.07).cos() * 2.0;
2657            close[i] = c;
2658            high[i] = c + 1.1 + (i % 3) as f64 * 0.05;
2659            low[i] = c - 1.0 - (i % 2) as f64 * 0.05;
2660        }
2661        (high, low, close)
2662    }
2663
2664    fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
2665        assert_eq!(actual.len(), expected.len());
2666        for (idx, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
2667            if a.is_nan() || e.is_nan() {
2668                assert!(a.is_nan() && e.is_nan(), "NaN mismatch at {}", idx);
2669            } else {
2670                assert!((a - e).abs() <= tol, "mismatch at {}", idx);
2671            }
2672        }
2673    }
2674
2675    #[test]
2676    fn standardized_psar_oscillator_stream_matches_batch() -> Result<(), Box<dyn StdError>> {
2677        let (high, low, close) = mixed_data(220);
2678        let params = StandardizedPsarOscillatorParams {
2679            start: Some(0.03),
2680            increment: Some(0.001),
2681            maximum: Some(0.25),
2682            standardization_length: Some(14),
2683            wma_length: Some(12),
2684            wma_lag: Some(2),
2685            pivot_left: Some(8),
2686            pivot_right: Some(1),
2687            plot_bullish: Some(true),
2688            plot_bearish: Some(true),
2689        };
2690        let batch = standardized_psar_oscillator(&StandardizedPsarOscillatorInput::from_slices(
2691            &high,
2692            &low,
2693            &close,
2694            params.clone(),
2695        ))?;
2696        let mut stream = StandardizedPsarOscillatorStream::try_new(params)?;
2697        let mut oscillator = vec![f64::NAN; close.len()];
2698        let mut ma = vec![f64::NAN; close.len()];
2699        for i in 0..close.len() {
2700            if let Some((o, m, _, _, _, _, _, _)) = stream.update(high[i], low[i], close[i]) {
2701                oscillator[i] = o;
2702                ma[i] = m;
2703            }
2704        }
2705        assert_close(&oscillator, &batch.oscillator, 1e-12);
2706        assert_close(&ma, &batch.ma, 1e-12);
2707        Ok(())
2708    }
2709
2710    #[test]
2711    fn standardized_psar_oscillator_batch_matches_single() -> Result<(), Box<dyn StdError>> {
2712        let (high, low, close) = mixed_data(180);
2713        let sweep = StandardizedPsarOscillatorBatchRange {
2714            start: (0.02, 0.03, 0.01),
2715            increment: (0.0005, 0.0005, 0.0),
2716            maximum: (0.2, 0.2, 0.0),
2717            standardization_length: (10, 11, 1),
2718            wma_length: (8, 8, 0),
2719            wma_lag: (2, 2, 0),
2720            pivot_left: (6, 6, 0),
2721            pivot_right: (1, 1, 0),
2722            plot_bullish: true,
2723            plot_bearish: true,
2724        };
2725        let batch = standardized_psar_oscillator_batch_with_kernel(
2726            &high,
2727            &low,
2728            &close,
2729            &sweep,
2730            Kernel::Auto,
2731        )?;
2732        assert_eq!(batch.rows, 4);
2733        assert_eq!(batch.cols, close.len());
2734        Ok(())
2735    }
2736}