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}