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::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::collections::VecDeque;
26use std::error::Error;
27use thiserror::Error;
28
29#[derive(Debug, Clone)]
30pub enum ProjectionOscillatorData<'a> {
31 Candles {
32 candles: &'a Candles,
33 source: &'a str,
34 },
35 Slices {
36 high: &'a [f64],
37 low: &'a [f64],
38 source: &'a [f64],
39 },
40}
41
42#[derive(Debug, Clone)]
43pub struct ProjectionOscillatorOutput {
44 pub pbo: Vec<f64>,
45 pub signal: Vec<f64>,
46}
47
48#[derive(Debug, Clone)]
49#[cfg_attr(
50 all(target_arch = "wasm32", feature = "wasm"),
51 derive(Serialize, Deserialize)
52)]
53pub struct ProjectionOscillatorParams {
54 pub length: Option<usize>,
55 pub smooth_length: Option<usize>,
56}
57
58impl Default for ProjectionOscillatorParams {
59 fn default() -> Self {
60 Self {
61 length: Some(14),
62 smooth_length: Some(4),
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
68pub struct ProjectionOscillatorInput<'a> {
69 pub data: ProjectionOscillatorData<'a>,
70 pub params: ProjectionOscillatorParams,
71}
72
73impl<'a> ProjectionOscillatorInput<'a> {
74 #[inline]
75 pub fn from_candles(
76 candles: &'a Candles,
77 source: &'a str,
78 params: ProjectionOscillatorParams,
79 ) -> Self {
80 Self {
81 data: ProjectionOscillatorData::Candles { candles, source },
82 params,
83 }
84 }
85
86 #[inline]
87 pub fn from_slices(
88 high: &'a [f64],
89 low: &'a [f64],
90 source: &'a [f64],
91 params: ProjectionOscillatorParams,
92 ) -> Self {
93 Self {
94 data: ProjectionOscillatorData::Slices { high, low, source },
95 params,
96 }
97 }
98
99 #[inline]
100 pub fn with_default_candles(candles: &'a Candles) -> Self {
101 Self::from_candles(candles, "close", ProjectionOscillatorParams::default())
102 }
103
104 #[inline]
105 pub fn get_length(&self) -> usize {
106 self.params.length.unwrap_or(14)
107 }
108
109 #[inline]
110 pub fn get_smooth_length(&self) -> usize {
111 self.params.smooth_length.unwrap_or(4)
112 }
113}
114
115#[derive(Copy, Clone, Debug)]
116pub struct ProjectionOscillatorBuilder {
117 length: Option<usize>,
118 smooth_length: Option<usize>,
119 kernel: Kernel,
120}
121
122impl Default for ProjectionOscillatorBuilder {
123 fn default() -> Self {
124 Self {
125 length: None,
126 smooth_length: None,
127 kernel: Kernel::Auto,
128 }
129 }
130}
131
132impl ProjectionOscillatorBuilder {
133 #[inline(always)]
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 #[inline(always)]
139 pub fn length(mut self, value: usize) -> Self {
140 self.length = Some(value);
141 self
142 }
143
144 #[inline(always)]
145 pub fn smooth_length(mut self, value: usize) -> Self {
146 self.smooth_length = Some(value);
147 self
148 }
149
150 #[inline(always)]
151 pub fn kernel(mut self, value: Kernel) -> Self {
152 self.kernel = value;
153 self
154 }
155
156 #[inline(always)]
157 pub fn apply(
158 self,
159 candles: &Candles,
160 ) -> Result<ProjectionOscillatorOutput, ProjectionOscillatorError> {
161 let params = ProjectionOscillatorParams {
162 length: self.length,
163 smooth_length: self.smooth_length,
164 };
165 projection_oscillator_with_kernel(
166 &ProjectionOscillatorInput::from_candles(candles, "close", params),
167 self.kernel,
168 )
169 }
170
171 #[inline(always)]
172 pub fn apply_slices(
173 self,
174 high: &[f64],
175 low: &[f64],
176 source: &[f64],
177 ) -> Result<ProjectionOscillatorOutput, ProjectionOscillatorError> {
178 let params = ProjectionOscillatorParams {
179 length: self.length,
180 smooth_length: self.smooth_length,
181 };
182 projection_oscillator_with_kernel(
183 &ProjectionOscillatorInput::from_slices(high, low, source, params),
184 self.kernel,
185 )
186 }
187
188 #[inline(always)]
189 pub fn into_stream(self) -> Result<ProjectionOscillatorStream, ProjectionOscillatorError> {
190 ProjectionOscillatorStream::try_new(ProjectionOscillatorParams {
191 length: self.length,
192 smooth_length: self.smooth_length,
193 })
194 }
195}
196
197#[derive(Debug, Error)]
198pub enum ProjectionOscillatorError {
199 #[error("projection_oscillator: Input data slice is empty.")]
200 EmptyInputData,
201 #[error(
202 "projection_oscillator: Input length mismatch: high = {high_len}, low = {low_len}, source = {source_len}"
203 )]
204 InputLengthMismatch {
205 high_len: usize,
206 low_len: usize,
207 source_len: usize,
208 },
209 #[error("projection_oscillator: All values are NaN.")]
210 AllValuesNaN,
211 #[error("projection_oscillator: Invalid length: {length}")]
212 InvalidLength { length: usize },
213 #[error("projection_oscillator: Invalid smooth_length: {smooth_length}")]
214 InvalidSmoothLength { smooth_length: usize },
215 #[error("projection_oscillator: Not enough valid data: needed = {needed}, valid = {valid}")]
216 NotEnoughValidData { needed: usize, valid: usize },
217 #[error("projection_oscillator: Output length mismatch: expected = {expected}, got = {got}")]
218 OutputLengthMismatch { expected: usize, got: usize },
219 #[error("projection_oscillator: Invalid range: start={start}, end={end}, step={step}")]
220 InvalidRange {
221 start: usize,
222 end: usize,
223 step: usize,
224 },
225 #[error("projection_oscillator: Invalid kernel for batch: {0:?}")]
226 InvalidKernelForBatch(Kernel),
227 #[error(
228 "projection_oscillator: Output length mismatch: dst = {dst_len}, expected = {expected_len}"
229 )]
230 MismatchedOutputLen { dst_len: usize, expected_len: usize },
231 #[error("projection_oscillator: Invalid input: {msg}")]
232 InvalidInput { msg: String },
233}
234
235#[derive(Debug, Clone)]
236struct WmaState {
237 period: usize,
238 denom: f64,
239 window: VecDeque<f64>,
240 sum: f64,
241 weighted_sum: f64,
242}
243
244impl WmaState {
245 #[inline(always)]
246 fn new(period: usize) -> Self {
247 Self {
248 period,
249 denom: (period * (period + 1) / 2) as f64,
250 window: VecDeque::with_capacity(period.max(1)),
251 sum: 0.0,
252 weighted_sum: 0.0,
253 }
254 }
255
256 #[inline(always)]
257 fn reset(&mut self) {
258 self.window.clear();
259 self.sum = 0.0;
260 self.weighted_sum = 0.0;
261 }
262
263 #[inline(always)]
264 fn update(&mut self, value: f64) -> Option<f64> {
265 if !value.is_finite() {
266 self.reset();
267 return None;
268 }
269 if self.period == 1 {
270 return Some(value);
271 }
272 if self.window.len() < self.period {
273 let weight = self.window.len() + 1;
274 self.window.push_back(value);
275 self.sum += value;
276 self.weighted_sum += value * weight as f64;
277 if self.window.len() == self.period {
278 Some(self.weighted_sum / self.denom)
279 } else {
280 None
281 }
282 } else {
283 let oldest = self.window.pop_front().unwrap_or(0.0);
284 let old_sum = self.sum;
285 self.window.push_back(value);
286 self.sum = old_sum - oldest + value;
287 self.weighted_sum = self.weighted_sum - old_sum + self.period as f64 * value;
288 Some(self.weighted_sum / self.denom)
289 }
290 }
291}
292
293#[derive(Debug, Clone)]
294pub struct ProjectionOscillatorStream {
295 length: usize,
296 smooth_length: usize,
297 high_window: VecDeque<f64>,
298 low_window: VecDeque<f64>,
299 high_slopes: VecDeque<f64>,
300 low_slopes: VecDeque<f64>,
301 pbo_wma: WmaState,
302 signal_wma: WmaState,
303}
304
305impl ProjectionOscillatorStream {
306 #[inline(always)]
307 pub fn try_new(params: ProjectionOscillatorParams) -> Result<Self, ProjectionOscillatorError> {
308 let length = params.length.unwrap_or(14);
309 let smooth_length = params.smooth_length.unwrap_or(4);
310 validate_params(length, smooth_length)?;
311 Ok(Self {
312 length,
313 smooth_length,
314 high_window: VecDeque::with_capacity(length.max(1)),
315 low_window: VecDeque::with_capacity(length.max(1)),
316 high_slopes: VecDeque::with_capacity(length.max(1)),
317 low_slopes: VecDeque::with_capacity(length.max(1)),
318 pbo_wma: WmaState::new(smooth_length),
319 signal_wma: WmaState::new(smooth_length),
320 })
321 }
322
323 #[inline(always)]
324 pub fn reset(&mut self) {
325 self.high_window.clear();
326 self.low_window.clear();
327 self.high_slopes.clear();
328 self.low_slopes.clear();
329 self.pbo_wma.reset();
330 self.signal_wma.reset();
331 }
332
333 #[inline(always)]
334 pub fn update(&mut self, high: f64, low: f64, source: f64) -> Option<(f64, f64)> {
335 if !is_valid_triple(high, low, source) {
336 self.reset();
337 return None;
338 }
339
340 push_ring(&mut self.high_window, high, self.length);
341 push_ring(&mut self.low_window, low, self.length);
342
343 let high_slope = if self.high_window.len() == self.length {
344 linreg_slope_from_window(&self.high_window)
345 } else {
346 f64::NAN
347 };
348 let low_slope = if self.low_window.len() == self.length {
349 linreg_slope_from_window(&self.low_window)
350 } else {
351 f64::NAN
352 };
353 push_ring(&mut self.high_slopes, high_slope, self.length);
354 push_ring(&mut self.low_slopes, low_slope, self.length);
355
356 if self.high_window.len() != self.length
357 || self.low_window.len() != self.length
358 || self.high_slopes.len() != self.length
359 || self.low_slopes.len() != self.length
360 || self.high_slopes.iter().any(|v| !v.is_finite())
361 || self.low_slopes.iter().any(|v| !v.is_finite())
362 {
363 return None;
364 }
365
366 let mut upper = f64::NEG_INFINITY;
367 let mut lower = f64::INFINITY;
368 let last = self.length - 1;
369 for age in 0..self.length {
370 let idx = last - age;
371 let projected_high = self.high_window[idx] + self.high_slopes[idx] * age as f64;
372 let projected_low = self.low_window[idx] + self.low_slopes[idx] * age as f64;
373 if projected_high > upper {
374 upper = projected_high;
375 }
376 if projected_low < lower {
377 lower = projected_low;
378 }
379 }
380
381 let range = upper - lower;
382 let raw = if range.abs() <= f64::EPSILON {
383 0.0
384 } else {
385 100.0 * (source - lower) / range
386 };
387
388 let pbo = self.pbo_wma.update(raw)?;
389 let signal = self.signal_wma.update(pbo).unwrap_or(f64::NAN);
390 Some((pbo, signal))
391 }
392
393 #[inline(always)]
394 pub fn get_pbo_warmup_period(&self) -> usize {
395 pbo_warmup_prefix(self.length, self.smooth_length)
396 }
397
398 #[inline(always)]
399 pub fn get_signal_warmup_period(&self) -> usize {
400 signal_warmup_prefix(self.length, self.smooth_length)
401 }
402}
403
404#[inline(always)]
405fn push_ring(buf: &mut VecDeque<f64>, value: f64, cap: usize) {
406 if cap == 0 {
407 return;
408 }
409 if buf.len() == cap {
410 buf.pop_front();
411 }
412 buf.push_back(value);
413}
414
415#[inline(always)]
416fn linreg_slope_from_window(window: &VecDeque<f64>) -> f64 {
417 let n = window.len();
418 if n <= 1 {
419 return 0.0;
420 }
421 let nf = n as f64;
422 let sum_x = (n * (n - 1) / 2) as f64;
423 let sum_x2 = ((n - 1) * n * (2 * n - 1) / 6) as f64;
424 let denom = nf * sum_x2 - sum_x * sum_x;
425 if denom.abs() <= f64::EPSILON {
426 return 0.0;
427 }
428 let mut sum_y = 0.0;
429 let mut sum_xy = 0.0;
430 for (idx, &value) in window.iter().enumerate() {
431 let x = idx as f64;
432 sum_y += value;
433 sum_xy += x * value;
434 }
435 (nf * sum_xy - sum_x * sum_y) / denom
436}
437
438#[inline(always)]
439fn is_valid_triple(high: f64, low: f64, source: f64) -> bool {
440 high.is_finite() && low.is_finite() && source.is_finite()
441}
442
443#[inline(always)]
444fn input_slices<'a>(
445 input: &'a ProjectionOscillatorInput<'a>,
446) -> Result<(&'a [f64], &'a [f64], &'a [f64]), ProjectionOscillatorError> {
447 match &input.data {
448 ProjectionOscillatorData::Candles { candles, source } => Ok((
449 candles.high.as_slice(),
450 candles.low.as_slice(),
451 source_type(candles, source),
452 )),
453 ProjectionOscillatorData::Slices { high, low, source } => Ok((high, low, source)),
454 }
455}
456
457#[inline(always)]
458fn validate_params(length: usize, smooth_length: usize) -> Result<(), ProjectionOscillatorError> {
459 if length == 0 {
460 return Err(ProjectionOscillatorError::InvalidLength { length });
461 }
462 if smooth_length == 0 {
463 return Err(ProjectionOscillatorError::InvalidSmoothLength { smooth_length });
464 }
465 Ok(())
466}
467
468#[inline(always)]
469fn signal_needed_bars(
470 length: usize,
471 smooth_length: usize,
472) -> Result<usize, ProjectionOscillatorError> {
473 length
474 .checked_mul(2)
475 .and_then(|v| smooth_length.checked_mul(2).and_then(|s| v.checked_add(s)))
476 .and_then(|v| v.checked_sub(3))
477 .ok_or_else(|| ProjectionOscillatorError::InvalidInput {
478 msg: "projection_oscillator: warmup overflow".to_string(),
479 })
480}
481
482#[inline(always)]
483fn pbo_warmup_prefix(length: usize, smooth_length: usize) -> usize {
484 length
485 .saturating_mul(2)
486 .saturating_add(smooth_length)
487 .saturating_sub(3)
488}
489
490#[inline(always)]
491fn signal_warmup_prefix(length: usize, smooth_length: usize) -> usize {
492 length
493 .saturating_mul(2)
494 .saturating_add(smooth_length.saturating_mul(2))
495 .saturating_sub(4)
496}
497
498#[inline(always)]
499fn longest_valid_run(high: &[f64], low: &[f64], source: &[f64]) -> usize {
500 let mut best = 0usize;
501 let mut cur = 0usize;
502 for ((&h, &l), &s) in high.iter().zip(low.iter()).zip(source.iter()) {
503 if is_valid_triple(h, l, s) {
504 cur += 1;
505 best = best.max(cur);
506 } else {
507 cur = 0;
508 }
509 }
510 best
511}
512
513fn validate_common(
514 high: &[f64],
515 low: &[f64],
516 source: &[f64],
517 length: usize,
518 smooth_length: usize,
519) -> Result<(), ProjectionOscillatorError> {
520 validate_params(length, smooth_length)?;
521 if high.is_empty() || low.is_empty() || source.is_empty() {
522 return Err(ProjectionOscillatorError::EmptyInputData);
523 }
524 if high.len() != low.len() || high.len() != source.len() {
525 return Err(ProjectionOscillatorError::InputLengthMismatch {
526 high_len: high.len(),
527 low_len: low.len(),
528 source_len: source.len(),
529 });
530 }
531 let longest = longest_valid_run(high, low, source);
532 if longest == 0 {
533 return Err(ProjectionOscillatorError::AllValuesNaN);
534 }
535 let needed = signal_needed_bars(length, smooth_length)?;
536 if longest < needed {
537 return Err(ProjectionOscillatorError::NotEnoughValidData {
538 needed,
539 valid: longest,
540 });
541 }
542 Ok(())
543}
544
545#[inline(always)]
546fn compute_row(
547 high: &[f64],
548 low: &[f64],
549 source: &[f64],
550 length: usize,
551 smooth_length: usize,
552 out_pbo: &mut [f64],
553 out_signal: &mut [f64],
554) {
555 let mut stream = ProjectionOscillatorStream::try_new(ProjectionOscillatorParams {
556 length: Some(length),
557 smooth_length: Some(smooth_length),
558 })
559 .expect("validated params");
560
561 for i in 0..high.len() {
562 if let Some((pbo, signal)) = stream.update(high[i], low[i], source[i]) {
563 out_pbo[i] = pbo;
564 if signal.is_finite() {
565 out_signal[i] = signal;
566 }
567 }
568 }
569}
570
571pub fn projection_oscillator(
572 input: &ProjectionOscillatorInput,
573) -> Result<ProjectionOscillatorOutput, ProjectionOscillatorError> {
574 projection_oscillator_with_kernel(input, Kernel::Auto)
575}
576
577pub fn projection_oscillator_with_kernel(
578 input: &ProjectionOscillatorInput,
579 kernel: Kernel,
580) -> Result<ProjectionOscillatorOutput, ProjectionOscillatorError> {
581 let (high, low, source) = input_slices(input)?;
582 let length = input.get_length();
583 let smooth_length = input.get_smooth_length();
584 validate_common(high, low, source, length, smooth_length)?;
585
586 let _chosen = match kernel {
587 Kernel::Auto => detect_best_kernel(),
588 other => other,
589 };
590
591 let mut pbo = alloc_with_nan_prefix(high.len(), pbo_warmup_prefix(length, smooth_length));
592 let mut signal = alloc_with_nan_prefix(high.len(), signal_warmup_prefix(length, smooth_length));
593 compute_row(
594 high,
595 low,
596 source,
597 length,
598 smooth_length,
599 &mut pbo,
600 &mut signal,
601 );
602 Ok(ProjectionOscillatorOutput { pbo, signal })
603}
604
605pub fn projection_oscillator_into_slice(
606 out_pbo: &mut [f64],
607 out_signal: &mut [f64],
608 input: &ProjectionOscillatorInput,
609 kernel: Kernel,
610) -> Result<(), ProjectionOscillatorError> {
611 let (high, low, source) = input_slices(input)?;
612 if out_pbo.len() != high.len() {
613 return Err(ProjectionOscillatorError::OutputLengthMismatch {
614 expected: high.len(),
615 got: out_pbo.len(),
616 });
617 }
618 if out_signal.len() != high.len() {
619 return Err(ProjectionOscillatorError::OutputLengthMismatch {
620 expected: high.len(),
621 got: out_signal.len(),
622 });
623 }
624 let length = input.get_length();
625 let smooth_length = input.get_smooth_length();
626 validate_common(high, low, source, length, smooth_length)?;
627
628 let _chosen = match kernel {
629 Kernel::Auto => detect_best_kernel(),
630 other => other,
631 };
632
633 out_pbo.fill(f64::NAN);
634 out_signal.fill(f64::NAN);
635 compute_row(
636 high,
637 low,
638 source,
639 length,
640 smooth_length,
641 out_pbo,
642 out_signal,
643 );
644 Ok(())
645}
646
647#[cfg(not(target_arch = "wasm32"))]
648pub fn projection_oscillator_into(
649 input: &ProjectionOscillatorInput,
650 out_pbo: &mut [f64],
651 out_signal: &mut [f64],
652) -> Result<(), ProjectionOscillatorError> {
653 projection_oscillator_into_slice(out_pbo, out_signal, input, Kernel::Auto)
654}
655
656#[derive(Debug, Clone, Copy)]
657pub struct ProjectionOscillatorBatchRange {
658 pub length: (usize, usize, usize),
659 pub smooth_length: (usize, usize, usize),
660}
661
662impl Default for ProjectionOscillatorBatchRange {
663 fn default() -> Self {
664 Self {
665 length: (14, 14, 0),
666 smooth_length: (4, 4, 0),
667 }
668 }
669}
670
671#[derive(Debug, Clone)]
672pub struct ProjectionOscillatorBatchOutput {
673 pub pbo: Vec<f64>,
674 pub signal: Vec<f64>,
675 pub combos: Vec<ProjectionOscillatorParams>,
676 pub rows: usize,
677 pub cols: usize,
678}
679
680#[derive(Copy, Clone, Debug)]
681pub struct ProjectionOscillatorBatchBuilder {
682 range: ProjectionOscillatorBatchRange,
683 kernel: Kernel,
684}
685
686impl Default for ProjectionOscillatorBatchBuilder {
687 fn default() -> Self {
688 Self {
689 range: ProjectionOscillatorBatchRange::default(),
690 kernel: Kernel::Auto,
691 }
692 }
693}
694
695impl ProjectionOscillatorBatchBuilder {
696 #[inline(always)]
697 pub fn new() -> Self {
698 Self::default()
699 }
700
701 #[inline(always)]
702 pub fn length_range(mut self, value: (usize, usize, usize)) -> Self {
703 self.range.length = value;
704 self
705 }
706
707 #[inline(always)]
708 pub fn smooth_length_range(mut self, value: (usize, usize, usize)) -> Self {
709 self.range.smooth_length = value;
710 self
711 }
712
713 #[inline(always)]
714 pub fn kernel(mut self, value: Kernel) -> Self {
715 self.kernel = value;
716 self
717 }
718
719 #[inline(always)]
720 pub fn apply_slices(
721 self,
722 high: &[f64],
723 low: &[f64],
724 source: &[f64],
725 ) -> Result<ProjectionOscillatorBatchOutput, ProjectionOscillatorError> {
726 projection_oscillator_batch_with_kernel(high, low, source, &self.range, self.kernel)
727 }
728
729 #[inline(always)]
730 pub fn apply(
731 self,
732 candles: &Candles,
733 ) -> Result<ProjectionOscillatorBatchOutput, ProjectionOscillatorError> {
734 projection_oscillator_batch_with_kernel(
735 candles.high.as_slice(),
736 candles.low.as_slice(),
737 candles.close.as_slice(),
738 &self.range,
739 self.kernel,
740 )
741 }
742}
743
744fn expand_axis(range: (usize, usize, usize)) -> Result<Vec<usize>, ProjectionOscillatorError> {
745 let (start, end, step) = range;
746 if step == 0 {
747 return Ok(vec![start]);
748 }
749 if start > end {
750 return Err(ProjectionOscillatorError::InvalidRange { start, end, step });
751 }
752 let mut out = Vec::new();
753 let mut cur = start;
754 loop {
755 out.push(cur);
756 if cur >= end {
757 break;
758 }
759 let next =
760 cur.checked_add(step)
761 .ok_or_else(|| ProjectionOscillatorError::InvalidInput {
762 msg: "projection_oscillator: range step overflow".to_string(),
763 })?;
764 if next <= cur {
765 return Err(ProjectionOscillatorError::InvalidRange { start, end, step });
766 }
767 cur = next.min(end);
768 }
769 Ok(out)
770}
771
772fn expand_grid_checked(
773 sweep: &ProjectionOscillatorBatchRange,
774) -> Result<Vec<ProjectionOscillatorParams>, ProjectionOscillatorError> {
775 let lengths = expand_axis(sweep.length)?;
776 let smooth_lengths = expand_axis(sweep.smooth_length)?;
777 let total = lengths
778 .len()
779 .checked_mul(smooth_lengths.len())
780 .ok_or_else(|| ProjectionOscillatorError::InvalidInput {
781 msg: "projection_oscillator: parameter grid size overflow".to_string(),
782 })?;
783 let mut out = Vec::with_capacity(total);
784 for &length in &lengths {
785 for &smooth_length in &smooth_lengths {
786 out.push(ProjectionOscillatorParams {
787 length: Some(length),
788 smooth_length: Some(smooth_length),
789 });
790 }
791 }
792 Ok(out)
793}
794
795pub fn expand_grid_projection_oscillator(
796 sweep: &ProjectionOscillatorBatchRange,
797) -> Result<Vec<ProjectionOscillatorParams>, ProjectionOscillatorError> {
798 expand_grid_checked(sweep)
799}
800
801pub fn projection_oscillator_batch_with_kernel(
802 high: &[f64],
803 low: &[f64],
804 source: &[f64],
805 sweep: &ProjectionOscillatorBatchRange,
806 kernel: Kernel,
807) -> Result<ProjectionOscillatorBatchOutput, ProjectionOscillatorError> {
808 projection_oscillator_batch_inner(high, low, source, sweep, kernel, true)
809}
810
811pub fn projection_oscillator_batch_slice(
812 high: &[f64],
813 low: &[f64],
814 source: &[f64],
815 sweep: &ProjectionOscillatorBatchRange,
816 kernel: Kernel,
817) -> Result<ProjectionOscillatorBatchOutput, ProjectionOscillatorError> {
818 projection_oscillator_batch_inner(high, low, source, sweep, kernel, false)
819}
820
821pub fn projection_oscillator_batch_par_slice(
822 high: &[f64],
823 low: &[f64],
824 source: &[f64],
825 sweep: &ProjectionOscillatorBatchRange,
826 kernel: Kernel,
827) -> Result<ProjectionOscillatorBatchOutput, ProjectionOscillatorError> {
828 projection_oscillator_batch_inner(high, low, source, sweep, kernel, true)
829}
830
831fn projection_oscillator_batch_inner(
832 high: &[f64],
833 low: &[f64],
834 source: &[f64],
835 sweep: &ProjectionOscillatorBatchRange,
836 kernel: Kernel,
837 parallel: bool,
838) -> Result<ProjectionOscillatorBatchOutput, ProjectionOscillatorError> {
839 let combos = expand_grid_checked(sweep)?;
840 let rows = combos.len();
841 let cols = high.len();
842 let total = rows
843 .checked_mul(cols)
844 .ok_or_else(|| ProjectionOscillatorError::InvalidInput {
845 msg: "projection_oscillator: rows*cols overflow in batch".to_string(),
846 })?;
847
848 if high.is_empty() || low.is_empty() || source.is_empty() {
849 return Err(ProjectionOscillatorError::EmptyInputData);
850 }
851 if high.len() != low.len() || high.len() != source.len() {
852 return Err(ProjectionOscillatorError::InputLengthMismatch {
853 high_len: high.len(),
854 low_len: low.len(),
855 source_len: source.len(),
856 });
857 }
858
859 let mut pbo_warmups = Vec::with_capacity(rows);
860 let mut signal_warmups = Vec::with_capacity(rows);
861 let mut max_needed = 0usize;
862 for combo in &combos {
863 let length = combo.length.unwrap_or(14);
864 let smooth_length = combo.smooth_length.unwrap_or(4);
865 validate_params(length, smooth_length)?;
866 pbo_warmups.push(pbo_warmup_prefix(length, smooth_length));
867 signal_warmups.push(signal_warmup_prefix(length, smooth_length));
868 max_needed = max_needed.max(signal_needed_bars(length, smooth_length)?);
869 }
870
871 let longest = longest_valid_run(high, low, source);
872 if longest == 0 {
873 return Err(ProjectionOscillatorError::AllValuesNaN);
874 }
875 if longest < max_needed {
876 return Err(ProjectionOscillatorError::NotEnoughValidData {
877 needed: max_needed,
878 valid: longest,
879 });
880 }
881
882 let mut pbo_mu = make_uninit_matrix(rows, cols);
883 let mut signal_mu = make_uninit_matrix(rows, cols);
884 init_matrix_prefixes(&mut pbo_mu, cols, &pbo_warmups);
885 init_matrix_prefixes(&mut signal_mu, cols, &signal_warmups);
886
887 let mut pbo = unsafe {
888 Vec::from_raw_parts(
889 pbo_mu.as_mut_ptr() as *mut f64,
890 pbo_mu.len(),
891 pbo_mu.capacity(),
892 )
893 };
894 let mut signal = unsafe {
895 Vec::from_raw_parts(
896 signal_mu.as_mut_ptr() as *mut f64,
897 signal_mu.len(),
898 signal_mu.capacity(),
899 )
900 };
901 std::mem::forget(pbo_mu);
902 std::mem::forget(signal_mu);
903 debug_assert_eq!(pbo.len(), total);
904 debug_assert_eq!(signal.len(), total);
905
906 projection_oscillator_batch_inner_into(
907 high,
908 low,
909 source,
910 sweep,
911 kernel,
912 parallel,
913 &mut pbo,
914 &mut signal,
915 )?;
916
917 Ok(ProjectionOscillatorBatchOutput {
918 pbo,
919 signal,
920 combos,
921 rows,
922 cols,
923 })
924}
925
926fn projection_oscillator_batch_inner_into(
927 high: &[f64],
928 low: &[f64],
929 source: &[f64],
930 sweep: &ProjectionOscillatorBatchRange,
931 kernel: Kernel,
932 parallel: bool,
933 out_pbo: &mut [f64],
934 out_signal: &mut [f64],
935) -> Result<Vec<ProjectionOscillatorParams>, ProjectionOscillatorError> {
936 match kernel {
937 Kernel::Auto
938 | Kernel::Scalar
939 | Kernel::ScalarBatch
940 | Kernel::Avx2
941 | Kernel::Avx2Batch
942 | Kernel::Avx512
943 | Kernel::Avx512Batch => {}
944 other => return Err(ProjectionOscillatorError::InvalidKernelForBatch(other)),
945 }
946
947 let combos = expand_grid_checked(sweep)?;
948 let len = high.len();
949 if len == 0 || low.is_empty() || source.is_empty() {
950 return Err(ProjectionOscillatorError::EmptyInputData);
951 }
952 if len != low.len() || len != source.len() {
953 return Err(ProjectionOscillatorError::InputLengthMismatch {
954 high_len: len,
955 low_len: low.len(),
956 source_len: source.len(),
957 });
958 }
959
960 let total =
961 combos
962 .len()
963 .checked_mul(len)
964 .ok_or_else(|| ProjectionOscillatorError::InvalidInput {
965 msg: "projection_oscillator: rows*cols overflow in batch_into".to_string(),
966 })?;
967 if out_pbo.len() != total {
968 return Err(ProjectionOscillatorError::MismatchedOutputLen {
969 dst_len: out_pbo.len(),
970 expected_len: total,
971 });
972 }
973 if out_signal.len() != total {
974 return Err(ProjectionOscillatorError::MismatchedOutputLen {
975 dst_len: out_signal.len(),
976 expected_len: total,
977 });
978 }
979
980 let longest = longest_valid_run(high, low, source);
981 if longest == 0 {
982 return Err(ProjectionOscillatorError::AllValuesNaN);
983 }
984 let mut max_needed = 0usize;
985 for combo in &combos {
986 let length = combo.length.unwrap_or(14);
987 let smooth_length = combo.smooth_length.unwrap_or(4);
988 validate_params(length, smooth_length)?;
989 max_needed = max_needed.max(signal_needed_bars(length, smooth_length)?);
990 }
991 if longest < max_needed {
992 return Err(ProjectionOscillatorError::NotEnoughValidData {
993 needed: max_needed,
994 valid: longest,
995 });
996 }
997
998 let _chosen = match kernel {
999 Kernel::Auto => detect_best_batch_kernel(),
1000 other => other,
1001 };
1002
1003 let worker = |row: usize, dst_pbo: &mut [f64], dst_signal: &mut [f64]| {
1004 let combo = &combos[row];
1005 dst_pbo.fill(f64::NAN);
1006 dst_signal.fill(f64::NAN);
1007 compute_row(
1008 high,
1009 low,
1010 source,
1011 combo.length.unwrap_or(14),
1012 combo.smooth_length.unwrap_or(4),
1013 dst_pbo,
1014 dst_signal,
1015 );
1016 };
1017
1018 if parallel && combos.len() > 1 {
1019 #[cfg(not(target_arch = "wasm32"))]
1020 {
1021 out_pbo
1022 .par_chunks_mut(len)
1023 .zip(out_signal.par_chunks_mut(len))
1024 .enumerate()
1025 .for_each(|(row, (dst_pbo, dst_signal))| worker(row, dst_pbo, dst_signal));
1026 }
1027 #[cfg(target_arch = "wasm32")]
1028 {
1029 for (row, (dst_pbo, dst_signal)) in out_pbo
1030 .chunks_mut(len)
1031 .zip(out_signal.chunks_mut(len))
1032 .enumerate()
1033 {
1034 worker(row, dst_pbo, dst_signal);
1035 }
1036 }
1037 } else {
1038 for (row, (dst_pbo, dst_signal)) in out_pbo
1039 .chunks_mut(len)
1040 .zip(out_signal.chunks_mut(len))
1041 .enumerate()
1042 {
1043 worker(row, dst_pbo, dst_signal);
1044 }
1045 }
1046
1047 Ok(combos)
1048}
1049
1050#[cfg(feature = "python")]
1051#[pyfunction(name = "projection_oscillator", signature = (high, low, source, length=14, smooth_length=4, kernel=None))]
1052pub fn projection_oscillator_py<'py>(
1053 py: Python<'py>,
1054 high: PyReadonlyArray1<'py, f64>,
1055 low: PyReadonlyArray1<'py, f64>,
1056 source: PyReadonlyArray1<'py, f64>,
1057 length: usize,
1058 smooth_length: usize,
1059 kernel: Option<&str>,
1060) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
1061 let high = high.as_slice()?;
1062 let low = low.as_slice()?;
1063 let source = source.as_slice()?;
1064 let kern = validate_kernel(kernel, true)?;
1065 let input = ProjectionOscillatorInput::from_slices(
1066 high,
1067 low,
1068 source,
1069 ProjectionOscillatorParams {
1070 length: Some(length),
1071 smooth_length: Some(smooth_length),
1072 },
1073 );
1074 let out = py
1075 .allow_threads(|| projection_oscillator_with_kernel(&input, kern))
1076 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1077 Ok((out.pbo.into_pyarray(py), out.signal.into_pyarray(py)))
1078}
1079
1080#[cfg(feature = "python")]
1081#[pyclass(name = "ProjectionOscillatorStream")]
1082pub struct ProjectionOscillatorStreamPy {
1083 inner: ProjectionOscillatorStream,
1084}
1085
1086#[cfg(feature = "python")]
1087#[pymethods]
1088impl ProjectionOscillatorStreamPy {
1089 #[new]
1090 #[pyo3(signature = (length=14, smooth_length=4))]
1091 fn new(length: usize, smooth_length: usize) -> PyResult<Self> {
1092 let inner = ProjectionOscillatorStream::try_new(ProjectionOscillatorParams {
1093 length: Some(length),
1094 smooth_length: Some(smooth_length),
1095 })
1096 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1097 Ok(Self { inner })
1098 }
1099
1100 fn update(&mut self, high: f64, low: f64, source: f64) -> Option<(f64, f64)> {
1101 self.inner.update(high, low, source)
1102 }
1103
1104 fn reset(&mut self) {
1105 self.inner.reset();
1106 }
1107}
1108
1109#[cfg(feature = "python")]
1110#[pyfunction(name = "projection_oscillator_batch", signature = (high, low, source, length_range=(14, 14, 0), smooth_length_range=(4, 4, 0), kernel=None))]
1111pub fn projection_oscillator_batch_py<'py>(
1112 py: Python<'py>,
1113 high: PyReadonlyArray1<'py, f64>,
1114 low: PyReadonlyArray1<'py, f64>,
1115 source: PyReadonlyArray1<'py, f64>,
1116 length_range: (usize, usize, usize),
1117 smooth_length_range: (usize, usize, usize),
1118 kernel: Option<&str>,
1119) -> PyResult<Bound<'py, PyDict>> {
1120 let high = high.as_slice()?;
1121 let low = low.as_slice()?;
1122 let source = source.as_slice()?;
1123 let kern = validate_kernel(kernel, true)?;
1124
1125 let output = py
1126 .allow_threads(|| {
1127 projection_oscillator_batch_with_kernel(
1128 high,
1129 low,
1130 source,
1131 &ProjectionOscillatorBatchRange {
1132 length: length_range,
1133 smooth_length: smooth_length_range,
1134 },
1135 kern,
1136 )
1137 })
1138 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1139
1140 let dict = PyDict::new(py);
1141 dict.set_item(
1142 "pbo",
1143 output
1144 .pbo
1145 .into_pyarray(py)
1146 .reshape((output.rows, output.cols))?,
1147 )?;
1148 dict.set_item(
1149 "signal",
1150 output
1151 .signal
1152 .into_pyarray(py)
1153 .reshape((output.rows, output.cols))?,
1154 )?;
1155 dict.set_item(
1156 "lengths",
1157 output
1158 .combos
1159 .iter()
1160 .map(|params| params.length.unwrap_or(14) as u64)
1161 .collect::<Vec<_>>()
1162 .into_pyarray(py),
1163 )?;
1164 dict.set_item(
1165 "smooth_lengths",
1166 output
1167 .combos
1168 .iter()
1169 .map(|params| params.smooth_length.unwrap_or(4) as u64)
1170 .collect::<Vec<_>>()
1171 .into_pyarray(py),
1172 )?;
1173 dict.set_item("rows", output.rows)?;
1174 dict.set_item("cols", output.cols)?;
1175 Ok(dict)
1176}
1177
1178#[cfg(feature = "python")]
1179pub fn register_projection_oscillator_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1180 m.add_function(wrap_pyfunction!(projection_oscillator_py, m)?)?;
1181 m.add_function(wrap_pyfunction!(projection_oscillator_batch_py, m)?)?;
1182 m.add_class::<ProjectionOscillatorStreamPy>()?;
1183 Ok(())
1184}
1185
1186#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1187#[derive(Debug, Clone, Serialize, Deserialize)]
1188pub struct ProjectionOscillatorBatchConfig {
1189 pub length_range: Vec<usize>,
1190 pub smooth_length_range: Vec<usize>,
1191}
1192
1193#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1194#[wasm_bindgen(js_name = projection_oscillator_js)]
1195pub fn projection_oscillator_js(
1196 high: &[f64],
1197 low: &[f64],
1198 source: &[f64],
1199 length: usize,
1200 smooth_length: usize,
1201) -> Result<JsValue, JsValue> {
1202 let input = ProjectionOscillatorInput::from_slices(
1203 high,
1204 low,
1205 source,
1206 ProjectionOscillatorParams {
1207 length: Some(length),
1208 smooth_length: Some(smooth_length),
1209 },
1210 );
1211 let out = projection_oscillator_with_kernel(&input, Kernel::Auto)
1212 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1213 let obj = js_sys::Object::new();
1214 js_sys::Reflect::set(
1215 &obj,
1216 &JsValue::from_str("pbo"),
1217 &serde_wasm_bindgen::to_value(&out.pbo).unwrap(),
1218 )?;
1219 js_sys::Reflect::set(
1220 &obj,
1221 &JsValue::from_str("signal"),
1222 &serde_wasm_bindgen::to_value(&out.signal).unwrap(),
1223 )?;
1224 Ok(obj.into())
1225}
1226
1227#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1228#[wasm_bindgen(js_name = projection_oscillator_batch_js)]
1229pub fn projection_oscillator_batch_js(
1230 high: &[f64],
1231 low: &[f64],
1232 source: &[f64],
1233 config: JsValue,
1234) -> Result<JsValue, JsValue> {
1235 let config: ProjectionOscillatorBatchConfig = serde_wasm_bindgen::from_value(config)
1236 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1237 if config.length_range.len() != 3 || config.smooth_length_range.len() != 3 {
1238 return Err(JsValue::from_str(
1239 "Invalid config: every range must have exactly 3 elements [start, end, step]",
1240 ));
1241 }
1242 let out = projection_oscillator_batch_with_kernel(
1243 high,
1244 low,
1245 source,
1246 &ProjectionOscillatorBatchRange {
1247 length: (
1248 config.length_range[0],
1249 config.length_range[1],
1250 config.length_range[2],
1251 ),
1252 smooth_length: (
1253 config.smooth_length_range[0],
1254 config.smooth_length_range[1],
1255 config.smooth_length_range[2],
1256 ),
1257 },
1258 Kernel::Auto,
1259 )
1260 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1261 let obj = js_sys::Object::new();
1262 js_sys::Reflect::set(
1263 &obj,
1264 &JsValue::from_str("pbo"),
1265 &serde_wasm_bindgen::to_value(&out.pbo).unwrap(),
1266 )?;
1267 js_sys::Reflect::set(
1268 &obj,
1269 &JsValue::from_str("signal"),
1270 &serde_wasm_bindgen::to_value(&out.signal).unwrap(),
1271 )?;
1272 js_sys::Reflect::set(
1273 &obj,
1274 &JsValue::from_str("rows"),
1275 &JsValue::from_f64(out.rows as f64),
1276 )?;
1277 js_sys::Reflect::set(
1278 &obj,
1279 &JsValue::from_str("cols"),
1280 &JsValue::from_f64(out.cols as f64),
1281 )?;
1282 js_sys::Reflect::set(
1283 &obj,
1284 &JsValue::from_str("combos"),
1285 &serde_wasm_bindgen::to_value(&out.combos).unwrap(),
1286 )?;
1287 Ok(obj.into())
1288}
1289
1290#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1291#[wasm_bindgen]
1292pub fn projection_oscillator_alloc(len: usize) -> *mut f64 {
1293 let mut vec = Vec::<f64>::with_capacity(2 * len);
1294 let ptr = vec.as_mut_ptr();
1295 std::mem::forget(vec);
1296 ptr
1297}
1298
1299#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1300#[wasm_bindgen]
1301pub fn projection_oscillator_free(ptr: *mut f64, len: usize) {
1302 if !ptr.is_null() {
1303 unsafe {
1304 let _ = Vec::from_raw_parts(ptr, 2 * len, 2 * len);
1305 }
1306 }
1307}
1308
1309#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1310#[wasm_bindgen]
1311pub fn projection_oscillator_into(
1312 high_ptr: *const f64,
1313 low_ptr: *const f64,
1314 source_ptr: *const f64,
1315 out_ptr: *mut f64,
1316 len: usize,
1317 length: usize,
1318 smooth_length: usize,
1319) -> Result<(), JsValue> {
1320 if high_ptr.is_null() || low_ptr.is_null() || source_ptr.is_null() || out_ptr.is_null() {
1321 return Err(JsValue::from_str(
1322 "null pointer passed to projection_oscillator_into",
1323 ));
1324 }
1325 unsafe {
1326 let high = std::slice::from_raw_parts(high_ptr, len);
1327 let low = std::slice::from_raw_parts(low_ptr, len);
1328 let source = std::slice::from_raw_parts(source_ptr, len);
1329 let out = std::slice::from_raw_parts_mut(out_ptr, 2 * len);
1330 let (dst_pbo, dst_signal) = out.split_at_mut(len);
1331 let input = ProjectionOscillatorInput::from_slices(
1332 high,
1333 low,
1334 source,
1335 ProjectionOscillatorParams {
1336 length: Some(length),
1337 smooth_length: Some(smooth_length),
1338 },
1339 );
1340 projection_oscillator_into_slice(dst_pbo, dst_signal, &input, Kernel::Auto)
1341 .map_err(|e| JsValue::from_str(&e.to_string()))
1342 }
1343}
1344
1345#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1346#[wasm_bindgen]
1347pub fn projection_oscillator_batch_into(
1348 high_ptr: *const f64,
1349 low_ptr: *const f64,
1350 source_ptr: *const f64,
1351 out_ptr: *mut f64,
1352 len: usize,
1353 length_start: usize,
1354 length_end: usize,
1355 length_step: usize,
1356 smooth_length_start: usize,
1357 smooth_length_end: usize,
1358 smooth_length_step: usize,
1359) -> Result<usize, JsValue> {
1360 if high_ptr.is_null() || low_ptr.is_null() || source_ptr.is_null() || out_ptr.is_null() {
1361 return Err(JsValue::from_str(
1362 "null pointer passed to projection_oscillator_batch_into",
1363 ));
1364 }
1365 let sweep = ProjectionOscillatorBatchRange {
1366 length: (length_start, length_end, length_step),
1367 smooth_length: (smooth_length_start, smooth_length_end, smooth_length_step),
1368 };
1369 let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1370 let rows = combos.len();
1371 let total = rows
1372 .checked_mul(len)
1373 .and_then(|v| v.checked_mul(2))
1374 .ok_or_else(|| {
1375 JsValue::from_str("rows*cols overflow in projection_oscillator_batch_into")
1376 })?;
1377 unsafe {
1378 let high = std::slice::from_raw_parts(high_ptr, len);
1379 let low = std::slice::from_raw_parts(low_ptr, len);
1380 let source = std::slice::from_raw_parts(source_ptr, len);
1381 let out = std::slice::from_raw_parts_mut(out_ptr, total);
1382 let split = rows * len;
1383 let (dst_pbo, dst_signal) = out.split_at_mut(split);
1384 projection_oscillator_batch_inner_into(
1385 high,
1386 low,
1387 source,
1388 &sweep,
1389 Kernel::Auto,
1390 false,
1391 dst_pbo,
1392 dst_signal,
1393 )
1394 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1395 }
1396 Ok(rows)
1397}
1398
1399#[cfg(test)]
1400mod tests {
1401 use super::*;
1402 use crate::indicators::dispatch::{
1403 compute_cpu, IndicatorComputeRequest, IndicatorDataRef, ParamKV, ParamValue,
1404 };
1405
1406 fn sample_ohlc(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1407 let high: Vec<f64> = (0..len)
1408 .map(|i| {
1409 let x = i as f64;
1410 101.0 + x * 0.04 + (x * 0.11).sin() * 1.4 + (x * 0.017).cos() * 0.5
1411 })
1412 .collect();
1413 let low: Vec<f64> = high
1414 .iter()
1415 .enumerate()
1416 .map(|(i, &h)| h - 1.2 - ((i as f64) * 0.07).cos().abs() * 0.35)
1417 .collect();
1418 let close: Vec<f64> = high
1419 .iter()
1420 .zip(low.iter())
1421 .enumerate()
1422 .map(|(i, (&h, &l))| l + (h - l) * (0.35 + 0.25 * ((i as f64) * 0.09).sin().abs()))
1423 .collect();
1424 (high, low, close)
1425 }
1426
1427 fn naive_projection_oscillator(
1428 high: &[f64],
1429 low: &[f64],
1430 source: &[f64],
1431 length: usize,
1432 smooth_length: usize,
1433 ) -> (Vec<f64>, Vec<f64>) {
1434 let mut pbo = vec![f64::NAN; high.len()];
1435 let mut signal = vec![f64::NAN; high.len()];
1436 compute_row(
1437 high,
1438 low,
1439 source,
1440 length,
1441 smooth_length,
1442 &mut pbo,
1443 &mut signal,
1444 );
1445 (pbo, signal)
1446 }
1447
1448 fn assert_series_close(left: &[f64], right: &[f64], tol: f64) {
1449 assert_eq!(left.len(), right.len());
1450 for (a, b) in left.iter().zip(right.iter()) {
1451 if a.is_nan() || b.is_nan() {
1452 assert!(a.is_nan() && b.is_nan());
1453 } else {
1454 assert!((a - b).abs() <= tol, "left={a} right={b}");
1455 }
1456 }
1457 }
1458
1459 #[test]
1460 fn projection_oscillator_matches_naive() -> Result<(), Box<dyn Error>> {
1461 let (high, low, close) = sample_ohlc(256);
1462 let input = ProjectionOscillatorInput::from_slices(
1463 &high,
1464 &low,
1465 &close,
1466 ProjectionOscillatorParams::default(),
1467 );
1468 let out = projection_oscillator_with_kernel(&input, Kernel::Scalar)?;
1469 let (expected_pbo, expected_signal) =
1470 naive_projection_oscillator(&high, &low, &close, 14, 4);
1471 assert_series_close(&out.pbo, &expected_pbo, 1e-12);
1472 assert_series_close(&out.signal, &expected_signal, 1e-12);
1473 Ok(())
1474 }
1475
1476 #[test]
1477 fn projection_oscillator_into_matches_api() -> Result<(), Box<dyn Error>> {
1478 let (high, low, close) = sample_ohlc(200);
1479 let input = ProjectionOscillatorInput::from_slices(
1480 &high,
1481 &low,
1482 &close,
1483 ProjectionOscillatorParams {
1484 length: Some(10),
1485 smooth_length: Some(3),
1486 },
1487 );
1488 let base = projection_oscillator(&input)?;
1489 let mut pbo = vec![0.0; close.len()];
1490 let mut signal = vec![0.0; close.len()];
1491 projection_oscillator_into_slice(&mut pbo, &mut signal, &input, Kernel::Auto)?;
1492 assert_series_close(&base.pbo, &pbo, 1e-12);
1493 assert_series_close(&base.signal, &signal, 1e-12);
1494 Ok(())
1495 }
1496
1497 #[test]
1498 fn projection_oscillator_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1499 let (high, low, close) = sample_ohlc(220);
1500 let params = ProjectionOscillatorParams {
1501 length: Some(14),
1502 smooth_length: Some(4),
1503 };
1504 let batch = projection_oscillator(&ProjectionOscillatorInput::from_slices(
1505 &high,
1506 &low,
1507 &close,
1508 params.clone(),
1509 ))?;
1510 let mut stream = ProjectionOscillatorStream::try_new(params)?;
1511 let mut pbo = Vec::with_capacity(close.len());
1512 let mut signal = Vec::with_capacity(close.len());
1513 for i in 0..close.len() {
1514 if let Some((p, s)) = stream.update(high[i], low[i], close[i]) {
1515 pbo.push(p);
1516 signal.push(s);
1517 } else {
1518 pbo.push(f64::NAN);
1519 signal.push(f64::NAN);
1520 }
1521 }
1522 assert_series_close(&batch.pbo, &pbo, 1e-12);
1523 assert_series_close(&batch.signal, &signal, 1e-12);
1524 Ok(())
1525 }
1526
1527 #[test]
1528 fn projection_oscillator_batch_single_matches_single() -> Result<(), Box<dyn Error>> {
1529 let (high, low, close) = sample_ohlc(180);
1530 let single = projection_oscillator(&ProjectionOscillatorInput::from_slices(
1531 &high,
1532 &low,
1533 &close,
1534 ProjectionOscillatorParams::default(),
1535 ))?;
1536 let batch = projection_oscillator_batch_with_kernel(
1537 &high,
1538 &low,
1539 &close,
1540 &ProjectionOscillatorBatchRange::default(),
1541 Kernel::Auto,
1542 )?;
1543 assert_eq!(batch.rows, 1);
1544 assert_eq!(batch.cols, close.len());
1545 assert_series_close(&batch.pbo, &single.pbo, 1e-12);
1546 assert_series_close(&batch.signal, &single.signal, 1e-12);
1547 Ok(())
1548 }
1549
1550 #[test]
1551 fn projection_oscillator_rejects_invalid_params() {
1552 let (high, low, close) = sample_ohlc(64);
1553 let err = projection_oscillator(&ProjectionOscillatorInput::from_slices(
1554 &high,
1555 &low,
1556 &close,
1557 ProjectionOscillatorParams {
1558 length: Some(0),
1559 ..ProjectionOscillatorParams::default()
1560 },
1561 ))
1562 .unwrap_err();
1563 assert!(matches!(
1564 err,
1565 ProjectionOscillatorError::InvalidLength { .. }
1566 ));
1567 }
1568
1569 #[test]
1570 fn projection_oscillator_dispatch_compute_returns_pbo() -> Result<(), Box<dyn Error>> {
1571 let (high, low, close) = sample_ohlc(160);
1572 let out = compute_cpu(IndicatorComputeRequest {
1573 indicator_id: "projection_oscillator",
1574 output_id: Some("pbo"),
1575 data: IndicatorDataRef::Ohlc {
1576 open: &close,
1577 high: &high,
1578 low: &low,
1579 close: &close,
1580 },
1581 params: &[
1582 ParamKV {
1583 key: "length",
1584 value: ParamValue::Int(14),
1585 },
1586 ParamKV {
1587 key: "smooth_length",
1588 value: ParamValue::Int(4),
1589 },
1590 ],
1591 kernel: Kernel::Auto,
1592 })?;
1593 assert_eq!(out.output_id, "pbo");
1594 assert_eq!(out.cols, close.len());
1595 Ok(())
1596 }
1597}