Skip to main content

vector_ta/indicators/
pretty_good_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::indicators::atr::{AtrParams, AtrStream};
16use crate::indicators::moving_averages::sma::{SmaParams, SmaStream};
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25
26#[cfg(not(target_arch = "wasm32"))]
27use rayon::prelude::*;
28use std::convert::AsRef;
29use std::mem::{ManuallyDrop, MaybeUninit};
30use thiserror::Error;
31
32impl<'a> AsRef<[f64]> for PrettyGoodOscillatorInput<'a> {
33    #[inline(always)]
34    fn as_ref(&self) -> &[f64] {
35        match &self.data {
36            PrettyGoodOscillatorData::Candles { candles, source } => source_type(candles, source),
37            PrettyGoodOscillatorData::Slices { source, .. } => source,
38        }
39    }
40}
41
42#[derive(Debug, Clone)]
43pub enum PrettyGoodOscillatorData<'a> {
44    Candles {
45        candles: &'a Candles,
46        source: &'a str,
47    },
48    Slices {
49        high: &'a [f64],
50        low: &'a [f64],
51        close: &'a [f64],
52        source: &'a [f64],
53    },
54}
55
56#[derive(Debug, Clone)]
57pub struct PrettyGoodOscillatorOutput {
58    pub values: Vec<f64>,
59}
60
61#[derive(Debug, Clone)]
62#[cfg_attr(
63    all(target_arch = "wasm32", feature = "wasm"),
64    derive(Serialize, Deserialize)
65)]
66pub struct PrettyGoodOscillatorParams {
67    pub length: Option<usize>,
68}
69
70impl Default for PrettyGoodOscillatorParams {
71    fn default() -> Self {
72        Self { length: Some(14) }
73    }
74}
75
76#[derive(Debug, Clone)]
77pub struct PrettyGoodOscillatorInput<'a> {
78    pub data: PrettyGoodOscillatorData<'a>,
79    pub params: PrettyGoodOscillatorParams,
80}
81
82impl<'a> PrettyGoodOscillatorInput<'a> {
83    #[inline]
84    pub fn from_candles(
85        candles: &'a Candles,
86        source: &'a str,
87        params: PrettyGoodOscillatorParams,
88    ) -> Self {
89        Self {
90            data: PrettyGoodOscillatorData::Candles { candles, source },
91            params,
92        }
93    }
94
95    #[inline]
96    pub fn from_slices(
97        high: &'a [f64],
98        low: &'a [f64],
99        close: &'a [f64],
100        source: &'a [f64],
101        params: PrettyGoodOscillatorParams,
102    ) -> Self {
103        Self {
104            data: PrettyGoodOscillatorData::Slices {
105                high,
106                low,
107                close,
108                source,
109            },
110            params,
111        }
112    }
113
114    #[inline]
115    pub fn with_default_candles(candles: &'a Candles) -> Self {
116        Self::from_candles(candles, "close", PrettyGoodOscillatorParams::default())
117    }
118
119    #[inline]
120    pub fn get_length(&self) -> usize {
121        self.params.length.unwrap_or(14)
122    }
123
124    #[inline]
125    pub fn as_refs(&'a self) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64]) {
126        match &self.data {
127            PrettyGoodOscillatorData::Candles { candles, source } => (
128                candles.high.as_slice(),
129                candles.low.as_slice(),
130                candles.close.as_slice(),
131                source_type(candles, source),
132            ),
133            PrettyGoodOscillatorData::Slices {
134                high,
135                low,
136                close,
137                source,
138            } => (*high, *low, *close, *source),
139        }
140    }
141}
142
143#[derive(Copy, Clone, Debug)]
144pub struct PrettyGoodOscillatorBuilder {
145    length: Option<usize>,
146    kernel: Kernel,
147}
148
149impl Default for PrettyGoodOscillatorBuilder {
150    fn default() -> Self {
151        Self {
152            length: None,
153            kernel: Kernel::Auto,
154        }
155    }
156}
157
158impl PrettyGoodOscillatorBuilder {
159    #[inline(always)]
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    #[inline(always)]
165    pub fn length(mut self, value: usize) -> Self {
166        self.length = Some(value);
167        self
168    }
169
170    #[inline(always)]
171    pub fn kernel(mut self, value: Kernel) -> Self {
172        self.kernel = value;
173        self
174    }
175
176    #[inline(always)]
177    pub fn apply(
178        self,
179        candles: &Candles,
180    ) -> Result<PrettyGoodOscillatorOutput, PrettyGoodOscillatorError> {
181        let input = PrettyGoodOscillatorInput::from_candles(
182            candles,
183            "close",
184            PrettyGoodOscillatorParams {
185                length: self.length,
186            },
187        );
188        pretty_good_oscillator_with_kernel(&input, self.kernel)
189    }
190
191    #[inline(always)]
192    pub fn apply_slices(
193        self,
194        high: &[f64],
195        low: &[f64],
196        close: &[f64],
197        source: &[f64],
198    ) -> Result<PrettyGoodOscillatorOutput, PrettyGoodOscillatorError> {
199        let input = PrettyGoodOscillatorInput::from_slices(
200            high,
201            low,
202            close,
203            source,
204            PrettyGoodOscillatorParams {
205                length: self.length,
206            },
207        );
208        pretty_good_oscillator_with_kernel(&input, self.kernel)
209    }
210
211    #[inline(always)]
212    pub fn into_stream(self) -> Result<PrettyGoodOscillatorStream, PrettyGoodOscillatorError> {
213        PrettyGoodOscillatorStream::try_new(PrettyGoodOscillatorParams {
214            length: self.length,
215        })
216    }
217}
218
219#[derive(Debug, Error)]
220pub enum PrettyGoodOscillatorError {
221    #[error("pretty_good_oscillator: Empty input data.")]
222    EmptyInputData,
223    #[error("pretty_good_oscillator: Data length mismatch across high, low, close, and source.")]
224    DataLengthMismatch,
225    #[error("pretty_good_oscillator: All OHLC/source values are invalid.")]
226    AllValuesNaN,
227    #[error("pretty_good_oscillator: Invalid length: length = {length}, data length = {data_len}")]
228    InvalidLength { length: usize, data_len: usize },
229    #[error("pretty_good_oscillator: Not enough valid data: needed = {needed}, valid = {valid}")]
230    NotEnoughValidData { needed: usize, valid: usize },
231    #[error("pretty_good_oscillator: Output length mismatch: expected = {expected}, got = {got}")]
232    OutputLengthMismatch { expected: usize, got: usize },
233    #[error("pretty_good_oscillator: Invalid range: start={start}, end={end}, step={step}")]
234    InvalidRange {
235        start: usize,
236        end: usize,
237        step: usize,
238    },
239    #[error("pretty_good_oscillator: Invalid kernel for batch: {0:?}")]
240    InvalidKernelForBatch(Kernel),
241}
242
243#[inline(always)]
244fn is_valid_bar(high: f64, low: f64, close: f64, source: f64) -> bool {
245    high.is_finite() && low.is_finite() && close.is_finite() && source.is_finite() && high >= low
246}
247
248#[inline(always)]
249fn first_valid_bar(high: &[f64], low: &[f64], close: &[f64], source: &[f64]) -> Option<usize> {
250    (0..source.len()).find(|&i| is_valid_bar(high[i], low[i], close[i], source[i]))
251}
252
253#[inline(always)]
254fn true_range(high: &[f64], low: &[f64], close: &[f64], first: usize, i: usize) -> f64 {
255    if i == first {
256        high[i] - low[i]
257    } else {
258        let prev_close = close[i - 1];
259        let up = if high[i] > prev_close {
260            high[i]
261        } else {
262            prev_close
263        };
264        let dn = if low[i] < prev_close {
265            low[i]
266        } else {
267            prev_close
268        };
269        up - dn
270    }
271}
272
273#[inline(always)]
274fn is_fast_path_clean(
275    high: &[f64],
276    low: &[f64],
277    close: &[f64],
278    source: &[f64],
279    first: usize,
280) -> bool {
281    for i in first..source.len() {
282        if !is_valid_bar(high[i], low[i], close[i], source[i]) {
283            return false;
284        }
285    }
286    true
287}
288
289#[inline(always)]
290fn pgo_compute_fast(
291    high: &[f64],
292    low: &[f64],
293    close: &[f64],
294    source: &[f64],
295    length: usize,
296    first: usize,
297    out: &mut [f64],
298) {
299    let warmup = first + length - 1;
300    let alpha = 1.0 / length as f64;
301    let mut sum_source = 0.0;
302    let mut sum_tr = 0.0;
303
304    for i in first..=warmup {
305        sum_source += source[i];
306        sum_tr += true_range(high, low, close, first, i);
307    }
308
309    let inv = 1.0 / length as f64;
310    let mut sma = sum_source * inv;
311    let mut atr = sum_tr * inv;
312    out[warmup] = if atr != 0.0 {
313        (source[warmup] - sma) / atr
314    } else {
315        f64::NAN
316    };
317
318    for i in (warmup + 1)..source.len() {
319        sum_source += source[i] - source[i - length];
320        sma = sum_source * inv;
321        let tr = true_range(high, low, close, first, i);
322        atr = alpha.mul_add(tr - atr, atr);
323        out[i] = if atr != 0.0 {
324            (source[i] - sma) / atr
325        } else {
326            f64::NAN
327        };
328    }
329}
330
331#[inline]
332fn pgo_prepare<'a>(
333    input: &'a PrettyGoodOscillatorInput<'a>,
334    kernel: Kernel,
335) -> Result<
336    (
337        &'a [f64],
338        &'a [f64],
339        &'a [f64],
340        &'a [f64],
341        usize,
342        usize,
343        Kernel,
344    ),
345    PrettyGoodOscillatorError,
346> {
347    let (high, low, close, source) = input.as_refs();
348    if high.is_empty() {
349        return Err(PrettyGoodOscillatorError::EmptyInputData);
350    }
351    if high.len() != low.len() || low.len() != close.len() || close.len() != source.len() {
352        return Err(PrettyGoodOscillatorError::DataLengthMismatch);
353    }
354    let length = input.get_length();
355    if length == 0 || length > source.len() {
356        return Err(PrettyGoodOscillatorError::InvalidLength {
357            length,
358            data_len: source.len(),
359        });
360    }
361    let first =
362        first_valid_bar(high, low, close, source).ok_or(PrettyGoodOscillatorError::AllValuesNaN)?;
363    let valid = source.len().saturating_sub(first);
364    if valid < length {
365        return Err(PrettyGoodOscillatorError::NotEnoughValidData {
366            needed: length,
367            valid,
368        });
369    }
370    let chosen = match kernel {
371        Kernel::Auto => detect_best_kernel(),
372        other => other.to_non_batch(),
373    };
374    Ok((high, low, close, source, length, first, chosen))
375}
376
377#[inline]
378pub fn pretty_good_oscillator(
379    input: &PrettyGoodOscillatorInput,
380) -> Result<PrettyGoodOscillatorOutput, PrettyGoodOscillatorError> {
381    pretty_good_oscillator_with_kernel(input, Kernel::Auto)
382}
383
384pub fn pretty_good_oscillator_with_kernel(
385    input: &PrettyGoodOscillatorInput,
386    kernel: Kernel,
387) -> Result<PrettyGoodOscillatorOutput, PrettyGoodOscillatorError> {
388    let (_, _, _, source, length, first, chosen) = pgo_prepare(input, kernel)?;
389    let mut out = alloc_with_nan_prefix(source.len(), first + length - 1);
390    pretty_good_oscillator_into_slice(&mut out, input, chosen)?;
391    Ok(PrettyGoodOscillatorOutput { values: out })
392}
393
394#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
395pub fn pretty_good_oscillator_into(
396    input: &PrettyGoodOscillatorInput,
397    out: &mut [f64],
398) -> Result<(), PrettyGoodOscillatorError> {
399    pretty_good_oscillator_into_slice(out, input, Kernel::Auto)
400}
401
402pub fn pretty_good_oscillator_into_slice(
403    dst: &mut [f64],
404    input: &PrettyGoodOscillatorInput,
405    kern: Kernel,
406) -> Result<(), PrettyGoodOscillatorError> {
407    let (high, low, close, source, length, first, _chosen) = pgo_prepare(input, kern)?;
408    if dst.len() != source.len() {
409        return Err(PrettyGoodOscillatorError::OutputLengthMismatch {
410            expected: source.len(),
411            got: dst.len(),
412        });
413    }
414
415    let warmup = first + length - 1;
416    let prefix = warmup.min(dst.len());
417    for v in &mut dst[..prefix] {
418        *v = f64::NAN;
419    }
420
421    if is_fast_path_clean(high, low, close, source, first) {
422        pgo_compute_fast(high, low, close, source, length, first, dst);
423        return Ok(());
424    }
425
426    let mut sma_stream = SmaStream::try_new(SmaParams {
427        period: Some(length),
428    })
429    .map_err(|_| PrettyGoodOscillatorError::InvalidLength {
430        length,
431        data_len: source.len(),
432    })?;
433    let mut atr_stream = AtrStream::try_new(AtrParams {
434        length: Some(length),
435    })
436    .map_err(|_| PrettyGoodOscillatorError::InvalidLength {
437        length,
438        data_len: source.len(),
439    })?;
440
441    for i in 0..source.len() {
442        if !is_valid_bar(high[i], low[i], close[i], source[i]) {
443            dst[i] = f64::NAN;
444            continue;
445        }
446        let sma = sma_stream.update(source[i]);
447        let atr = atr_stream.update(high[i], low[i], close[i]);
448        dst[i] = match (sma, atr) {
449            (Some(sma), Some(atr)) if atr != 0.0 => (source[i] - sma) / atr,
450            _ => f64::NAN,
451        };
452    }
453
454    Ok(())
455}
456
457#[derive(Debug, Clone)]
458pub struct PrettyGoodOscillatorStream {
459    sma: SmaStream,
460    atr: AtrStream,
461}
462
463impl PrettyGoodOscillatorStream {
464    #[inline(always)]
465    pub fn try_new(params: PrettyGoodOscillatorParams) -> Result<Self, PrettyGoodOscillatorError> {
466        let length = params.length.unwrap_or(14);
467        if length == 0 {
468            return Err(PrettyGoodOscillatorError::InvalidLength {
469                length,
470                data_len: 0,
471            });
472        }
473        let sma = SmaStream::try_new(SmaParams {
474            period: Some(length),
475        })
476        .map_err(|_| PrettyGoodOscillatorError::InvalidLength {
477            length,
478            data_len: 0,
479        })?;
480        let atr = AtrStream::try_new(AtrParams {
481            length: Some(length),
482        })
483        .map_err(|_| PrettyGoodOscillatorError::InvalidLength {
484            length,
485            data_len: 0,
486        })?;
487        Ok(Self { sma, atr })
488    }
489
490    #[inline(always)]
491    pub fn update(&mut self, high: f64, low: f64, close: f64, source: f64) -> Option<f64> {
492        if !is_valid_bar(high, low, close, source) {
493            return None;
494        }
495        let sma = self.sma.update(source);
496        let atr = self.atr.update(high, low, close);
497        match (sma, atr) {
498            (Some(sma), Some(atr)) if atr != 0.0 => Some((source - sma) / atr),
499            _ => None,
500        }
501    }
502}
503
504#[derive(Clone, Debug)]
505pub struct PrettyGoodOscillatorBatchRange {
506    pub length: (usize, usize, usize),
507}
508
509impl Default for PrettyGoodOscillatorBatchRange {
510    fn default() -> Self {
511        Self {
512            length: (14, 14, 0),
513        }
514    }
515}
516
517#[derive(Clone, Debug, Default)]
518pub struct PrettyGoodOscillatorBatchBuilder {
519    range: PrettyGoodOscillatorBatchRange,
520    kernel: Kernel,
521}
522
523impl PrettyGoodOscillatorBatchBuilder {
524    #[inline(always)]
525    pub fn new() -> Self {
526        Self::default()
527    }
528
529    #[inline(always)]
530    pub fn kernel(mut self, kernel: Kernel) -> Self {
531        self.kernel = kernel;
532        self
533    }
534
535    #[inline(always)]
536    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
537        self.range.length = (start, end, step);
538        self
539    }
540
541    #[inline(always)]
542    pub fn length_static(mut self, length: usize) -> Self {
543        self.range.length = (length, length, 0);
544        self
545    }
546
547    #[inline(always)]
548    pub fn apply_candles(
549        self,
550        candles: &Candles,
551        source: &str,
552    ) -> Result<PrettyGoodOscillatorBatchOutput, PrettyGoodOscillatorError> {
553        pretty_good_oscillator_batch_with_kernel(
554            candles.high.as_slice(),
555            candles.low.as_slice(),
556            candles.close.as_slice(),
557            source_type(candles, source),
558            &self.range,
559            self.kernel,
560        )
561    }
562
563    #[inline(always)]
564    pub fn apply_slices(
565        self,
566        high: &[f64],
567        low: &[f64],
568        close: &[f64],
569        source: &[f64],
570    ) -> Result<PrettyGoodOscillatorBatchOutput, PrettyGoodOscillatorError> {
571        pretty_good_oscillator_batch_with_kernel(high, low, close, source, &self.range, self.kernel)
572    }
573}
574
575#[derive(Debug, Clone)]
576pub struct PrettyGoodOscillatorBatchOutput {
577    pub values: Vec<f64>,
578    pub rows: usize,
579    pub cols: usize,
580    pub combos: Vec<PrettyGoodOscillatorParams>,
581}
582
583impl PrettyGoodOscillatorBatchOutput {
584    #[inline(always)]
585    pub fn row_for_params(&self, params: &PrettyGoodOscillatorParams) -> Option<usize> {
586        self.combos.iter().position(|p| p.length == params.length)
587    }
588
589    #[inline(always)]
590    pub fn values_for(&self, params: &PrettyGoodOscillatorParams) -> Option<&[f64]> {
591        let row = self.row_for_params(params)?;
592        let start = row * self.cols;
593        Some(&self.values[start..start + self.cols])
594    }
595}
596
597#[inline]
598pub fn expand_grid_pretty_good_oscillator(
599    sweep: &PrettyGoodOscillatorBatchRange,
600) -> Result<Vec<PrettyGoodOscillatorParams>, PrettyGoodOscillatorError> {
601    let (start, end, step) = sweep.length;
602    if start == 0 || end == 0 || start > end || (start != end && step == 0) {
603        return Err(PrettyGoodOscillatorError::InvalidRange { start, end, step });
604    }
605    let mut combos = Vec::new();
606    let mut value = start;
607    loop {
608        combos.push(PrettyGoodOscillatorParams {
609            length: Some(value),
610        });
611        if value == end {
612            break;
613        }
614        value = value.saturating_add(step);
615        if value > end {
616            break;
617        }
618    }
619    Ok(combos)
620}
621
622pub fn pretty_good_oscillator_batch_with_kernel(
623    high: &[f64],
624    low: &[f64],
625    close: &[f64],
626    source: &[f64],
627    sweep: &PrettyGoodOscillatorBatchRange,
628    kernel: Kernel,
629) -> Result<PrettyGoodOscillatorBatchOutput, PrettyGoodOscillatorError> {
630    let batch_kernel = match kernel {
631        Kernel::Auto => detect_best_batch_kernel(),
632        other if other.is_batch() => other,
633        other => return Err(PrettyGoodOscillatorError::InvalidKernelForBatch(other)),
634    };
635    pretty_good_oscillator_batch_impl(high, low, close, source, sweep, batch_kernel, true)
636}
637
638pub fn pretty_good_oscillator_batch_slice(
639    high: &[f64],
640    low: &[f64],
641    close: &[f64],
642    source: &[f64],
643    sweep: &PrettyGoodOscillatorBatchRange,
644) -> Result<PrettyGoodOscillatorBatchOutput, PrettyGoodOscillatorError> {
645    pretty_good_oscillator_batch_impl(high, low, close, source, sweep, Kernel::ScalarBatch, false)
646}
647
648pub fn pretty_good_oscillator_batch_par_slice(
649    high: &[f64],
650    low: &[f64],
651    close: &[f64],
652    source: &[f64],
653    sweep: &PrettyGoodOscillatorBatchRange,
654) -> Result<PrettyGoodOscillatorBatchOutput, PrettyGoodOscillatorError> {
655    pretty_good_oscillator_batch_impl(high, low, close, source, sweep, Kernel::ScalarBatch, true)
656}
657
658fn pretty_good_oscillator_batch_impl(
659    high: &[f64],
660    low: &[f64],
661    close: &[f64],
662    source: &[f64],
663    sweep: &PrettyGoodOscillatorBatchRange,
664    kernel: Kernel,
665    parallel: bool,
666) -> Result<PrettyGoodOscillatorBatchOutput, PrettyGoodOscillatorError> {
667    if high.is_empty() {
668        return Err(PrettyGoodOscillatorError::EmptyInputData);
669    }
670    if high.len() != low.len() || low.len() != close.len() || close.len() != source.len() {
671        return Err(PrettyGoodOscillatorError::DataLengthMismatch);
672    }
673    let combos = expand_grid_pretty_good_oscillator(sweep)?;
674    let rows = combos.len();
675    let cols = source.len();
676    let mut out_mu = make_uninit_matrix(rows, cols);
677    let warmups: Vec<usize> = combos
678        .iter()
679        .map(|p| p.length.unwrap_or(14).saturating_sub(1))
680        .collect();
681    init_matrix_prefixes(&mut out_mu, cols, &warmups);
682    let mut guard = ManuallyDrop::new(out_mu);
683    let out =
684        unsafe { std::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
685    pretty_good_oscillator_batch_inner_into(
686        high,
687        low,
688        close,
689        source,
690        sweep,
691        kernel.to_non_batch(),
692        parallel,
693        out,
694    )?;
695    let values = unsafe {
696        Vec::from_raw_parts(
697            guard.as_mut_ptr() as *mut f64,
698            guard.len(),
699            guard.capacity(),
700        )
701    };
702    Ok(PrettyGoodOscillatorBatchOutput {
703        values,
704        rows,
705        cols,
706        combos,
707    })
708}
709
710fn pretty_good_oscillator_batch_inner_into(
711    high: &[f64],
712    low: &[f64],
713    close: &[f64],
714    source: &[f64],
715    sweep: &PrettyGoodOscillatorBatchRange,
716    kernel: Kernel,
717    parallel: bool,
718    out: &mut [f64],
719) -> Result<Vec<PrettyGoodOscillatorParams>, PrettyGoodOscillatorError> {
720    if high.is_empty() {
721        return Err(PrettyGoodOscillatorError::EmptyInputData);
722    }
723    if high.len() != low.len() || low.len() != close.len() || close.len() != source.len() {
724        return Err(PrettyGoodOscillatorError::DataLengthMismatch);
725    }
726    let combos = expand_grid_pretty_good_oscillator(sweep)?;
727    let rows = combos.len();
728    let cols = source.len();
729    let expected = rows
730        .checked_mul(cols)
731        .ok_or(PrettyGoodOscillatorError::InvalidRange {
732            start: sweep.length.0,
733            end: sweep.length.1,
734            step: sweep.length.2,
735        })?;
736    if out.len() != expected {
737        return Err(PrettyGoodOscillatorError::OutputLengthMismatch {
738            expected,
739            got: out.len(),
740        });
741    }
742
743    unsafe {
744        let out_mu =
745            std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, expected);
746        let warmups: Vec<usize> = combos
747            .iter()
748            .map(|p| p.length.unwrap_or(14).saturating_sub(1))
749            .collect();
750        init_matrix_prefixes(out_mu, cols, &warmups);
751    }
752
753    let do_row = |row: usize, dst: &mut [f64]| {
754        let input =
755            PrettyGoodOscillatorInput::from_slices(high, low, close, source, combos[row].clone());
756        let _ = pretty_good_oscillator_into_slice(dst, &input, kernel);
757    };
758
759    if parallel {
760        #[cfg(not(target_arch = "wasm32"))]
761        {
762            out.par_chunks_mut(cols)
763                .enumerate()
764                .for_each(|(row, dst)| do_row(row, dst));
765        }
766        #[cfg(target_arch = "wasm32")]
767        {
768            for (row, dst) in out.chunks_mut(cols).enumerate() {
769                do_row(row, dst);
770            }
771        }
772    } else {
773        for (row, dst) in out.chunks_mut(cols).enumerate() {
774            do_row(row, dst);
775        }
776    }
777
778    Ok(combos)
779}
780
781#[cfg(feature = "python")]
782#[pyfunction(name = "pretty_good_oscillator")]
783#[pyo3(signature = (high, low, close, source, length=14, kernel=None))]
784pub fn pretty_good_oscillator_py<'py>(
785    py: Python<'py>,
786    high: PyReadonlyArray1<'py, f64>,
787    low: PyReadonlyArray1<'py, f64>,
788    close: PyReadonlyArray1<'py, f64>,
789    source: PyReadonlyArray1<'py, f64>,
790    length: usize,
791    kernel: Option<&str>,
792) -> PyResult<Bound<'py, PyArray1<f64>>> {
793    let high = high.as_slice()?;
794    let low = low.as_slice()?;
795    let close = close.as_slice()?;
796    let source = source.as_slice()?;
797    let kernel = validate_kernel(kernel, false)?;
798    let input = PrettyGoodOscillatorInput::from_slices(
799        high,
800        low,
801        close,
802        source,
803        PrettyGoodOscillatorParams {
804            length: Some(length),
805        },
806    );
807    let output = py
808        .allow_threads(|| pretty_good_oscillator_with_kernel(&input, kernel))
809        .map_err(|e| PyValueError::new_err(e.to_string()))?;
810    Ok(output.values.into_pyarray(py))
811}
812
813#[cfg(feature = "python")]
814#[pyclass(name = "PrettyGoodOscillatorStream")]
815pub struct PrettyGoodOscillatorStreamPy {
816    stream: PrettyGoodOscillatorStream,
817}
818
819#[cfg(feature = "python")]
820#[pymethods]
821impl PrettyGoodOscillatorStreamPy {
822    #[new]
823    #[pyo3(signature = (length=14))]
824    fn new(length: usize) -> PyResult<Self> {
825        let stream = PrettyGoodOscillatorStream::try_new(PrettyGoodOscillatorParams {
826            length: Some(length),
827        })
828        .map_err(|e| PyValueError::new_err(e.to_string()))?;
829        Ok(Self { stream })
830    }
831
832    fn update(&mut self, high: f64, low: f64, close: f64, source: f64) -> Option<f64> {
833        self.stream.update(high, low, close, source)
834    }
835}
836
837#[cfg(feature = "python")]
838#[pyfunction(name = "pretty_good_oscillator_batch")]
839#[pyo3(signature = (high, low, close, source, length_range, kernel=None))]
840pub fn pretty_good_oscillator_batch_py<'py>(
841    py: Python<'py>,
842    high: PyReadonlyArray1<'py, f64>,
843    low: PyReadonlyArray1<'py, f64>,
844    close: PyReadonlyArray1<'py, f64>,
845    source: PyReadonlyArray1<'py, f64>,
846    length_range: (usize, usize, usize),
847    kernel: Option<&str>,
848) -> PyResult<Bound<'py, PyDict>> {
849    let high = high.as_slice()?;
850    let low = low.as_slice()?;
851    let close = close.as_slice()?;
852    let source = source.as_slice()?;
853    let sweep = PrettyGoodOscillatorBatchRange {
854        length: length_range,
855    };
856    let combos = expand_grid_pretty_good_oscillator(&sweep)
857        .map_err(|e| PyValueError::new_err(e.to_string()))?;
858    let rows = combos.len();
859    let cols = source.len();
860    let total = rows
861        .checked_mul(cols)
862        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
863    let arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
864    let out = unsafe { arr.as_slice_mut()? };
865    let kernel = validate_kernel(kernel, true)?;
866
867    py.allow_threads(|| {
868        let batch_kernel = match kernel {
869            Kernel::Auto => detect_best_batch_kernel(),
870            other => other,
871        };
872        pretty_good_oscillator_batch_inner_into(
873            high,
874            low,
875            close,
876            source,
877            &sweep,
878            batch_kernel.to_non_batch(),
879            true,
880            out,
881        )
882    })
883    .map_err(|e| PyValueError::new_err(e.to_string()))?;
884
885    let dict = PyDict::new(py);
886    dict.set_item("values", arr.reshape((rows, cols))?)?;
887    dict.set_item(
888        "lengths",
889        combos
890            .iter()
891            .map(|p| p.length.unwrap_or(14) as u64)
892            .collect::<Vec<_>>()
893            .into_pyarray(py),
894    )?;
895    dict.set_item("rows", rows)?;
896    dict.set_item("cols", cols)?;
897    Ok(dict)
898}
899
900#[cfg(feature = "python")]
901pub fn register_pretty_good_oscillator_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
902    m.add_function(wrap_pyfunction!(pretty_good_oscillator_py, m)?)?;
903    m.add_function(wrap_pyfunction!(pretty_good_oscillator_batch_py, m)?)?;
904    m.add_class::<PrettyGoodOscillatorStreamPy>()?;
905    Ok(())
906}
907
908#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
909#[derive(Debug, Clone, Serialize, Deserialize)]
910struct PrettyGoodOscillatorBatchConfig {
911    length_range: Vec<usize>,
912}
913
914#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
915#[derive(Debug, Clone, Serialize, Deserialize)]
916struct PrettyGoodOscillatorBatchJsOutput {
917    values: Vec<f64>,
918    rows: usize,
919    cols: usize,
920    combos: Vec<PrettyGoodOscillatorParams>,
921}
922
923#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
924#[wasm_bindgen(js_name = "pretty_good_oscillator_js")]
925pub fn pretty_good_oscillator_js(
926    high: &[f64],
927    low: &[f64],
928    close: &[f64],
929    source: &[f64],
930    length: usize,
931) -> Result<Vec<f64>, JsValue> {
932    let input = PrettyGoodOscillatorInput::from_slices(
933        high,
934        low,
935        close,
936        source,
937        PrettyGoodOscillatorParams {
938            length: Some(length),
939        },
940    );
941    let mut out = vec![0.0; source.len()];
942    pretty_good_oscillator_into_slice(&mut out, &input, Kernel::Auto)
943        .map_err(|e| JsValue::from_str(&e.to_string()))?;
944    Ok(out)
945}
946
947#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
948#[wasm_bindgen(js_name = "pretty_good_oscillator_batch_js")]
949pub fn pretty_good_oscillator_batch_js(
950    high: &[f64],
951    low: &[f64],
952    close: &[f64],
953    source: &[f64],
954    config: JsValue,
955) -> Result<JsValue, JsValue> {
956    let config: PrettyGoodOscillatorBatchConfig = serde_wasm_bindgen::from_value(config)
957        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
958    if config.length_range.len() != 3 {
959        return Err(JsValue::from_str(
960            "Invalid config: length_range must have exactly 3 elements [start, end, step]",
961        ));
962    }
963    let sweep = PrettyGoodOscillatorBatchRange {
964        length: (
965            config.length_range[0],
966            config.length_range[1],
967            config.length_range[2],
968        ),
969    };
970    let batch = pretty_good_oscillator_batch_slice(high, low, close, source, &sweep)
971        .map_err(|e| JsValue::from_str(&e.to_string()))?;
972    serde_wasm_bindgen::to_value(&PrettyGoodOscillatorBatchJsOutput {
973        values: batch.values,
974        rows: batch.rows,
975        cols: batch.cols,
976        combos: batch.combos,
977    })
978    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
979}
980
981#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
982#[wasm_bindgen]
983pub fn pretty_good_oscillator_alloc(len: usize) -> *mut f64 {
984    let mut v = Vec::<f64>::with_capacity(len);
985    let ptr = v.as_mut_ptr();
986    std::mem::forget(v);
987    ptr
988}
989
990#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
991#[wasm_bindgen]
992pub fn pretty_good_oscillator_free(ptr: *mut f64, len: usize) {
993    unsafe {
994        let _ = Vec::from_raw_parts(ptr, len, len);
995    }
996}
997
998#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
999#[wasm_bindgen]
1000pub fn pretty_good_oscillator_into(
1001    high_ptr: *const f64,
1002    low_ptr: *const f64,
1003    close_ptr: *const f64,
1004    source_ptr: *const f64,
1005    out_ptr: *mut f64,
1006    len: usize,
1007    length: usize,
1008) -> Result<(), JsValue> {
1009    if high_ptr.is_null()
1010        || low_ptr.is_null()
1011        || close_ptr.is_null()
1012        || source_ptr.is_null()
1013        || out_ptr.is_null()
1014    {
1015        return Err(JsValue::from_str(
1016            "null pointer passed to pretty_good_oscillator_into",
1017        ));
1018    }
1019    unsafe {
1020        let high = std::slice::from_raw_parts(high_ptr, len);
1021        let low = std::slice::from_raw_parts(low_ptr, len);
1022        let close = std::slice::from_raw_parts(close_ptr, len);
1023        let source = std::slice::from_raw_parts(source_ptr, len);
1024        let out = std::slice::from_raw_parts_mut(out_ptr, len);
1025        let input = PrettyGoodOscillatorInput::from_slices(
1026            high,
1027            low,
1028            close,
1029            source,
1030            PrettyGoodOscillatorParams {
1031                length: Some(length),
1032            },
1033        );
1034        pretty_good_oscillator_into_slice(out, &input, Kernel::Auto)
1035            .map_err(|e| JsValue::from_str(&e.to_string()))
1036    }
1037}
1038
1039#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1040#[wasm_bindgen(js_name = "pretty_good_oscillator_into_host")]
1041pub fn pretty_good_oscillator_into_host(
1042    high: &[f64],
1043    low: &[f64],
1044    close: &[f64],
1045    source: &[f64],
1046    out_ptr: *mut f64,
1047    length: usize,
1048) -> Result<(), JsValue> {
1049    if out_ptr.is_null() {
1050        return Err(JsValue::from_str(
1051            "null pointer passed to pretty_good_oscillator_into_host",
1052        ));
1053    }
1054    unsafe {
1055        let out = std::slice::from_raw_parts_mut(out_ptr, source.len());
1056        let input = PrettyGoodOscillatorInput::from_slices(
1057            high,
1058            low,
1059            close,
1060            source,
1061            PrettyGoodOscillatorParams {
1062                length: Some(length),
1063            },
1064        );
1065        pretty_good_oscillator_into_slice(out, &input, Kernel::Auto)
1066            .map_err(|e| JsValue::from_str(&e.to_string()))
1067    }
1068}
1069
1070#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1071#[wasm_bindgen]
1072pub fn pretty_good_oscillator_batch_into(
1073    high_ptr: *const f64,
1074    low_ptr: *const f64,
1075    close_ptr: *const f64,
1076    source_ptr: *const f64,
1077    out_ptr: *mut f64,
1078    len: usize,
1079    length_start: usize,
1080    length_end: usize,
1081    length_step: usize,
1082) -> Result<usize, JsValue> {
1083    if high_ptr.is_null()
1084        || low_ptr.is_null()
1085        || close_ptr.is_null()
1086        || source_ptr.is_null()
1087        || out_ptr.is_null()
1088    {
1089        return Err(JsValue::from_str(
1090            "null pointer passed to pretty_good_oscillator_batch_into",
1091        ));
1092    }
1093    unsafe {
1094        let high = std::slice::from_raw_parts(high_ptr, len);
1095        let low = std::slice::from_raw_parts(low_ptr, len);
1096        let close = std::slice::from_raw_parts(close_ptr, len);
1097        let source = std::slice::from_raw_parts(source_ptr, len);
1098        let sweep = PrettyGoodOscillatorBatchRange {
1099            length: (length_start, length_end, length_step),
1100        };
1101        let combos = expand_grid_pretty_good_oscillator(&sweep)
1102            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1103        let rows = combos.len();
1104        let out = std::slice::from_raw_parts_mut(out_ptr, rows * len);
1105        pretty_good_oscillator_batch_inner_into(
1106            high,
1107            low,
1108            close,
1109            source,
1110            &sweep,
1111            Kernel::Scalar,
1112            false,
1113            out,
1114        )
1115        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1116        Ok(rows)
1117    }
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122    use super::*;
1123    use crate::indicators::dispatch::{
1124        compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
1125        ParamValue,
1126    };
1127
1128    fn sample_ohlcs(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1129        let mut high = Vec::with_capacity(len);
1130        let mut low = Vec::with_capacity(len);
1131        let mut close = Vec::with_capacity(len);
1132        let mut source = Vec::with_capacity(len);
1133        for i in 0..len {
1134            let base = 100.0 + (i as f64 * 0.17).sin() * 2.0 + i as f64 * 0.03;
1135            let c = base + (i as f64 * 0.11).cos() * 0.4;
1136            let h = c + 1.2 + (i as f64 * 0.07).sin().abs() * 0.3;
1137            let l = c - 1.1 - (i as f64 * 0.05).cos().abs() * 0.25;
1138            high.push(h);
1139            low.push(l);
1140            close.push(c);
1141            source.push((h + l) * 0.5);
1142        }
1143        (high, low, close, source)
1144    }
1145
1146    fn naive_pgo(
1147        high: &[f64],
1148        low: &[f64],
1149        close: &[f64],
1150        source: &[f64],
1151        length: usize,
1152    ) -> Vec<f64> {
1153        let len = source.len();
1154        let mut out = vec![f64::NAN; len];
1155        if len < length {
1156            return out;
1157        }
1158        let mut sum_source = 0.0;
1159        let mut sum_tr = 0.0;
1160        for i in 0..length {
1161            sum_source += source[i];
1162            let tr = if i == 0 {
1163                high[i] - low[i]
1164            } else {
1165                let up = high[i].max(close[i - 1]);
1166                let dn = low[i].min(close[i - 1]);
1167                up - dn
1168            };
1169            sum_tr += tr;
1170        }
1171        let mut atr = sum_tr / length as f64;
1172        out[length - 1] = (source[length - 1] - (sum_source / length as f64)) / atr;
1173        for i in length..len {
1174            sum_source += source[i] - source[i - length];
1175            let tr = {
1176                let up = high[i].max(close[i - 1]);
1177                let dn = low[i].min(close[i - 1]);
1178                up - dn
1179            };
1180            atr = atr + (tr - atr) / length as f64;
1181            out[i] = (source[i] - (sum_source / length as f64)) / atr;
1182        }
1183        out
1184    }
1185
1186    fn assert_close(a: &[f64], b: &[f64]) {
1187        assert_eq!(a.len(), b.len());
1188        for i in 0..a.len() {
1189            if a[i].is_nan() || b[i].is_nan() {
1190                assert!(
1191                    a[i].is_nan() && b[i].is_nan(),
1192                    "nan mismatch at {i}: {} vs {}",
1193                    a[i],
1194                    b[i]
1195                );
1196            } else {
1197                assert!(
1198                    (a[i] - b[i]).abs() <= 1e-10,
1199                    "mismatch at {i}: {} vs {}",
1200                    a[i],
1201                    b[i]
1202                );
1203            }
1204        }
1205    }
1206
1207    #[test]
1208    fn pretty_good_oscillator_matches_naive() {
1209        let (high, low, close, source) = sample_ohlcs(256);
1210        let input = PrettyGoodOscillatorInput::from_slices(
1211            &high,
1212            &low,
1213            &close,
1214            &source,
1215            PrettyGoodOscillatorParams { length: Some(14) },
1216        );
1217        let out = pretty_good_oscillator(&input).expect("indicator");
1218        let reference = naive_pgo(&high, &low, &close, &source, 14);
1219        assert_close(&out.values, &reference);
1220    }
1221
1222    #[test]
1223    fn pretty_good_oscillator_into_matches_api() {
1224        let (high, low, close, source) = sample_ohlcs(192);
1225        let input = PrettyGoodOscillatorInput::from_slices(
1226            &high,
1227            &low,
1228            &close,
1229            &source,
1230            PrettyGoodOscillatorParams { length: Some(10) },
1231        );
1232        let baseline = pretty_good_oscillator(&input).expect("baseline");
1233        let mut out = vec![0.0; source.len()];
1234        pretty_good_oscillator_into(&input, &mut out).expect("into");
1235        assert_close(&baseline.values, &out);
1236    }
1237
1238    #[test]
1239    fn pretty_good_oscillator_stream_matches_batch() {
1240        let (high, low, close, source) = sample_ohlcs(192);
1241        let input = PrettyGoodOscillatorInput::from_slices(
1242            &high,
1243            &low,
1244            &close,
1245            &source,
1246            PrettyGoodOscillatorParams { length: Some(14) },
1247        );
1248        let batch = pretty_good_oscillator(&input).expect("batch");
1249        let mut stream =
1250            PrettyGoodOscillatorStream::try_new(PrettyGoodOscillatorParams { length: Some(14) })
1251                .expect("stream");
1252        let mut values = Vec::with_capacity(source.len());
1253        for i in 0..source.len() {
1254            values.push(
1255                stream
1256                    .update(high[i], low[i], close[i], source[i])
1257                    .unwrap_or(f64::NAN),
1258            );
1259        }
1260        assert_close(&batch.values, &values);
1261    }
1262
1263    #[test]
1264    fn pretty_good_oscillator_batch_single_param_matches_single() {
1265        let (high, low, close, source) = sample_ohlcs(192);
1266        let sweep = PrettyGoodOscillatorBatchRange {
1267            length: (14, 14, 0),
1268        };
1269        let batch = pretty_good_oscillator_batch_with_kernel(
1270            &high,
1271            &low,
1272            &close,
1273            &source,
1274            &sweep,
1275            Kernel::ScalarBatch,
1276        )
1277        .expect("batch");
1278        let single = pretty_good_oscillator(&PrettyGoodOscillatorInput::from_slices(
1279            &high,
1280            &low,
1281            &close,
1282            &source,
1283            PrettyGoodOscillatorParams { length: Some(14) },
1284        ))
1285        .expect("single");
1286        assert_eq!(batch.rows, 1);
1287        assert_eq!(batch.cols, source.len());
1288        assert_close(&batch.values, &single.values);
1289    }
1290
1291    #[test]
1292    fn pretty_good_oscillator_rejects_invalid_length() {
1293        let (high, low, close, source) = sample_ohlcs(32);
1294        let err = pretty_good_oscillator(&PrettyGoodOscillatorInput::from_slices(
1295            &high,
1296            &low,
1297            &close,
1298            &source,
1299            PrettyGoodOscillatorParams { length: Some(0) },
1300        ))
1301        .expect_err("invalid length");
1302        assert!(matches!(
1303            err,
1304            PrettyGoodOscillatorError::InvalidLength { .. }
1305        ));
1306    }
1307
1308    #[test]
1309    fn pretty_good_oscillator_dispatch_matches_direct() {
1310        let (high, low, close, _source) = sample_ohlcs(192);
1311        let params = [ParamKV {
1312            key: "length",
1313            value: ParamValue::Int(14),
1314        }];
1315        let combos = [IndicatorParamSet { params: &params }];
1316        let out = compute_cpu_batch(IndicatorBatchRequest {
1317            indicator_id: "pretty_good_oscillator",
1318            output_id: Some("value"),
1319            data: IndicatorDataRef::Ohlc {
1320                open: &close,
1321                high: &high,
1322                low: &low,
1323                close: &close,
1324            },
1325            combos: &combos,
1326            kernel: Kernel::ScalarBatch,
1327        })
1328        .expect("dispatch");
1329        let direct = pretty_good_oscillator(&PrettyGoodOscillatorInput::from_slices(
1330            &high,
1331            &low,
1332            &close,
1333            &close,
1334            PrettyGoodOscillatorParams { length: Some(14) },
1335        ))
1336        .expect("direct");
1337        assert_eq!(out.rows, 1);
1338        assert_eq!(out.cols, close.len());
1339        assert_close(out.values_f64.as_ref().expect("values"), &direct.values);
1340    }
1341}