1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
3#[cfg(all(feature = "python", feature = "cuda"))]
4use cust::context::Context;
5#[cfg(all(feature = "python", feature = "cuda"))]
6use cust::memory::DeviceBuffer;
7#[cfg(feature = "python")]
8use numpy::{
9 IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1, PyReadonlyArray2,
10 PyUntypedArrayMethods,
11};
12#[cfg(feature = "python")]
13use pyo3::exceptions::PyValueError;
14#[cfg(feature = "python")]
15use pyo3::prelude::*;
16#[cfg(feature = "python")]
17use pyo3::types::PyDict;
18
19#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
20use serde::{Deserialize, Serialize};
21#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
22use wasm_bindgen::prelude::*;
23
24use crate::utilities::data_loader::Candles;
25use crate::utilities::enums::Kernel;
26use crate::utilities::helpers::{
27 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
28 make_uninit_matrix,
29};
30#[cfg(feature = "python")]
31use crate::utilities::kernel_validation::validate_kernel;
32#[cfg(not(target_arch = "wasm32"))]
33use rayon::prelude::*;
34use std::mem::{ManuallyDrop, MaybeUninit};
35#[cfg(all(feature = "python", feature = "cuda"))]
36use std::sync::Arc;
37use thiserror::Error;
38
39const FOUR_LN_2: f64 = 4.0 * std::f64::consts::LN_2;
40
41#[derive(Debug, Clone)]
42pub enum ParkinsonVolatilityData<'a> {
43 Candles { candles: &'a Candles },
44 Slices { high: &'a [f64], low: &'a [f64] },
45}
46
47#[derive(Debug, Clone)]
48pub struct ParkinsonVolatilityOutput {
49 pub volatility: Vec<f64>,
50 pub variance: Vec<f64>,
51}
52
53#[derive(Debug, Clone)]
54#[cfg_attr(
55 all(target_arch = "wasm32", feature = "wasm"),
56 derive(Serialize, Deserialize)
57)]
58pub struct ParkinsonVolatilityParams {
59 pub period: Option<usize>,
60}
61
62impl Default for ParkinsonVolatilityParams {
63 fn default() -> Self {
64 Self { period: Some(8) }
65 }
66}
67
68#[derive(Debug, Clone)]
69pub struct ParkinsonVolatilityInput<'a> {
70 pub data: ParkinsonVolatilityData<'a>,
71 pub params: ParkinsonVolatilityParams,
72}
73
74impl<'a> ParkinsonVolatilityInput<'a> {
75 #[inline]
76 pub fn from_candles(candles: &'a Candles, params: ParkinsonVolatilityParams) -> Self {
77 Self {
78 data: ParkinsonVolatilityData::Candles { candles },
79 params,
80 }
81 }
82
83 #[inline]
84 pub fn from_slices(high: &'a [f64], low: &'a [f64], params: ParkinsonVolatilityParams) -> Self {
85 Self {
86 data: ParkinsonVolatilityData::Slices { high, low },
87 params,
88 }
89 }
90
91 #[inline]
92 pub fn with_default_candles(candles: &'a Candles) -> Self {
93 Self::from_candles(candles, ParkinsonVolatilityParams::default())
94 }
95
96 #[inline]
97 pub fn get_period(&self) -> usize {
98 self.params.period.unwrap_or(8)
99 }
100
101 #[inline]
102 pub fn as_refs(&'a self) -> Result<(&'a [f64], &'a [f64]), ParkinsonVolatilityError> {
103 match &self.data {
104 ParkinsonVolatilityData::Candles { candles } => {
105 let high = candles
106 .select_candle_field("high")
107 .map_err(|_| ParkinsonVolatilityError::CandleFieldError { field: "high" })?;
108 let low = candles
109 .select_candle_field("low")
110 .map_err(|_| ParkinsonVolatilityError::CandleFieldError { field: "low" })?;
111 Ok((high, low))
112 }
113 ParkinsonVolatilityData::Slices { high, low } => Ok((*high, *low)),
114 }
115 }
116}
117
118#[derive(Copy, Clone, Debug)]
119pub struct ParkinsonVolatilityBuilder {
120 period: Option<usize>,
121 kernel: Kernel,
122}
123
124impl Default for ParkinsonVolatilityBuilder {
125 fn default() -> Self {
126 Self {
127 period: None,
128 kernel: Kernel::Auto,
129 }
130 }
131}
132
133impl ParkinsonVolatilityBuilder {
134 #[inline(always)]
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 #[inline(always)]
140 pub fn period(mut self, n: usize) -> Self {
141 self.period = Some(n);
142 self
143 }
144
145 #[inline(always)]
146 pub fn kernel(mut self, k: Kernel) -> Self {
147 self.kernel = k;
148 self
149 }
150
151 #[inline(always)]
152 pub fn apply(
153 self,
154 candles: &Candles,
155 ) -> Result<ParkinsonVolatilityOutput, ParkinsonVolatilityError> {
156 let params = ParkinsonVolatilityParams {
157 period: self.period,
158 };
159 let input = ParkinsonVolatilityInput::from_candles(candles, params);
160 parkinson_volatility_with_kernel(&input, self.kernel)
161 }
162
163 #[inline(always)]
164 pub fn apply_slices(
165 self,
166 high: &[f64],
167 low: &[f64],
168 ) -> Result<ParkinsonVolatilityOutput, ParkinsonVolatilityError> {
169 let params = ParkinsonVolatilityParams {
170 period: self.period,
171 };
172 let input = ParkinsonVolatilityInput::from_slices(high, low, params);
173 parkinson_volatility_with_kernel(&input, self.kernel)
174 }
175
176 #[inline(always)]
177 pub fn into_stream(self) -> Result<ParkinsonVolatilityStream, ParkinsonVolatilityError> {
178 let params = ParkinsonVolatilityParams {
179 period: self.period,
180 };
181 ParkinsonVolatilityStream::try_new(params)
182 }
183}
184
185#[derive(Debug, Error)]
186pub enum ParkinsonVolatilityError {
187 #[error("parkinson_volatility: Empty input data.")]
188 EmptyInputData,
189 #[error("parkinson_volatility: Data length mismatch between high and low.")]
190 DataLengthMismatch,
191 #[error("parkinson_volatility: Invalid period: period = {period}, data length = {data_len}")]
192 InvalidPeriod { period: usize, data_len: usize },
193 #[error("parkinson_volatility: Not enough valid data: needed = {needed}, valid = {valid}")]
194 NotEnoughValidData { needed: usize, valid: usize },
195 #[error("parkinson_volatility: All values are invalid in high or low.")]
196 AllValuesNaN,
197 #[error("parkinson_volatility: Candle field error: {field}")]
198 CandleFieldError { field: &'static str },
199 #[error("parkinson_volatility: Output length mismatch (expected {expected}, got {got})")]
200 OutputLengthMismatch { expected: usize, got: usize },
201 #[error("parkinson_volatility: invalid input: {0}")]
202 InvalidInput(&'static str),
203 #[error("parkinson_volatility: invalid range: start={start} end={end} step={step}")]
204 InvalidRange {
205 start: usize,
206 end: usize,
207 step: usize,
208 },
209 #[error("parkinson_volatility: invalid kernel for batch path: {0:?}")]
210 InvalidKernelForBatch(Kernel),
211}
212
213#[inline]
214pub fn parkinson_volatility(
215 input: &ParkinsonVolatilityInput,
216) -> Result<ParkinsonVolatilityOutput, ParkinsonVolatilityError> {
217 parkinson_volatility_with_kernel(input, Kernel::Auto)
218}
219
220#[inline(always)]
221fn is_valid_high_low(high: f64, low: f64) -> bool {
222 high.is_finite() && low.is_finite() && high > 0.0 && low > 0.0
223}
224
225#[inline(always)]
226fn first_valid_high_low(high: &[f64], low: &[f64]) -> Option<usize> {
227 high.iter()
228 .zip(low.iter())
229 .position(|(&h, &l)| is_valid_high_low(h, l))
230}
231
232#[inline(always)]
233fn log_range_sq(high: f64, low: f64) -> f64 {
234 let x = (high / low).ln();
235 x * x
236}
237
238#[inline(always)]
239fn outputs_from_sum(sum_log_sq: f64, period: usize) -> (f64, f64) {
240 let variance = ((sum_log_sq / (period as f64)) / FOUR_LN_2).max(0.0);
241 (variance.sqrt(), variance)
242}
243
244#[inline(always)]
245fn parkinson_prepare<'a>(
246 input: &'a ParkinsonVolatilityInput,
247 kernel: Kernel,
248) -> Result<(&'a [f64], &'a [f64], usize, usize, Kernel), ParkinsonVolatilityError> {
249 let (high, low) = input.as_refs()?;
250 if high.is_empty() || low.is_empty() {
251 return Err(ParkinsonVolatilityError::EmptyInputData);
252 }
253 if high.len() != low.len() {
254 return Err(ParkinsonVolatilityError::DataLengthMismatch);
255 }
256
257 let period = input.get_period();
258 if period == 0 || period > high.len() {
259 return Err(ParkinsonVolatilityError::InvalidPeriod {
260 period,
261 data_len: high.len(),
262 });
263 }
264
265 let first = first_valid_high_low(high, low).ok_or(ParkinsonVolatilityError::AllValuesNaN)?;
266 if high.len() - first < period {
267 return Err(ParkinsonVolatilityError::NotEnoughValidData {
268 needed: period,
269 valid: high.len() - first,
270 });
271 }
272
273 let chosen = match kernel {
274 Kernel::Auto => detect_best_kernel(),
275 other => other.to_non_batch(),
276 };
277 Ok((high, low, period, first, chosen))
278}
279
280#[inline(always)]
281fn parkinson_compute_into(
282 high: &[f64],
283 low: &[f64],
284 period: usize,
285 first: usize,
286 out_volatility: &mut [f64],
287 out_variance: &mut [f64],
288) {
289 let warm = first + period - 1;
290 if warm >= high.len() {
291 return;
292 }
293
294 let mut invalid = 0usize;
295 let mut sum_log_sq = 0.0f64;
296
297 for i in first..=warm {
298 if is_valid_high_low(high[i], low[i]) {
299 sum_log_sq += log_range_sq(high[i], low[i]);
300 } else {
301 invalid += 1;
302 }
303 }
304
305 if invalid == 0 {
306 let (vol, var) = outputs_from_sum(sum_log_sq, period);
307 out_volatility[warm] = vol;
308 out_variance[warm] = var;
309 }
310
311 for i in (warm + 1)..high.len() {
312 let old_idx = i - period;
313 if is_valid_high_low(high[old_idx], low[old_idx]) {
314 sum_log_sq -= log_range_sq(high[old_idx], low[old_idx]);
315 } else {
316 invalid -= 1;
317 }
318
319 if is_valid_high_low(high[i], low[i]) {
320 sum_log_sq += log_range_sq(high[i], low[i]);
321 } else {
322 invalid += 1;
323 }
324
325 if invalid == 0 {
326 let (vol, var) = outputs_from_sum(sum_log_sq, period);
327 out_volatility[i] = vol;
328 out_variance[i] = var;
329 }
330 }
331}
332
333#[inline]
334pub fn parkinson_volatility_with_kernel(
335 input: &ParkinsonVolatilityInput,
336 kernel: Kernel,
337) -> Result<ParkinsonVolatilityOutput, ParkinsonVolatilityError> {
338 let (high, low, period, first, _chosen) = parkinson_prepare(input, kernel)?;
339 let warm = first + period - 1;
340 let mut volatility = alloc_with_nan_prefix(high.len(), warm);
341 let mut variance = alloc_with_nan_prefix(high.len(), warm);
342 parkinson_compute_into(high, low, period, first, &mut volatility, &mut variance);
343 Ok(ParkinsonVolatilityOutput {
344 volatility,
345 variance,
346 })
347}
348
349#[inline]
350pub fn parkinson_volatility_into_slice(
351 dst_volatility: &mut [f64],
352 dst_variance: &mut [f64],
353 input: &ParkinsonVolatilityInput,
354 kernel: Kernel,
355) -> Result<(), ParkinsonVolatilityError> {
356 let (high, low, period, first, _chosen) = parkinson_prepare(input, kernel)?;
357 let expected = high.len();
358 if dst_volatility.len() != expected || dst_variance.len() != expected {
359 return Err(ParkinsonVolatilityError::OutputLengthMismatch {
360 expected,
361 got: dst_volatility.len().max(dst_variance.len()),
362 });
363 }
364
365 dst_volatility.fill(f64::NAN);
366 dst_variance.fill(f64::NAN);
367 parkinson_compute_into(high, low, period, first, dst_volatility, dst_variance);
368 Ok(())
369}
370
371#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
372#[inline]
373pub fn parkinson_volatility_into(
374 input: &ParkinsonVolatilityInput,
375 out_volatility: &mut [f64],
376 out_variance: &mut [f64],
377) -> Result<(), ParkinsonVolatilityError> {
378 parkinson_volatility_into_slice(out_volatility, out_variance, input, Kernel::Auto)
379}
380
381#[derive(Debug, Clone)]
382pub struct ParkinsonVolatilityStream {
383 period: usize,
384 buffer: Vec<f64>,
385 head: usize,
386 len: usize,
387 invalid: usize,
388 sum_log_sq: f64,
389}
390
391impl ParkinsonVolatilityStream {
392 #[inline]
393 pub fn try_new(params: ParkinsonVolatilityParams) -> Result<Self, ParkinsonVolatilityError> {
394 let period = params.period.unwrap_or(8);
395 if period == 0 {
396 return Err(ParkinsonVolatilityError::InvalidPeriod {
397 period,
398 data_len: 0,
399 });
400 }
401
402 Ok(Self {
403 period,
404 buffer: vec![f64::NAN; period],
405 head: 0,
406 len: 0,
407 invalid: 0,
408 sum_log_sq: 0.0,
409 })
410 }
411
412 #[inline(always)]
413 pub fn update(&mut self, high: f64, low: f64) -> Option<(f64, f64)> {
414 if self.len == self.period {
415 let old = self.buffer[self.head];
416 if old.is_nan() {
417 self.invalid -= 1;
418 } else {
419 self.sum_log_sq -= old;
420 }
421 }
422
423 let contrib = if is_valid_high_low(high, low) {
424 log_range_sq(high, low)
425 } else {
426 f64::NAN
427 };
428 self.buffer[self.head] = contrib;
429 if contrib.is_nan() {
430 self.invalid += 1;
431 } else {
432 self.sum_log_sq += contrib;
433 }
434
435 self.head += 1;
436 if self.head == self.period {
437 self.head = 0;
438 }
439 if self.len < self.period {
440 self.len += 1;
441 }
442
443 if self.len < self.period {
444 return None;
445 }
446 if self.invalid != 0 {
447 return Some((f64::NAN, f64::NAN));
448 }
449 Some(outputs_from_sum(self.sum_log_sq, self.period))
450 }
451
452 #[inline(always)]
453 pub fn get_warmup_period(&self) -> usize {
454 self.period
455 }
456}
457
458#[derive(Clone, Debug)]
459pub struct ParkinsonVolatilityBatchRange {
460 pub period: (usize, usize, usize),
461}
462
463impl Default for ParkinsonVolatilityBatchRange {
464 fn default() -> Self {
465 Self {
466 period: (8, 256, 1),
467 }
468 }
469}
470
471#[derive(Clone, Debug, Default)]
472pub struct ParkinsonVolatilityBatchBuilder {
473 range: ParkinsonVolatilityBatchRange,
474 kernel: Kernel,
475}
476
477impl ParkinsonVolatilityBatchBuilder {
478 pub fn new() -> Self {
479 Self::default()
480 }
481
482 pub fn kernel(mut self, k: Kernel) -> Self {
483 self.kernel = k;
484 self
485 }
486
487 #[inline]
488 pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
489 self.range.period = (start, end, step);
490 self
491 }
492
493 #[inline]
494 pub fn period_static(mut self, p: usize) -> Self {
495 self.range.period = (p, p, 0);
496 self
497 }
498
499 pub fn apply_slices(
500 self,
501 high: &[f64],
502 low: &[f64],
503 ) -> Result<ParkinsonVolatilityBatchOutput, ParkinsonVolatilityError> {
504 parkinson_volatility_batch_with_kernel(high, low, &self.range, self.kernel)
505 }
506}
507
508#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
509#[derive(Serialize, Deserialize)]
510pub struct ParkinsonVolatilityBatchConfig {
511 pub period_range: Vec<usize>,
512}
513
514#[derive(Clone, Debug)]
515pub struct ParkinsonVolatilityBatchOutput {
516 pub volatility: Vec<f64>,
517 pub variance: Vec<f64>,
518 pub combos: Vec<ParkinsonVolatilityParams>,
519 pub rows: usize,
520 pub cols: usize,
521}
522
523impl ParkinsonVolatilityBatchOutput {
524 pub fn row_for_params(&self, params: &ParkinsonVolatilityParams) -> Option<usize> {
525 self.combos
526 .iter()
527 .position(|c| c.period.unwrap_or(8) == params.period.unwrap_or(8))
528 }
529
530 pub fn volatility_for(&self, params: &ParkinsonVolatilityParams) -> Option<&[f64]> {
531 self.row_for_params(params).and_then(|row| {
532 let start = row * self.cols;
533 self.volatility.get(start..start + self.cols)
534 })
535 }
536
537 pub fn variance_for(&self, params: &ParkinsonVolatilityParams) -> Option<&[f64]> {
538 self.row_for_params(params).and_then(|row| {
539 let start = row * self.cols;
540 self.variance.get(start..start + self.cols)
541 })
542 }
543}
544
545#[inline]
546pub fn expand_grid_parkinson(
547 range: &ParkinsonVolatilityBatchRange,
548) -> Result<Vec<ParkinsonVolatilityParams>, ParkinsonVolatilityError> {
549 fn axis_usize(
550 (start, end, step): (usize, usize, usize),
551 ) -> Result<Vec<usize>, ParkinsonVolatilityError> {
552 if step == 0 || start == end {
553 return Ok(vec![start]);
554 }
555 if start < end {
556 let mut values = Vec::new();
557 let mut x = start;
558 while x <= end {
559 values.push(x);
560 match x.checked_add(step) {
561 Some(next) if next > x => x = next,
562 _ => break,
563 }
564 }
565 if values.is_empty() {
566 return Err(ParkinsonVolatilityError::InvalidRange { start, end, step });
567 }
568 Ok(values)
569 } else {
570 let mut values = Vec::new();
571 let st = step.max(1);
572 let mut x = start;
573 while x >= end {
574 values.push(x);
575 if x == end {
576 break;
577 }
578 let next = x.saturating_sub(st);
579 if next == x || next < end {
580 break;
581 }
582 x = next;
583 }
584 if values.is_empty() {
585 return Err(ParkinsonVolatilityError::InvalidRange { start, end, step });
586 }
587 Ok(values)
588 }
589 }
590
591 Ok(axis_usize(range.period)?
592 .into_iter()
593 .map(|period| ParkinsonVolatilityParams {
594 period: Some(period),
595 })
596 .collect())
597}
598
599#[inline]
600pub fn parkinson_volatility_batch_with_kernel(
601 high: &[f64],
602 low: &[f64],
603 sweep: &ParkinsonVolatilityBatchRange,
604 kernel: Kernel,
605) -> Result<ParkinsonVolatilityBatchOutput, ParkinsonVolatilityError> {
606 let batch = match kernel {
607 Kernel::Auto => detect_best_batch_kernel(),
608 other if other.is_batch() => other,
609 other => return Err(ParkinsonVolatilityError::InvalidKernelForBatch(other)),
610 };
611 parkinson_volatility_batch_par_slice(high, low, sweep, batch.to_non_batch())
612}
613
614#[inline(always)]
615pub fn parkinson_volatility_batch_slice(
616 high: &[f64],
617 low: &[f64],
618 sweep: &ParkinsonVolatilityBatchRange,
619 kernel: Kernel,
620) -> Result<ParkinsonVolatilityBatchOutput, ParkinsonVolatilityError> {
621 parkinson_volatility_batch_inner(high, low, sweep, kernel, false)
622}
623
624#[inline(always)]
625pub fn parkinson_volatility_batch_par_slice(
626 high: &[f64],
627 low: &[f64],
628 sweep: &ParkinsonVolatilityBatchRange,
629 kernel: Kernel,
630) -> Result<ParkinsonVolatilityBatchOutput, ParkinsonVolatilityError> {
631 parkinson_volatility_batch_inner(high, low, sweep, kernel, true)
632}
633
634#[inline(always)]
635fn parkinson_volatility_batch_inner(
636 high: &[f64],
637 low: &[f64],
638 sweep: &ParkinsonVolatilityBatchRange,
639 _kernel: Kernel,
640 parallel: bool,
641) -> Result<ParkinsonVolatilityBatchOutput, ParkinsonVolatilityError> {
642 let combos = expand_grid_parkinson(sweep)?;
643 if high.is_empty() || low.is_empty() {
644 return Err(ParkinsonVolatilityError::EmptyInputData);
645 }
646 if high.len() != low.len() {
647 return Err(ParkinsonVolatilityError::DataLengthMismatch);
648 }
649
650 let first = first_valid_high_low(high, low).ok_or(ParkinsonVolatilityError::AllValuesNaN)?;
651 let max_period = combos
652 .iter()
653 .map(|c| c.period.unwrap_or(8))
654 .max()
655 .unwrap_or(0);
656 if max_period == 0 || high.len() - first < max_period {
657 return Err(ParkinsonVolatilityError::NotEnoughValidData {
658 needed: max_period,
659 valid: high.len() - first,
660 });
661 }
662
663 let rows = combos.len();
664 let cols = high.len();
665 let mut volatility_mu = make_uninit_matrix(rows, cols);
666 let mut variance_mu = make_uninit_matrix(rows, cols);
667 let warmups: Vec<usize> = combos
668 .iter()
669 .map(|c| first + c.period.unwrap_or(8) - 1)
670 .collect();
671 init_matrix_prefixes(&mut volatility_mu, cols, &warmups);
672 init_matrix_prefixes(&mut variance_mu, cols, &warmups);
673
674 let mut volatility_guard = ManuallyDrop::new(volatility_mu);
675 let mut variance_guard = ManuallyDrop::new(variance_mu);
676 let volatility = unsafe {
677 core::slice::from_raw_parts_mut(
678 volatility_guard.as_mut_ptr() as *mut f64,
679 volatility_guard.len(),
680 )
681 };
682 let variance = unsafe {
683 core::slice::from_raw_parts_mut(
684 variance_guard.as_mut_ptr() as *mut f64,
685 variance_guard.len(),
686 )
687 };
688
689 parkinson_volatility_batch_inner_into(
690 high,
691 low,
692 sweep,
693 Kernel::Scalar,
694 parallel,
695 volatility,
696 variance,
697 )?;
698
699 let volatility_values = unsafe {
700 Vec::from_raw_parts(
701 volatility_guard.as_mut_ptr() as *mut f64,
702 volatility_guard.len(),
703 volatility_guard.capacity(),
704 )
705 };
706 let variance_values = unsafe {
707 Vec::from_raw_parts(
708 variance_guard.as_mut_ptr() as *mut f64,
709 variance_guard.len(),
710 variance_guard.capacity(),
711 )
712 };
713
714 Ok(ParkinsonVolatilityBatchOutput {
715 volatility: volatility_values,
716 variance: variance_values,
717 combos,
718 rows,
719 cols,
720 })
721}
722
723#[inline(always)]
724fn parkinson_volatility_batch_inner_into(
725 high: &[f64],
726 low: &[f64],
727 sweep: &ParkinsonVolatilityBatchRange,
728 _kernel: Kernel,
729 parallel: bool,
730 out_volatility: &mut [f64],
731 out_variance: &mut [f64],
732) -> Result<Vec<ParkinsonVolatilityParams>, ParkinsonVolatilityError> {
733 let combos = expand_grid_parkinson(sweep)?;
734 if high.is_empty() || low.is_empty() {
735 return Err(ParkinsonVolatilityError::EmptyInputData);
736 }
737 if high.len() != low.len() {
738 return Err(ParkinsonVolatilityError::DataLengthMismatch);
739 }
740
741 let first = first_valid_high_low(high, low).ok_or(ParkinsonVolatilityError::AllValuesNaN)?;
742 let max_period = combos
743 .iter()
744 .map(|c| c.period.unwrap_or(8))
745 .max()
746 .unwrap_or(0);
747 if max_period == 0 || high.len() - first < max_period {
748 return Err(ParkinsonVolatilityError::NotEnoughValidData {
749 needed: max_period,
750 valid: high.len() - first,
751 });
752 }
753
754 let rows = combos.len();
755 let cols = high.len();
756 let total = rows
757 .checked_mul(cols)
758 .ok_or(ParkinsonVolatilityError::InvalidInput("rows*cols overflow"))?;
759 if out_volatility.len() != total || out_variance.len() != total {
760 return Err(ParkinsonVolatilityError::OutputLengthMismatch {
761 expected: total,
762 got: out_volatility.len().max(out_variance.len()),
763 });
764 }
765
766 let vol_mu = unsafe {
767 core::slice::from_raw_parts_mut(out_volatility.as_mut_ptr() as *mut MaybeUninit<f64>, total)
768 };
769 let var_mu = unsafe {
770 core::slice::from_raw_parts_mut(out_variance.as_mut_ptr() as *mut MaybeUninit<f64>, total)
771 };
772 let warmups: Vec<usize> = combos
773 .iter()
774 .map(|c| first + c.period.unwrap_or(8) - 1)
775 .collect();
776 init_matrix_prefixes(vol_mu, cols, &warmups);
777 init_matrix_prefixes(var_mu, cols, &warmups);
778
779 let n = high.len();
780 let mut prefix_sum = vec![0.0f64; n + 1];
781 let mut prefix_invalid = vec![0i32; n + 1];
782 for i in 0..n {
783 if is_valid_high_low(high[i], low[i]) {
784 prefix_sum[i + 1] = prefix_sum[i] + log_range_sq(high[i], low[i]);
785 prefix_invalid[i + 1] = prefix_invalid[i];
786 } else {
787 prefix_sum[i + 1] = prefix_sum[i];
788 prefix_invalid[i + 1] = prefix_invalid[i] + 1;
789 }
790 }
791
792 let do_row = |row: usize, vol_row: &mut [f64], var_row: &mut [f64]| {
793 let period = combos[row].period.unwrap_or(8);
794 let warm = first + period - 1;
795 for i in warm..n {
796 let end = i + 1;
797 let start = end - period;
798 if prefix_invalid[end] - prefix_invalid[start] != 0 {
799 vol_row[i] = f64::NAN;
800 var_row[i] = f64::NAN;
801 } else {
802 let sum = prefix_sum[end] - prefix_sum[start];
803 let (vol, var) = outputs_from_sum(sum, period);
804 vol_row[i] = vol;
805 var_row[i] = var;
806 }
807 }
808 };
809
810 if parallel {
811 #[cfg(not(target_arch = "wasm32"))]
812 {
813 out_volatility
814 .par_chunks_mut(cols)
815 .zip(out_variance.par_chunks_mut(cols))
816 .enumerate()
817 .for_each(|(row, (vol, var))| do_row(row, vol, var));
818 }
819 #[cfg(target_arch = "wasm32")]
820 {
821 for (row, (vol, var)) in out_volatility
822 .chunks_mut(cols)
823 .zip(out_variance.chunks_mut(cols))
824 .enumerate()
825 {
826 do_row(row, vol, var);
827 }
828 }
829 } else {
830 for (row, (vol, var)) in out_volatility
831 .chunks_mut(cols)
832 .zip(out_variance.chunks_mut(cols))
833 .enumerate()
834 {
835 do_row(row, vol, var);
836 }
837 }
838
839 Ok(combos)
840}
841
842#[cfg(feature = "python")]
843#[pyfunction(name = "parkinson_volatility")]
844#[pyo3(signature = (high, low, period, kernel=None))]
845pub fn parkinson_volatility_py<'py>(
846 py: Python<'py>,
847 high: PyReadonlyArray1<'py, f64>,
848 low: PyReadonlyArray1<'py, f64>,
849 period: usize,
850 kernel: Option<&str>,
851) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
852 let high = high.as_slice()?;
853 let low = low.as_slice()?;
854 let kernel = validate_kernel(kernel, false)?;
855 let params = ParkinsonVolatilityParams {
856 period: Some(period),
857 };
858 let input = ParkinsonVolatilityInput::from_slices(high, low, params);
859 let output = py
860 .allow_threads(|| parkinson_volatility_with_kernel(&input, kernel))
861 .map_err(|e| PyValueError::new_err(e.to_string()))?;
862 Ok((
863 output.volatility.into_pyarray(py),
864 output.variance.into_pyarray(py),
865 ))
866}
867
868#[cfg(feature = "python")]
869#[pyclass(name = "ParkinsonVolatilityStream")]
870pub struct ParkinsonVolatilityStreamPy {
871 stream: ParkinsonVolatilityStream,
872}
873
874#[cfg(feature = "python")]
875#[pymethods]
876impl ParkinsonVolatilityStreamPy {
877 #[new]
878 fn new(period: usize) -> PyResult<Self> {
879 let params = ParkinsonVolatilityParams {
880 period: Some(period),
881 };
882 let stream = ParkinsonVolatilityStream::try_new(params)
883 .map_err(|e| PyValueError::new_err(e.to_string()))?;
884 Ok(Self { stream })
885 }
886
887 fn update(&mut self, high: f64, low: f64) -> Option<(f64, f64)> {
888 self.stream.update(high, low)
889 }
890}
891
892#[cfg(feature = "python")]
893#[pyfunction(name = "parkinson_volatility_batch")]
894#[pyo3(signature = (high, low, period_range, kernel=None))]
895pub fn parkinson_volatility_batch_py<'py>(
896 py: Python<'py>,
897 high: PyReadonlyArray1<'py, f64>,
898 low: PyReadonlyArray1<'py, f64>,
899 period_range: (usize, usize, usize),
900 kernel: Option<&str>,
901) -> PyResult<Bound<'py, PyDict>> {
902 let high = high.as_slice()?;
903 let low = low.as_slice()?;
904 let sweep = ParkinsonVolatilityBatchRange {
905 period: period_range,
906 };
907 let combos = expand_grid_parkinson(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
908 let rows = combos.len();
909 let cols = high.len();
910 let total = rows
911 .checked_mul(cols)
912 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
913 let volatility_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
914 let variance_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
915 let volatility_out = unsafe { volatility_arr.as_slice_mut()? };
916 let variance_out = unsafe { variance_arr.as_slice_mut()? };
917 let kernel = validate_kernel(kernel, true)?;
918
919 py.allow_threads(|| {
920 let batch_kernel = match kernel {
921 Kernel::Auto => detect_best_batch_kernel(),
922 other => other,
923 };
924 parkinson_volatility_batch_inner_into(
925 high,
926 low,
927 &sweep,
928 batch_kernel.to_non_batch(),
929 true,
930 volatility_out,
931 variance_out,
932 )
933 })
934 .map_err(|e| PyValueError::new_err(e.to_string()))?;
935
936 let dict = PyDict::new(py);
937 dict.set_item("volatility", volatility_arr.reshape((rows, cols))?)?;
938 dict.set_item("variance", variance_arr.reshape((rows, cols))?)?;
939 dict.set_item(
940 "periods",
941 combos
942 .iter()
943 .map(|p| p.period.unwrap_or(8) as u64)
944 .collect::<Vec<_>>()
945 .into_pyarray(py),
946 )?;
947 dict.set_item("rows", rows)?;
948 dict.set_item("cols", cols)?;
949 Ok(dict)
950}
951
952#[cfg(all(feature = "python", feature = "cuda"))]
953#[pyclass(
954 module = "ta_indicators.cuda",
955 name = "ParkinsonVolatilityDeviceArrayF32",
956 unsendable
957)]
958pub struct ParkinsonVolatilityDeviceArrayF32Py {
959 pub(crate) buf: Option<DeviceBuffer<f32>>,
960 pub(crate) rows: usize,
961 pub(crate) cols: usize,
962 pub(crate) ctx: Arc<Context>,
963 pub(crate) device_id: u32,
964}
965
966#[cfg(all(feature = "python", feature = "cuda"))]
967#[pymethods]
968impl ParkinsonVolatilityDeviceArrayF32Py {
969 #[getter]
970 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
971 let d = PyDict::new(py);
972 d.set_item("shape", (self.rows, self.cols))?;
973 d.set_item("typestr", "<f4")?;
974 let row_stride = self
975 .cols
976 .checked_mul(std::mem::size_of::<f32>())
977 .ok_or_else(|| PyValueError::new_err("stride overflow in __cuda_array_interface__"))?;
978 d.set_item("strides", (row_stride, std::mem::size_of::<f32>()))?;
979 let buf = self
980 .buf
981 .as_ref()
982 .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
983 let ptr = buf.as_device_ptr().as_raw() as usize;
984 d.set_item("data", (ptr, false))?;
985 d.set_item("version", 3)?;
986 Ok(d)
987 }
988
989 fn __dlpack_device__(&self) -> (i32, i32) {
990 (2, self.device_id as i32)
991 }
992
993 #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
994 fn __dlpack__<'py>(
995 &mut self,
996 py: Python<'py>,
997 stream: Option<PyObject>,
998 max_version: Option<(u8, u8)>,
999 dl_device: Option<(i32, i32)>,
1000 copy: Option<bool>,
1001 ) -> PyResult<PyObject> {
1002 let _ = stream;
1003 let _ = max_version;
1004 let _ = &self.ctx;
1005 if let Some((_ty, dev)) = dl_device {
1006 if dev != self.device_id as i32 {
1007 return Err(PyValueError::new_err("dlpack device mismatch"));
1008 }
1009 }
1010 if matches!(copy, Some(true)) {
1011 return Err(PyValueError::new_err(
1012 "copy=True not supported for ParkinsonVolatilityDeviceArrayF32",
1013 ));
1014 }
1015
1016 let buf = self
1017 .buf
1018 .take()
1019 .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
1020 export_f32_cuda_dlpack_2d(py, buf, self.rows, self.cols, self.device_id as i32, None)
1021 }
1022}
1023
1024#[cfg(all(feature = "python", feature = "cuda"))]
1025#[pyfunction(name = "parkinson_volatility_cuda_batch_dev")]
1026#[pyo3(signature = (high_f32, low_f32, period_range, device_id=0))]
1027pub fn parkinson_volatility_cuda_batch_dev_py<'py>(
1028 py: Python<'py>,
1029 high_f32: PyReadonlyArray1<'py, f32>,
1030 low_f32: PyReadonlyArray1<'py, f32>,
1031 period_range: (usize, usize, usize),
1032 device_id: usize,
1033) -> PyResult<Bound<'py, PyDict>> {
1034 if !crate::cuda::cuda_available() {
1035 return Err(PyValueError::new_err("CUDA not available"));
1036 }
1037 let high = high_f32.as_slice()?;
1038 let low = low_f32.as_slice()?;
1039 let sweep = ParkinsonVolatilityBatchRange {
1040 period: period_range,
1041 };
1042 let (result, ctx, dev_id) = py.allow_threads(|| -> PyResult<_> {
1043 let cuda = crate::cuda::CudaParkinsonVolatility::new(device_id)
1044 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1045 let result = cuda
1046 .parkinson_volatility_batch_dev(high, low, &sweep)
1047 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1048 Ok((result, cuda.context_arc(), cuda.device_id()))
1049 })?;
1050
1051 let rows = result.outputs.rows();
1052 let cols = result.outputs.cols();
1053 let dict = PyDict::new(py);
1054 dict.set_item(
1055 "volatility",
1056 Py::new(
1057 py,
1058 ParkinsonVolatilityDeviceArrayF32Py {
1059 buf: Some(result.outputs.volatility.buf),
1060 rows,
1061 cols,
1062 ctx: ctx.clone(),
1063 device_id: dev_id,
1064 },
1065 )?,
1066 )?;
1067 dict.set_item(
1068 "variance",
1069 Py::new(
1070 py,
1071 ParkinsonVolatilityDeviceArrayF32Py {
1072 buf: Some(result.outputs.variance.buf),
1073 rows,
1074 cols,
1075 ctx,
1076 device_id: dev_id,
1077 },
1078 )?,
1079 )?;
1080 dict.set_item(
1081 "periods",
1082 result
1083 .combos
1084 .iter()
1085 .map(|p| p.period.unwrap_or(8) as u64)
1086 .collect::<Vec<_>>()
1087 .into_pyarray(py),
1088 )?;
1089 dict.set_item("rows", rows)?;
1090 dict.set_item("cols", cols)?;
1091 Ok(dict)
1092}
1093
1094#[cfg(all(feature = "python", feature = "cuda"))]
1095#[pyfunction(name = "parkinson_volatility_cuda_many_series_one_param_dev")]
1096#[pyo3(signature = (high_tm_f32, low_tm_f32, period, device_id=0))]
1097pub fn parkinson_volatility_cuda_many_series_one_param_dev_py<'py>(
1098 py: Python<'py>,
1099 high_tm_f32: PyReadonlyArray2<'py, f32>,
1100 low_tm_f32: PyReadonlyArray2<'py, f32>,
1101 period: usize,
1102 device_id: usize,
1103) -> PyResult<Bound<'py, PyDict>> {
1104 if !crate::cuda::cuda_available() {
1105 return Err(PyValueError::new_err("CUDA not available"));
1106 }
1107 let sh = high_tm_f32.shape();
1108 let sl = low_tm_f32.shape();
1109 if sh.len() != 2 || sl.len() != 2 || sh != sl {
1110 return Err(PyValueError::new_err(
1111 "expected 2D arrays with identical shape",
1112 ));
1113 }
1114 let rows = sh[0];
1115 let cols = sh[1];
1116 let high = high_tm_f32.as_slice()?;
1117 let low = low_tm_f32.as_slice()?;
1118 let (outputs, ctx, dev_id) = py.allow_threads(|| -> PyResult<_> {
1119 let cuda = crate::cuda::CudaParkinsonVolatility::new(device_id)
1120 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1121 let outputs = cuda
1122 .parkinson_volatility_many_series_one_param_time_major_dev(
1123 high, low, cols, rows, period,
1124 )
1125 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1126 Ok((outputs, cuda.context_arc(), cuda.device_id()))
1127 })?;
1128 let dict = PyDict::new(py);
1129 dict.set_item(
1130 "volatility",
1131 Py::new(
1132 py,
1133 ParkinsonVolatilityDeviceArrayF32Py {
1134 buf: Some(outputs.volatility.buf),
1135 rows,
1136 cols,
1137 ctx: ctx.clone(),
1138 device_id: dev_id,
1139 },
1140 )?,
1141 )?;
1142 dict.set_item(
1143 "variance",
1144 Py::new(
1145 py,
1146 ParkinsonVolatilityDeviceArrayF32Py {
1147 buf: Some(outputs.variance.buf),
1148 rows,
1149 cols,
1150 ctx,
1151 device_id: dev_id,
1152 },
1153 )?,
1154 )?;
1155 dict.set_item("rows", rows)?;
1156 dict.set_item("cols", cols)?;
1157 Ok(dict)
1158}
1159
1160#[cfg(feature = "python")]
1161pub fn register_parkinson_volatility_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1162 m.add_function(wrap_pyfunction!(parkinson_volatility_py, m)?)?;
1163 m.add_function(wrap_pyfunction!(parkinson_volatility_batch_py, m)?)?;
1164 m.add_class::<ParkinsonVolatilityStreamPy>()?;
1165 #[cfg(feature = "cuda")]
1166 {
1167 m.add_class::<ParkinsonVolatilityDeviceArrayF32Py>()?;
1168 m.add_function(wrap_pyfunction!(parkinson_volatility_cuda_batch_dev_py, m)?)?;
1169 m.add_function(wrap_pyfunction!(
1170 parkinson_volatility_cuda_many_series_one_param_dev_py,
1171 m
1172 )?)?;
1173 }
1174 Ok(())
1175}
1176
1177#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1178#[wasm_bindgen(js_name = "parkinson_volatility_js")]
1179pub fn parkinson_volatility_js(
1180 high: &[f64],
1181 low: &[f64],
1182 period: usize,
1183) -> Result<JsValue, JsValue> {
1184 if high.len() != low.len() {
1185 return Err(JsValue::from_str("high/low slice length mismatch"));
1186 }
1187
1188 let params = ParkinsonVolatilityParams {
1189 period: Some(period),
1190 };
1191 let input = ParkinsonVolatilityInput::from_slices(high, low, params);
1192 let mut volatility = vec![0.0; high.len()];
1193 let mut variance = vec![0.0; high.len()];
1194 parkinson_volatility_into_slice(&mut volatility, &mut variance, &input, Kernel::Auto)
1195 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1196
1197 let obj = js_sys::Object::new();
1198 js_sys::Reflect::set(
1199 &obj,
1200 &JsValue::from_str("volatility"),
1201 &serde_wasm_bindgen::to_value(&volatility).unwrap(),
1202 )?;
1203 js_sys::Reflect::set(
1204 &obj,
1205 &JsValue::from_str("variance"),
1206 &serde_wasm_bindgen::to_value(&variance).unwrap(),
1207 )?;
1208 Ok(obj.into())
1209}
1210
1211#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1212#[wasm_bindgen(js_name = "parkinson_volatility_batch_js")]
1213pub fn parkinson_volatility_batch_js(
1214 high: &[f64],
1215 low: &[f64],
1216 config: JsValue,
1217) -> Result<JsValue, JsValue> {
1218 if high.len() != low.len() {
1219 return Err(JsValue::from_str("high/low slice length mismatch"));
1220 }
1221 let config: ParkinsonVolatilityBatchConfig = serde_wasm_bindgen::from_value(config)
1222 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1223 if config.period_range.len() != 3 {
1224 return Err(JsValue::from_str(
1225 "Invalid config: period_range must have exactly 3 elements [start, end, step]",
1226 ));
1227 }
1228
1229 let sweep = ParkinsonVolatilityBatchRange {
1230 period: (
1231 config.period_range[0],
1232 config.period_range[1],
1233 config.period_range[2],
1234 ),
1235 };
1236 let combos = expand_grid_parkinson(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1237 let rows = combos.len();
1238 let cols = high.len();
1239 let total = rows
1240 .checked_mul(cols)
1241 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1242 let mut volatility = vec![0.0; total];
1243 let mut variance = vec![0.0; total];
1244 parkinson_volatility_batch_inner_into(
1245 high,
1246 low,
1247 &sweep,
1248 Kernel::Scalar,
1249 false,
1250 &mut volatility,
1251 &mut variance,
1252 )
1253 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1254
1255 let obj = js_sys::Object::new();
1256 js_sys::Reflect::set(
1257 &obj,
1258 &JsValue::from_str("volatility"),
1259 &serde_wasm_bindgen::to_value(&volatility).unwrap(),
1260 )?;
1261 js_sys::Reflect::set(
1262 &obj,
1263 &JsValue::from_str("variance"),
1264 &serde_wasm_bindgen::to_value(&variance).unwrap(),
1265 )?;
1266 js_sys::Reflect::set(
1267 &obj,
1268 &JsValue::from_str("rows"),
1269 &JsValue::from_f64(rows as f64),
1270 )?;
1271 js_sys::Reflect::set(
1272 &obj,
1273 &JsValue::from_str("cols"),
1274 &JsValue::from_f64(cols as f64),
1275 )?;
1276 js_sys::Reflect::set(
1277 &obj,
1278 &JsValue::from_str("combos"),
1279 &serde_wasm_bindgen::to_value(&combos).unwrap(),
1280 )?;
1281 Ok(obj.into())
1282}
1283
1284#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1285#[wasm_bindgen]
1286pub fn parkinson_volatility_alloc(len: usize) -> *mut f64 {
1287 let mut v = Vec::<f64>::with_capacity(2 * len);
1288 let ptr = v.as_mut_ptr();
1289 std::mem::forget(v);
1290 ptr
1291}
1292
1293#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1294#[wasm_bindgen]
1295pub fn parkinson_volatility_free(ptr: *mut f64, len: usize) {
1296 unsafe {
1297 let _ = Vec::from_raw_parts(ptr, 2 * len, 2 * len);
1298 }
1299}
1300
1301#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1302#[wasm_bindgen]
1303pub fn parkinson_volatility_into(
1304 high_ptr: *const f64,
1305 low_ptr: *const f64,
1306 out_ptr: *mut f64,
1307 len: usize,
1308 period: usize,
1309) -> Result<(), JsValue> {
1310 if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
1311 return Err(JsValue::from_str(
1312 "null pointer passed to parkinson_volatility_into",
1313 ));
1314 }
1315
1316 unsafe {
1317 let high = std::slice::from_raw_parts(high_ptr, len);
1318 let low = std::slice::from_raw_parts(low_ptr, len);
1319 let out = std::slice::from_raw_parts_mut(out_ptr, 2 * len);
1320 let (volatility, variance) = out.split_at_mut(len);
1321 let params = ParkinsonVolatilityParams {
1322 period: Some(period),
1323 };
1324 let input = ParkinsonVolatilityInput::from_slices(high, low, params);
1325 parkinson_volatility_into_slice(volatility, variance, &input, Kernel::Auto)
1326 .map_err(|e| JsValue::from_str(&e.to_string()))
1327 }
1328}
1329
1330#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1331#[wasm_bindgen(js_name = "parkinson_volatility_into_host")]
1332pub fn parkinson_volatility_into_host(
1333 high: &[f64],
1334 low: &[f64],
1335 out_ptr: *mut f64,
1336 period: usize,
1337) -> Result<(), JsValue> {
1338 if out_ptr.is_null() {
1339 return Err(JsValue::from_str(
1340 "null pointer passed to parkinson_volatility_into_host",
1341 ));
1342 }
1343 if high.len() != low.len() {
1344 return Err(JsValue::from_str("high/low slice length mismatch"));
1345 }
1346
1347 unsafe {
1348 let out = std::slice::from_raw_parts_mut(out_ptr, 2 * high.len());
1349 let (volatility, variance) = out.split_at_mut(high.len());
1350 let params = ParkinsonVolatilityParams {
1351 period: Some(period),
1352 };
1353 let input = ParkinsonVolatilityInput::from_slices(high, low, params);
1354 parkinson_volatility_into_slice(volatility, variance, &input, Kernel::Auto)
1355 .map_err(|e| JsValue::from_str(&e.to_string()))
1356 }
1357}
1358
1359#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1360#[wasm_bindgen]
1361pub fn parkinson_volatility_batch_into(
1362 high_ptr: *const f64,
1363 low_ptr: *const f64,
1364 volatility_ptr: *mut f64,
1365 variance_ptr: *mut f64,
1366 len: usize,
1367 period_start: usize,
1368 period_end: usize,
1369 period_step: usize,
1370) -> Result<usize, JsValue> {
1371 if high_ptr.is_null() || low_ptr.is_null() || volatility_ptr.is_null() || variance_ptr.is_null()
1372 {
1373 return Err(JsValue::from_str(
1374 "null pointer passed to parkinson_volatility_batch_into",
1375 ));
1376 }
1377
1378 unsafe {
1379 let high = std::slice::from_raw_parts(high_ptr, len);
1380 let low = std::slice::from_raw_parts(low_ptr, len);
1381 let sweep = ParkinsonVolatilityBatchRange {
1382 period: (period_start, period_end, period_step),
1383 };
1384 let combos =
1385 expand_grid_parkinson(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1386 let rows = combos.len();
1387 let total = rows
1388 .checked_mul(len)
1389 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1390 let volatility = std::slice::from_raw_parts_mut(volatility_ptr, total);
1391 let variance = std::slice::from_raw_parts_mut(variance_ptr, total);
1392 parkinson_volatility_batch_inner_into(
1393 high,
1394 low,
1395 &sweep,
1396 Kernel::Scalar,
1397 false,
1398 volatility,
1399 variance,
1400 )
1401 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1402 Ok(rows)
1403 }
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408 use super::*;
1409
1410 fn vecs_match(a: &[f64], b: &[f64]) -> bool {
1411 a.len() == b.len()
1412 && a.iter().zip(b.iter()).all(|(&x, &y)| {
1413 (x.is_nan() && y.is_nan()) || (!x.is_nan() && !y.is_nan() && (x - y).abs() < 1e-12)
1414 })
1415 }
1416
1417 fn sample_high_low() -> (Vec<f64>, Vec<f64>) {
1418 let high = vec![10.0, 10.4, 10.6, 10.8, 10.7, 11.0, 11.2, 11.4];
1419 let low = vec![9.6, 10.0, 10.1, 10.2, 10.1, 10.5, 10.8, 11.0];
1420 (high, low)
1421 }
1422
1423 #[test]
1424 fn parkinson_output_contract() {
1425 let (high, low) = sample_high_low();
1426 let input = ParkinsonVolatilityInput::from_slices(
1427 &high,
1428 &low,
1429 ParkinsonVolatilityParams { period: Some(3) },
1430 );
1431 let out = parkinson_volatility(&input).expect("parkinson output");
1432 assert_eq!(out.volatility.len(), high.len());
1433 assert_eq!(out.variance.len(), high.len());
1434 assert!(out.volatility[..2].iter().all(|v| v.is_nan()));
1435 assert!(out.variance[..2].iter().all(|v| v.is_nan()));
1436 assert!(out.volatility[2].is_finite());
1437 assert!(out.variance[2].is_finite());
1438 assert!((out.volatility[2] * out.volatility[2] - out.variance[2]).abs() < 1e-12);
1439 }
1440
1441 #[test]
1442 fn parkinson_into_matches_api() {
1443 let (high, low) = sample_high_low();
1444 let input = ParkinsonVolatilityInput::from_slices(
1445 &high,
1446 &low,
1447 ParkinsonVolatilityParams { period: Some(4) },
1448 );
1449 let direct = parkinson_volatility(&input).expect("direct output");
1450 let mut volatility = vec![0.0; high.len()];
1451 let mut variance = vec![0.0; high.len()];
1452 parkinson_volatility_into(&input, &mut volatility, &mut variance).expect("into output");
1453 assert!(vecs_match(&direct.volatility, &volatility));
1454 assert!(vecs_match(&direct.variance, &variance));
1455 }
1456
1457 #[test]
1458 fn parkinson_stream_matches_batch() {
1459 let (high, low) = sample_high_low();
1460 let input = ParkinsonVolatilityInput::from_slices(
1461 &high,
1462 &low,
1463 ParkinsonVolatilityParams { period: Some(3) },
1464 );
1465 let batch = parkinson_volatility(&input).expect("batch output");
1466 let mut stream =
1467 ParkinsonVolatilityStream::try_new(ParkinsonVolatilityParams { period: Some(3) })
1468 .expect("stream");
1469 let mut stream_volatility = Vec::new();
1470 let mut stream_variance = Vec::new();
1471 for (&h, &l) in high.iter().zip(low.iter()) {
1472 match stream.update(h, l) {
1473 Some((vol, var)) => {
1474 stream_volatility.push(vol);
1475 stream_variance.push(var);
1476 }
1477 None => {
1478 stream_volatility.push(f64::NAN);
1479 stream_variance.push(f64::NAN);
1480 }
1481 }
1482 }
1483 assert!(vecs_match(&stream_volatility, &batch.volatility));
1484 assert!(vecs_match(&stream_variance, &batch.variance));
1485 }
1486
1487 #[test]
1488 fn parkinson_batch_single_param_matches_single() {
1489 let (high, low) = sample_high_low();
1490 let sweep = ParkinsonVolatilityBatchRange { period: (3, 3, 0) };
1491 let batch = parkinson_volatility_batch_with_kernel(&high, &low, &sweep, Kernel::Auto)
1492 .expect("batch output");
1493 let input = ParkinsonVolatilityInput::from_slices(
1494 &high,
1495 &low,
1496 ParkinsonVolatilityParams { period: Some(3) },
1497 );
1498 let single = parkinson_volatility(&input).expect("single output");
1499 assert_eq!(batch.rows, 1);
1500 assert_eq!(batch.cols, high.len());
1501 assert!(vecs_match(&batch.volatility, &single.volatility));
1502 assert!(vecs_match(&batch.variance, &single.variance));
1503 }
1504
1505 #[test]
1506 fn parkinson_rejects_invalid_period() {
1507 let (high, low) = sample_high_low();
1508 let input = ParkinsonVolatilityInput::from_slices(
1509 &high,
1510 &low,
1511 ParkinsonVolatilityParams { period: Some(0) },
1512 );
1513 let err = parkinson_volatility(&input).expect_err("invalid period should fail");
1514 assert!(matches!(
1515 err,
1516 ParkinsonVolatilityError::InvalidPeriod { .. }
1517 ));
1518 }
1519}