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