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: ¶ms }];
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}