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