1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::convert::AsRef;
26use std::mem::ManuallyDrop;
27use thiserror::Error;
28
29impl<'a> AsRef<[f64]> for HistoricalVolatilityInput<'a> {
30 #[inline(always)]
31 fn as_ref(&self) -> &[f64] {
32 match &self.data {
33 HistoricalVolatilityData::Slice(slice) => slice,
34 HistoricalVolatilityData::Candles { candles, source } => source_type(candles, source),
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
40pub enum HistoricalVolatilityData<'a> {
41 Candles {
42 candles: &'a Candles,
43 source: &'a str,
44 },
45 Slice(&'a [f64]),
46}
47
48#[derive(Debug, Clone)]
49pub struct HistoricalVolatilityOutput {
50 pub values: 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 HistoricalVolatilityParams {
59 pub lookback: Option<usize>,
60 pub annualization_days: Option<f64>,
61}
62
63impl Default for HistoricalVolatilityParams {
64 fn default() -> Self {
65 Self {
66 lookback: Some(20),
67 annualization_days: Some(250.0),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
73pub struct HistoricalVolatilityInput<'a> {
74 pub data: HistoricalVolatilityData<'a>,
75 pub params: HistoricalVolatilityParams,
76}
77
78impl<'a> HistoricalVolatilityInput<'a> {
79 #[inline]
80 pub fn from_candles(
81 candles: &'a Candles,
82 source: &'a str,
83 params: HistoricalVolatilityParams,
84 ) -> Self {
85 Self {
86 data: HistoricalVolatilityData::Candles { candles, source },
87 params,
88 }
89 }
90
91 #[inline]
92 pub fn from_slice(slice: &'a [f64], params: HistoricalVolatilityParams) -> Self {
93 Self {
94 data: HistoricalVolatilityData::Slice(slice),
95 params,
96 }
97 }
98
99 #[inline]
100 pub fn with_default_candles(candles: &'a Candles) -> Self {
101 Self::from_candles(candles, "close", HistoricalVolatilityParams::default())
102 }
103
104 #[inline]
105 pub fn get_lookback(&self) -> usize {
106 self.params.lookback.unwrap_or(20)
107 }
108
109 #[inline]
110 pub fn get_annualization_days(&self) -> f64 {
111 self.params.annualization_days.unwrap_or(250.0)
112 }
113}
114
115#[derive(Copy, Clone, Debug)]
116pub struct HistoricalVolatilityBuilder {
117 lookback: Option<usize>,
118 annualization_days: Option<f64>,
119 kernel: Kernel,
120}
121
122impl Default for HistoricalVolatilityBuilder {
123 fn default() -> Self {
124 Self {
125 lookback: None,
126 annualization_days: None,
127 kernel: Kernel::Auto,
128 }
129 }
130}
131
132impl HistoricalVolatilityBuilder {
133 #[inline(always)]
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 #[inline(always)]
139 pub fn lookback(mut self, lookback: usize) -> Self {
140 self.lookback = Some(lookback);
141 self
142 }
143
144 #[inline(always)]
145 pub fn annualization_days(mut self, annualization_days: f64) -> Self {
146 self.annualization_days = Some(annualization_days);
147 self
148 }
149
150 #[inline(always)]
151 pub fn kernel(mut self, kernel: Kernel) -> Self {
152 self.kernel = kernel;
153 self
154 }
155
156 #[inline(always)]
157 pub fn apply(
158 self,
159 candles: &Candles,
160 source: &str,
161 ) -> Result<HistoricalVolatilityOutput, HistoricalVolatilityError> {
162 let input = HistoricalVolatilityInput::from_candles(
163 candles,
164 source,
165 HistoricalVolatilityParams {
166 lookback: self.lookback,
167 annualization_days: self.annualization_days,
168 },
169 );
170 historical_volatility_with_kernel(&input, self.kernel)
171 }
172
173 #[inline(always)]
174 pub fn apply_slice(
175 self,
176 data: &[f64],
177 ) -> Result<HistoricalVolatilityOutput, HistoricalVolatilityError> {
178 let input = HistoricalVolatilityInput::from_slice(
179 data,
180 HistoricalVolatilityParams {
181 lookback: self.lookback,
182 annualization_days: self.annualization_days,
183 },
184 );
185 historical_volatility_with_kernel(&input, self.kernel)
186 }
187
188 #[inline(always)]
189 pub fn into_stream(self) -> Result<HistoricalVolatilityStream, HistoricalVolatilityError> {
190 HistoricalVolatilityStream::try_new(HistoricalVolatilityParams {
191 lookback: self.lookback,
192 annualization_days: self.annualization_days,
193 })
194 }
195}
196
197#[derive(Debug, Error)]
198pub enum HistoricalVolatilityError {
199 #[error("historical_volatility: Input data slice is empty.")]
200 EmptyInputData,
201 #[error("historical_volatility: All values are NaN or do not produce valid returns.")]
202 AllValuesNaN,
203 #[error(
204 "historical_volatility: Invalid lookback: lookback = {lookback}, data length = {data_len}"
205 )]
206 InvalidLookback { lookback: usize, data_len: usize },
207 #[error("historical_volatility: Not enough valid data: needed = {needed}, valid = {valid}")]
208 NotEnoughValidData { needed: usize, valid: usize },
209 #[error(
210 "historical_volatility: Invalid annualization_days: {annualization_days}. Must be finite and > 0."
211 )]
212 InvalidAnnualizationDays { annualization_days: f64 },
213 #[error("historical_volatility: Output length mismatch: expected = {expected}, got = {got}")]
214 OutputLengthMismatch { expected: usize, got: usize },
215 #[error("historical_volatility: Invalid range: start={start}, end={end}, step={step}")]
216 InvalidRange {
217 start: String,
218 end: String,
219 step: String,
220 },
221 #[error("historical_volatility: Invalid kernel for batch: {0:?}")]
222 InvalidKernelForBatch(Kernel),
223}
224
225#[derive(Debug, Clone)]
226pub struct HistoricalVolatilityStream {
227 lookback: usize,
228 annualization_scale: f64,
229 prev: f64,
230 has_prev: bool,
231 returns: Vec<f64>,
232 valid: Vec<u8>,
233 idx: usize,
234 cnt: usize,
235 valid_count: usize,
236 sum: f64,
237 sumsq: f64,
238}
239
240impl HistoricalVolatilityStream {
241 pub fn try_new(
242 params: HistoricalVolatilityParams,
243 ) -> Result<HistoricalVolatilityStream, HistoricalVolatilityError> {
244 let lookback = params.lookback.unwrap_or(20);
245 if lookback == 0 {
246 return Err(HistoricalVolatilityError::InvalidLookback {
247 lookback,
248 data_len: 0,
249 });
250 }
251 let annualization_days = params.annualization_days.unwrap_or(250.0);
252 if !annualization_days.is_finite() || annualization_days <= 0.0 {
253 return Err(HistoricalVolatilityError::InvalidAnnualizationDays { annualization_days });
254 }
255 Ok(Self {
256 lookback,
257 annualization_scale: annualization_days.sqrt(),
258 prev: f64::NAN,
259 has_prev: false,
260 returns: vec![0.0; lookback],
261 valid: vec![0u8; lookback],
262 idx: 0,
263 cnt: 0,
264 valid_count: 0,
265 sum: 0.0,
266 sumsq: 0.0,
267 })
268 }
269
270 #[inline(always)]
271 pub fn update(&mut self, value: f64) -> Option<f64> {
272 if !self.has_prev {
273 self.prev = value;
274 self.has_prev = true;
275 return None;
276 }
277
278 if self.cnt >= self.lookback {
279 let old_idx = self.idx;
280 if self.valid[old_idx] != 0 {
281 let old = self.returns[old_idx];
282 self.valid_count = self.valid_count.saturating_sub(1);
283 self.sum -= old;
284 self.sumsq -= old * old;
285 }
286 } else {
287 self.cnt += 1;
288 }
289
290 if valid_return_pair(self.prev, value) {
291 let ret = pct_return(self.prev, value);
292 self.returns[self.idx] = ret;
293 self.valid[self.idx] = 1;
294 self.valid_count += 1;
295 self.sum += ret;
296 self.sumsq += ret * ret;
297 } else {
298 self.returns[self.idx] = 0.0;
299 self.valid[self.idx] = 0;
300 }
301
302 self.prev = value;
303 self.idx += 1;
304 if self.idx == self.lookback {
305 self.idx = 0;
306 }
307
308 if self.cnt < self.lookback {
309 return None;
310 }
311 if self.valid_count != self.lookback {
312 return Some(f64::NAN);
313 }
314
315 let mean = self.sum / self.lookback as f64;
316 let variance = ((self.sumsq / self.lookback as f64) - mean * mean).max(0.0);
317 Some(variance.sqrt() * self.annualization_scale)
318 }
319
320 #[inline(always)]
321 pub fn get_warmup_period(&self) -> usize {
322 self.lookback
323 }
324}
325
326#[inline]
327pub fn historical_volatility(
328 input: &HistoricalVolatilityInput,
329) -> Result<HistoricalVolatilityOutput, HistoricalVolatilityError> {
330 historical_volatility_with_kernel(input, Kernel::Auto)
331}
332
333#[inline(always)]
334fn valid_return_pair(prev: f64, curr: f64) -> bool {
335 prev.is_finite() && curr.is_finite() && prev != 0.0
336}
337
338#[inline(always)]
339fn pct_return(prev: f64, curr: f64) -> f64 {
340 ((curr / prev) - 1.0) * 100.0
341}
342
343#[inline(always)]
344fn first_valid_return(data: &[f64]) -> usize {
345 let len = data.len();
346 let mut i = 1usize;
347 while i < len {
348 if valid_return_pair(data[i - 1], data[i]) {
349 return i;
350 }
351 i += 1;
352 }
353 len
354}
355
356#[inline(always)]
357fn count_valid_returns(data: &[f64]) -> usize {
358 let mut count = 0usize;
359 for i in 1..data.len() {
360 if valid_return_pair(data[i - 1], data[i]) {
361 count += 1;
362 }
363 }
364 count
365}
366
367#[inline(always)]
368fn build_return_prefixes(data: &[f64]) -> (Vec<u32>, Vec<f64>, Vec<f64>) {
369 let len = data.len();
370 let mut prefix_valid = vec![0u32; len + 1];
371 let mut prefix_sum = vec![0.0f64; len + 1];
372 let mut prefix_sumsq = vec![0.0f64; len + 1];
373
374 for i in 0..len {
375 prefix_valid[i + 1] = prefix_valid[i];
376 prefix_sum[i + 1] = prefix_sum[i];
377 prefix_sumsq[i + 1] = prefix_sumsq[i];
378
379 if i == 0 || !valid_return_pair(data[i - 1], data[i]) {
380 continue;
381 }
382
383 let ret = pct_return(data[i - 1], data[i]);
384 prefix_valid[i + 1] += 1;
385 prefix_sum[i + 1] += ret;
386 prefix_sumsq[i + 1] += ret * ret;
387 }
388
389 (prefix_valid, prefix_sum, prefix_sumsq)
390}
391
392#[inline(always)]
393fn hv_row_from_prefix(
394 prefix_valid: &[u32],
395 prefix_sum: &[f64],
396 prefix_sumsq: &[f64],
397 lookback: usize,
398 annualization_scale: f64,
399 first: usize,
400 out: &mut [f64],
401) {
402 let warmup = first.saturating_add(lookback.saturating_sub(1));
403 let lookback_u32 = lookback as u32;
404 let inv_lb = 1.0 / lookback as f64;
405
406 for (t, slot) in out.iter_mut().enumerate() {
407 if t < warmup {
408 *slot = f64::NAN;
409 continue;
410 }
411
412 let window_start = t + 1 - lookback;
413 let valid_count = prefix_valid[t + 1] - prefix_valid[window_start];
414 if valid_count != lookback_u32 {
415 *slot = f64::NAN;
416 continue;
417 }
418
419 let sum = prefix_sum[t + 1] - prefix_sum[window_start];
420 let sumsq = prefix_sumsq[t + 1] - prefix_sumsq[window_start];
421 let mean = sum * inv_lb;
422 let variance = (sumsq * inv_lb - mean * mean).max(0.0);
423 *slot = variance.sqrt() * annualization_scale;
424 }
425}
426
427#[inline(always)]
428fn historical_volatility_prepare<'a>(
429 input: &'a HistoricalVolatilityInput,
430 kernel: Kernel,
431) -> Result<(&'a [f64], usize, usize, f64, Kernel), HistoricalVolatilityError> {
432 let data = input.as_ref();
433 let len = data.len();
434 if len == 0 {
435 return Err(HistoricalVolatilityError::EmptyInputData);
436 }
437
438 let first = first_valid_return(data);
439 if first >= len {
440 return Err(HistoricalVolatilityError::AllValuesNaN);
441 }
442
443 let lookback = input.get_lookback();
444 if lookback == 0 || lookback > len {
445 return Err(HistoricalVolatilityError::InvalidLookback {
446 lookback,
447 data_len: len,
448 });
449 }
450
451 let annualization_days = input.get_annualization_days();
452 if !annualization_days.is_finite() || annualization_days <= 0.0 {
453 return Err(HistoricalVolatilityError::InvalidAnnualizationDays { annualization_days });
454 }
455
456 let valid = count_valid_returns(data);
457 if valid < lookback {
458 return Err(HistoricalVolatilityError::NotEnoughValidData {
459 needed: lookback,
460 valid,
461 });
462 }
463
464 let chosen = match kernel {
465 Kernel::Auto => detect_best_kernel(),
466 other => other.to_non_batch(),
467 };
468
469 Ok((data, lookback, first, annualization_days.sqrt(), chosen))
470}
471
472#[inline]
473pub fn historical_volatility_with_kernel(
474 input: &HistoricalVolatilityInput,
475 kernel: Kernel,
476) -> Result<HistoricalVolatilityOutput, HistoricalVolatilityError> {
477 let (data, lookback, first, annualization_scale, _chosen) =
478 historical_volatility_prepare(input, kernel)?;
479 let mut values =
480 alloc_with_nan_prefix(data.len(), first.saturating_add(lookback.saturating_sub(1)));
481 let (prefix_valid, prefix_sum, prefix_sumsq) = build_return_prefixes(data);
482 hv_row_from_prefix(
483 &prefix_valid,
484 &prefix_sum,
485 &prefix_sumsq,
486 lookback,
487 annualization_scale,
488 first,
489 &mut values,
490 );
491 Ok(HistoricalVolatilityOutput { values })
492}
493
494#[inline]
495pub fn historical_volatility_into_slice(
496 dst: &mut [f64],
497 input: &HistoricalVolatilityInput,
498 kernel: Kernel,
499) -> Result<(), HistoricalVolatilityError> {
500 let (data, lookback, first, annualization_scale, _chosen) =
501 historical_volatility_prepare(input, kernel)?;
502 if dst.len() != data.len() {
503 return Err(HistoricalVolatilityError::OutputLengthMismatch {
504 expected: data.len(),
505 got: dst.len(),
506 });
507 }
508 let (prefix_valid, prefix_sum, prefix_sumsq) = build_return_prefixes(data);
509 hv_row_from_prefix(
510 &prefix_valid,
511 &prefix_sum,
512 &prefix_sumsq,
513 lookback,
514 annualization_scale,
515 first,
516 dst,
517 );
518 Ok(())
519}
520
521#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
522#[inline]
523pub fn historical_volatility_into(
524 input: &HistoricalVolatilityInput,
525 out: &mut [f64],
526) -> Result<(), HistoricalVolatilityError> {
527 historical_volatility_into_slice(out, input, Kernel::Auto)
528}
529
530#[derive(Clone, Debug)]
531pub struct HistoricalVolatilityBatchRange {
532 pub lookback: (usize, usize, usize),
533 pub annualization_days: (f64, f64, f64),
534}
535
536impl Default for HistoricalVolatilityBatchRange {
537 fn default() -> Self {
538 Self {
539 lookback: (20, 252, 1),
540 annualization_days: (250.0, 250.0, 0.0),
541 }
542 }
543}
544
545#[derive(Clone, Debug, Default)]
546pub struct HistoricalVolatilityBatchBuilder {
547 range: HistoricalVolatilityBatchRange,
548 kernel: Kernel,
549}
550
551impl HistoricalVolatilityBatchBuilder {
552 #[inline]
553 pub fn new() -> Self {
554 Self::default()
555 }
556
557 #[inline]
558 pub fn kernel(mut self, kernel: Kernel) -> Self {
559 self.kernel = kernel;
560 self
561 }
562
563 #[inline]
564 pub fn lookback_range(mut self, start: usize, end: usize, step: usize) -> Self {
565 self.range.lookback = (start, end, step);
566 self
567 }
568
569 #[inline]
570 pub fn lookback_static(mut self, lookback: usize) -> Self {
571 self.range.lookback = (lookback, lookback, 0);
572 self
573 }
574
575 #[inline]
576 pub fn annualization_days_range(mut self, start: f64, end: f64, step: f64) -> Self {
577 self.range.annualization_days = (start, end, step);
578 self
579 }
580
581 #[inline]
582 pub fn annualization_days_static(mut self, annualization_days: f64) -> Self {
583 self.range.annualization_days = (annualization_days, annualization_days, 0.0);
584 self
585 }
586
587 #[inline]
588 pub fn apply_slice(
589 self,
590 data: &[f64],
591 ) -> Result<HistoricalVolatilityBatchOutput, HistoricalVolatilityError> {
592 historical_volatility_batch_with_kernel(data, &self.range, self.kernel)
593 }
594
595 #[inline]
596 pub fn apply_candles(
597 self,
598 candles: &Candles,
599 source: &str,
600 ) -> Result<HistoricalVolatilityBatchOutput, HistoricalVolatilityError> {
601 self.apply_slice(source_type(candles, source))
602 }
603
604 #[inline]
605 pub fn with_default_candles(
606 candles: &Candles,
607 ) -> Result<HistoricalVolatilityBatchOutput, HistoricalVolatilityError> {
608 HistoricalVolatilityBatchBuilder::new()
609 .kernel(Kernel::Auto)
610 .apply_candles(candles, "close")
611 }
612}
613
614#[derive(Clone, Debug)]
615pub struct HistoricalVolatilityBatchOutput {
616 pub values: Vec<f64>,
617 pub combos: Vec<HistoricalVolatilityParams>,
618 pub rows: usize,
619 pub cols: usize,
620}
621
622impl HistoricalVolatilityBatchOutput {
623 pub fn row_for_params(&self, params: &HistoricalVolatilityParams) -> Option<usize> {
624 self.combos.iter().position(|combo| {
625 combo.lookback.unwrap_or(20) == params.lookback.unwrap_or(20)
626 && (combo.annualization_days.unwrap_or(250.0)
627 - params.annualization_days.unwrap_or(250.0))
628 .abs()
629 < 1e-12
630 })
631 }
632
633 pub fn values_for(&self, params: &HistoricalVolatilityParams) -> Option<&[f64]> {
634 self.row_for_params(params).and_then(|row| {
635 row.checked_mul(self.cols)
636 .and_then(|start| self.values.get(start..start + self.cols))
637 })
638 }
639}
640
641#[inline(always)]
642fn expand_grid_historical_volatility(
643 range: &HistoricalVolatilityBatchRange,
644) -> Result<Vec<HistoricalVolatilityParams>, HistoricalVolatilityError> {
645 fn axis_usize(
646 (start, end, step): (usize, usize, usize),
647 ) -> Result<Vec<usize>, HistoricalVolatilityError> {
648 if step == 0 || start == end {
649 return Ok(vec![start]);
650 }
651
652 let mut out = Vec::new();
653 if start < end {
654 let mut x = start;
655 while x <= end {
656 out.push(x);
657 let next = x.saturating_add(step);
658 if next == x {
659 break;
660 }
661 x = next;
662 }
663 } else {
664 let mut x = start;
665 loop {
666 out.push(x);
667 if x == end {
668 break;
669 }
670 let next = x.saturating_sub(step);
671 if next == x || next < end {
672 break;
673 }
674 x = next;
675 }
676 }
677
678 if out.is_empty() {
679 return Err(HistoricalVolatilityError::InvalidRange {
680 start: start.to_string(),
681 end: end.to_string(),
682 step: step.to_string(),
683 });
684 }
685 Ok(out)
686 }
687
688 fn axis_f64(
689 (start, end, step): (f64, f64, f64),
690 ) -> Result<Vec<f64>, HistoricalVolatilityError> {
691 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
692 return Err(HistoricalVolatilityError::InvalidRange {
693 start: start.to_string(),
694 end: end.to_string(),
695 step: step.to_string(),
696 });
697 }
698 if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
699 return Ok(vec![start]);
700 }
701
702 let mut out = Vec::new();
703 if start < end {
704 let st = step.abs();
705 let mut x = start;
706 while x <= end + 1e-12 {
707 out.push(x);
708 x += st;
709 }
710 } else {
711 let st = -step.abs();
712 let mut x = start;
713 while x >= end - 1e-12 {
714 out.push(x);
715 x += st;
716 }
717 }
718
719 if out.is_empty() {
720 return Err(HistoricalVolatilityError::InvalidRange {
721 start: start.to_string(),
722 end: end.to_string(),
723 step: step.to_string(),
724 });
725 }
726 Ok(out)
727 }
728
729 let lookbacks = axis_usize(range.lookback)?;
730 if lookbacks.iter().any(|&lookback| lookback == 0) {
731 return Err(HistoricalVolatilityError::InvalidLookback {
732 lookback: 0,
733 data_len: 0,
734 });
735 }
736
737 let annualization_days = axis_f64(range.annualization_days)?;
738 if let Some(&bad) = annualization_days
739 .iter()
740 .find(|&&annualization_days| !annualization_days.is_finite() || annualization_days <= 0.0)
741 {
742 return Err(HistoricalVolatilityError::InvalidAnnualizationDays {
743 annualization_days: bad,
744 });
745 }
746
747 let mut out = Vec::with_capacity(lookbacks.len() * annualization_days.len());
748 for &lookback in &lookbacks {
749 for &annualization_days in &annualization_days {
750 out.push(HistoricalVolatilityParams {
751 lookback: Some(lookback),
752 annualization_days: Some(annualization_days),
753 });
754 }
755 }
756 Ok(out)
757}
758
759#[inline]
760pub fn historical_volatility_batch_with_kernel(
761 data: &[f64],
762 sweep: &HistoricalVolatilityBatchRange,
763 kernel: Kernel,
764) -> Result<HistoricalVolatilityBatchOutput, HistoricalVolatilityError> {
765 let batch_kernel = match kernel {
766 Kernel::Auto => detect_best_batch_kernel(),
767 other if other.is_batch() => other,
768 other => return Err(HistoricalVolatilityError::InvalidKernelForBatch(other)),
769 };
770 historical_volatility_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
771}
772
773#[inline]
774pub fn historical_volatility_batch_slice(
775 data: &[f64],
776 sweep: &HistoricalVolatilityBatchRange,
777 kernel: Kernel,
778) -> Result<HistoricalVolatilityBatchOutput, HistoricalVolatilityError> {
779 historical_volatility_batch_inner(data, sweep, kernel, false)
780}
781
782#[inline]
783pub fn historical_volatility_batch_par_slice(
784 data: &[f64],
785 sweep: &HistoricalVolatilityBatchRange,
786 kernel: Kernel,
787) -> Result<HistoricalVolatilityBatchOutput, HistoricalVolatilityError> {
788 historical_volatility_batch_inner(data, sweep, kernel, true)
789}
790
791#[inline(always)]
792fn historical_volatility_batch_inner(
793 data: &[f64],
794 sweep: &HistoricalVolatilityBatchRange,
795 _kernel: Kernel,
796 parallel: bool,
797) -> Result<HistoricalVolatilityBatchOutput, HistoricalVolatilityError> {
798 let combos = expand_grid_historical_volatility(sweep)?;
799 let rows = combos.len();
800 let cols = data.len();
801 if cols == 0 {
802 return Err(HistoricalVolatilityError::EmptyInputData);
803 }
804 let first = first_valid_return(data);
805 if first >= cols {
806 return Err(HistoricalVolatilityError::AllValuesNaN);
807 }
808 let valid = count_valid_returns(data);
809 let max_lookback = combos
810 .iter()
811 .map(|combo| combo.lookback.unwrap_or(20))
812 .max()
813 .unwrap_or(0);
814 if max_lookback == 0 || valid < max_lookback {
815 return Err(HistoricalVolatilityError::NotEnoughValidData {
816 needed: max_lookback,
817 valid,
818 });
819 }
820
821 let mut buf_mu = make_uninit_matrix(rows, cols);
822 let warmups: Vec<usize> = combos
823 .iter()
824 .map(|combo| first.saturating_add(combo.lookback.unwrap_or(20).saturating_sub(1)))
825 .collect();
826 init_matrix_prefixes(&mut buf_mu, cols, &warmups);
827
828 let mut guard = ManuallyDrop::new(buf_mu);
829 let out: &mut [f64] =
830 unsafe { std::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
831
832 let (prefix_valid, prefix_sum, prefix_sumsq) = build_return_prefixes(data);
833
834 if parallel {
835 #[cfg(not(target_arch = "wasm32"))]
836 out.par_chunks_mut(cols)
837 .enumerate()
838 .for_each(|(row, out_row)| {
839 let combo = &combos[row];
840 hv_row_from_prefix(
841 &prefix_valid,
842 &prefix_sum,
843 &prefix_sumsq,
844 combo.lookback.unwrap_or(20),
845 combo.annualization_days.unwrap_or(250.0).sqrt(),
846 first,
847 out_row,
848 );
849 });
850
851 #[cfg(target_arch = "wasm32")]
852 for (row, out_row) in out.chunks_mut(cols).enumerate() {
853 let combo = &combos[row];
854 hv_row_from_prefix(
855 &prefix_valid,
856 &prefix_sum,
857 &prefix_sumsq,
858 combo.lookback.unwrap_or(20),
859 combo.annualization_days.unwrap_or(250.0).sqrt(),
860 first,
861 out_row,
862 );
863 }
864 } else {
865 for (row, out_row) in out.chunks_mut(cols).enumerate() {
866 let combo = &combos[row];
867 hv_row_from_prefix(
868 &prefix_valid,
869 &prefix_sum,
870 &prefix_sumsq,
871 combo.lookback.unwrap_or(20),
872 combo.annualization_days.unwrap_or(250.0).sqrt(),
873 first,
874 out_row,
875 );
876 }
877 }
878
879 let values = unsafe {
880 Vec::from_raw_parts(
881 guard.as_mut_ptr() as *mut f64,
882 guard.len(),
883 guard.capacity(),
884 )
885 };
886
887 Ok(HistoricalVolatilityBatchOutput {
888 values,
889 combos,
890 rows,
891 cols,
892 })
893}
894
895#[inline(always)]
896pub fn historical_volatility_batch_inner_into(
897 data: &[f64],
898 sweep: &HistoricalVolatilityBatchRange,
899 _kernel: Kernel,
900 parallel: bool,
901 out: &mut [f64],
902) -> Result<Vec<HistoricalVolatilityParams>, HistoricalVolatilityError> {
903 let combos = expand_grid_historical_volatility(sweep)?;
904 let rows = combos.len();
905 let cols = data.len();
906 if cols == 0 {
907 return Err(HistoricalVolatilityError::EmptyInputData);
908 }
909 let total =
910 rows.checked_mul(cols)
911 .ok_or_else(|| HistoricalVolatilityError::OutputLengthMismatch {
912 expected: usize::MAX,
913 got: out.len(),
914 })?;
915 if out.len() != total {
916 return Err(HistoricalVolatilityError::OutputLengthMismatch {
917 expected: total,
918 got: out.len(),
919 });
920 }
921 let first = first_valid_return(data);
922 if first >= cols {
923 return Err(HistoricalVolatilityError::AllValuesNaN);
924 }
925 let valid = count_valid_returns(data);
926 let max_lookback = combos
927 .iter()
928 .map(|combo| combo.lookback.unwrap_or(20))
929 .max()
930 .unwrap_or(0);
931 if max_lookback == 0 || valid < max_lookback {
932 return Err(HistoricalVolatilityError::NotEnoughValidData {
933 needed: max_lookback,
934 valid,
935 });
936 }
937
938 let (prefix_valid, prefix_sum, prefix_sumsq) = build_return_prefixes(data);
939
940 if parallel {
941 #[cfg(not(target_arch = "wasm32"))]
942 out.par_chunks_mut(cols)
943 .enumerate()
944 .for_each(|(row, out_row)| {
945 let combo = &combos[row];
946 hv_row_from_prefix(
947 &prefix_valid,
948 &prefix_sum,
949 &prefix_sumsq,
950 combo.lookback.unwrap_or(20),
951 combo.annualization_days.unwrap_or(250.0).sqrt(),
952 first,
953 out_row,
954 );
955 });
956
957 #[cfg(target_arch = "wasm32")]
958 for (row, out_row) in out.chunks_mut(cols).enumerate() {
959 let combo = &combos[row];
960 hv_row_from_prefix(
961 &prefix_valid,
962 &prefix_sum,
963 &prefix_sumsq,
964 combo.lookback.unwrap_or(20),
965 combo.annualization_days.unwrap_or(250.0).sqrt(),
966 first,
967 out_row,
968 );
969 }
970 } else {
971 for (row, out_row) in out.chunks_mut(cols).enumerate() {
972 let combo = &combos[row];
973 hv_row_from_prefix(
974 &prefix_valid,
975 &prefix_sum,
976 &prefix_sumsq,
977 combo.lookback.unwrap_or(20),
978 combo.annualization_days.unwrap_or(250.0).sqrt(),
979 first,
980 out_row,
981 );
982 }
983 }
984
985 Ok(combos)
986}
987
988#[cfg(feature = "python")]
989#[pyfunction(name = "historical_volatility")]
990#[pyo3(signature = (data, lookback=20, annualization_days=250.0, kernel=None))]
991pub fn historical_volatility_py<'py>(
992 py: Python<'py>,
993 data: PyReadonlyArray1<'py, f64>,
994 lookback: usize,
995 annualization_days: f64,
996 kernel: Option<&str>,
997) -> PyResult<Bound<'py, PyArray1<f64>>> {
998 let slice = data.as_slice()?;
999 let kernel = validate_kernel(kernel, false)?;
1000 let input = HistoricalVolatilityInput::from_slice(
1001 slice,
1002 HistoricalVolatilityParams {
1003 lookback: Some(lookback),
1004 annualization_days: Some(annualization_days),
1005 },
1006 );
1007 let output = py
1008 .allow_threads(|| historical_volatility_with_kernel(&input, kernel))
1009 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1010 Ok(output.values.into_pyarray(py))
1011}
1012
1013#[cfg(feature = "python")]
1014#[pyclass(name = "HistoricalVolatilityStream")]
1015pub struct HistoricalVolatilityStreamPy {
1016 stream: HistoricalVolatilityStream,
1017}
1018
1019#[cfg(feature = "python")]
1020#[pymethods]
1021impl HistoricalVolatilityStreamPy {
1022 #[new]
1023 fn new(lookback: usize, annualization_days: f64) -> PyResult<Self> {
1024 let stream = HistoricalVolatilityStream::try_new(HistoricalVolatilityParams {
1025 lookback: Some(lookback),
1026 annualization_days: Some(annualization_days),
1027 })
1028 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1029 Ok(Self { stream })
1030 }
1031
1032 fn update(&mut self, value: f64) -> Option<f64> {
1033 self.stream.update(value)
1034 }
1035}
1036
1037#[cfg(feature = "python")]
1038#[pyfunction(name = "historical_volatility_batch")]
1039#[pyo3(signature = (data, lookback_range, annualization_days_range=(250.0, 250.0, 0.0), kernel=None))]
1040pub fn historical_volatility_batch_py<'py>(
1041 py: Python<'py>,
1042 data: PyReadonlyArray1<'py, f64>,
1043 lookback_range: (usize, usize, usize),
1044 annualization_days_range: (f64, f64, f64),
1045 kernel: Option<&str>,
1046) -> PyResult<Bound<'py, PyDict>> {
1047 let slice = data.as_slice()?;
1048 let kernel = validate_kernel(kernel, true)?;
1049 let sweep = HistoricalVolatilityBatchRange {
1050 lookback: lookback_range,
1051 annualization_days: annualization_days_range,
1052 };
1053
1054 let combos = expand_grid_historical_volatility(&sweep)
1055 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1056 let rows = combos.len();
1057 let cols = slice.len();
1058 let total = rows
1059 .checked_mul(cols)
1060 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1061
1062 let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1063 let slice_out = unsafe { out_arr.as_slice_mut()? };
1064
1065 let combos = py
1066 .allow_threads(|| {
1067 let batch = match kernel {
1068 Kernel::Auto => detect_best_batch_kernel(),
1069 other => other,
1070 };
1071 historical_volatility_batch_inner_into(
1072 slice,
1073 &sweep,
1074 batch.to_non_batch(),
1075 true,
1076 slice_out,
1077 )
1078 })
1079 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1080
1081 let dict = PyDict::new(py);
1082 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1083 dict.set_item(
1084 "lookbacks",
1085 combos
1086 .iter()
1087 .map(|combo| combo.lookback.unwrap_or(20) as u64)
1088 .collect::<Vec<_>>()
1089 .into_pyarray(py),
1090 )?;
1091 dict.set_item(
1092 "annualization_days",
1093 combos
1094 .iter()
1095 .map(|combo| combo.annualization_days.unwrap_or(250.0))
1096 .collect::<Vec<_>>()
1097 .into_pyarray(py),
1098 )?;
1099 dict.set_item("rows", rows)?;
1100 dict.set_item("cols", cols)?;
1101 Ok(dict)
1102}
1103
1104#[cfg(feature = "python")]
1105pub fn register_historical_volatility_module(
1106 module: &Bound<'_, pyo3::types::PyModule>,
1107) -> PyResult<()> {
1108 module.add_function(wrap_pyfunction!(historical_volatility_py, module)?)?;
1109 module.add_function(wrap_pyfunction!(historical_volatility_batch_py, module)?)?;
1110 module.add_class::<HistoricalVolatilityStreamPy>()?;
1111 Ok(())
1112}
1113
1114#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1115#[wasm_bindgen(js_name = "historical_volatility_js")]
1116pub fn historical_volatility_js(
1117 data: &[f64],
1118 lookback: usize,
1119 annualization_days: f64,
1120) -> Result<Vec<f64>, JsValue> {
1121 let input = HistoricalVolatilityInput::from_slice(
1122 data,
1123 HistoricalVolatilityParams {
1124 lookback: Some(lookback),
1125 annualization_days: Some(annualization_days),
1126 },
1127 );
1128 let mut output = vec![0.0; data.len()];
1129 historical_volatility_into_slice(&mut output, &input, Kernel::Auto)
1130 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1131 Ok(output)
1132}
1133
1134#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1135#[wasm_bindgen]
1136pub fn historical_volatility_alloc(len: usize) -> *mut f64 {
1137 let mut vec = Vec::<f64>::with_capacity(len);
1138 let ptr = vec.as_mut_ptr();
1139 std::mem::forget(vec);
1140 ptr
1141}
1142
1143#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1144#[wasm_bindgen]
1145pub fn historical_volatility_free(ptr: *mut f64, len: usize) {
1146 if !ptr.is_null() {
1147 unsafe {
1148 let _ = Vec::from_raw_parts(ptr, len, len);
1149 }
1150 }
1151}
1152
1153#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1154#[wasm_bindgen]
1155pub fn historical_volatility_into(
1156 in_ptr: *const f64,
1157 out_ptr: *mut f64,
1158 len: usize,
1159 lookback: usize,
1160 annualization_days: f64,
1161) -> Result<(), JsValue> {
1162 if in_ptr.is_null() || out_ptr.is_null() {
1163 return Err(JsValue::from_str("Null pointer provided"));
1164 }
1165
1166 unsafe {
1167 let data = std::slice::from_raw_parts(in_ptr, len);
1168 let input = HistoricalVolatilityInput::from_slice(
1169 data,
1170 HistoricalVolatilityParams {
1171 lookback: Some(lookback),
1172 annualization_days: Some(annualization_days),
1173 },
1174 );
1175
1176 if in_ptr == out_ptr {
1177 let mut tmp = vec![0.0; len];
1178 historical_volatility_into_slice(&mut tmp, &input, Kernel::Auto)
1179 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1180 std::slice::from_raw_parts_mut(out_ptr, len).copy_from_slice(&tmp);
1181 } else {
1182 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1183 historical_volatility_into_slice(out, &input, Kernel::Auto)
1184 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1185 }
1186 }
1187
1188 Ok(())
1189}
1190
1191#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1192#[derive(Serialize, Deserialize)]
1193pub struct HistoricalVolatilityBatchConfig {
1194 pub lookback_range: (usize, usize, usize),
1195 pub annualization_days_range: Option<(f64, f64, f64)>,
1196}
1197
1198#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1199#[derive(Serialize, Deserialize)]
1200pub struct HistoricalVolatilityBatchJsOutput {
1201 pub values: Vec<f64>,
1202 pub combos: Vec<HistoricalVolatilityParams>,
1203 pub lookbacks: Vec<usize>,
1204 pub annualization_days: Vec<f64>,
1205 pub rows: usize,
1206 pub cols: usize,
1207}
1208
1209#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1210#[wasm_bindgen(js_name = "historical_volatility_batch_js")]
1211pub fn historical_volatility_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1212 let config: HistoricalVolatilityBatchConfig = serde_wasm_bindgen::from_value(config)
1213 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1214 let sweep = HistoricalVolatilityBatchRange {
1215 lookback: config.lookback_range,
1216 annualization_days: config
1217 .annualization_days_range
1218 .unwrap_or((250.0, 250.0, 0.0)),
1219 };
1220 let output = historical_volatility_batch_inner(data, &sweep, detect_best_kernel(), false)
1221 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1222 serde_wasm_bindgen::to_value(&HistoricalVolatilityBatchJsOutput {
1223 lookbacks: output
1224 .combos
1225 .iter()
1226 .map(|combo| combo.lookback.unwrap_or(20))
1227 .collect(),
1228 annualization_days: output
1229 .combos
1230 .iter()
1231 .map(|combo| combo.annualization_days.unwrap_or(250.0))
1232 .collect(),
1233 values: output.values,
1234 combos: output.combos,
1235 rows: output.rows,
1236 cols: output.cols,
1237 })
1238 .map_err(|e| JsValue::from_str(&e.to_string()))
1239}
1240
1241#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1242#[wasm_bindgen]
1243pub fn historical_volatility_batch_into(
1244 in_ptr: *const f64,
1245 out_ptr: *mut f64,
1246 len: usize,
1247 lookback_start: usize,
1248 lookback_end: usize,
1249 lookback_step: usize,
1250 annualization_days_start: f64,
1251 annualization_days_end: f64,
1252 annualization_days_step: f64,
1253) -> Result<usize, JsValue> {
1254 if in_ptr.is_null() || out_ptr.is_null() {
1255 return Err(JsValue::from_str("Null pointer provided"));
1256 }
1257
1258 let sweep = HistoricalVolatilityBatchRange {
1259 lookback: (lookback_start, lookback_end, lookback_step),
1260 annualization_days: (
1261 annualization_days_start,
1262 annualization_days_end,
1263 annualization_days_step,
1264 ),
1265 };
1266 let combos =
1267 expand_grid_historical_volatility(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1268 let rows = combos.len();
1269
1270 unsafe {
1271 let data = std::slice::from_raw_parts(in_ptr, len);
1272 let total = rows
1273 .checked_mul(len)
1274 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1275 let out = std::slice::from_raw_parts_mut(out_ptr, total);
1276 historical_volatility_batch_inner_into(data, &sweep, detect_best_kernel(), false, out)
1277 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1278 }
1279
1280 Ok(rows)
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285 use super::*;
1286 use crate::utilities::data_loader::read_candles_from_csv;
1287 use std::error::Error;
1288
1289 fn load_close() -> Result<Vec<f64>, Box<dyn Error>> {
1290 let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
1291 Ok(candles.close)
1292 }
1293
1294 #[test]
1295 fn historical_volatility_output_contract() -> Result<(), Box<dyn Error>> {
1296 let close = load_close()?;
1297 let input = HistoricalVolatilityInput::from_slice(
1298 &close,
1299 HistoricalVolatilityParams {
1300 lookback: Some(20),
1301 annualization_days: Some(250.0),
1302 },
1303 );
1304 let out = historical_volatility_with_kernel(&input, Kernel::Scalar)?;
1305 assert_eq!(out.values.len(), close.len());
1306 let first_valid = out.values.iter().position(|v| !v.is_nan()).unwrap();
1307 assert!(first_valid >= 20);
1308 assert!(out.values[first_valid..].iter().any(|v| v.is_finite()));
1309 Ok(())
1310 }
1311
1312 #[test]
1313 fn historical_volatility_auto_matches_scalar() -> Result<(), Box<dyn Error>> {
1314 let close = load_close()?;
1315 let input = HistoricalVolatilityInput::from_slice(
1316 &close,
1317 HistoricalVolatilityParams {
1318 lookback: Some(30),
1319 annualization_days: Some(252.0),
1320 },
1321 );
1322 let auto = historical_volatility_with_kernel(&input, Kernel::Auto)?;
1323 let scalar = historical_volatility_with_kernel(&input, Kernel::Scalar)?;
1324 for (a, b) in auto.values.iter().zip(scalar.values.iter()) {
1325 if a.is_nan() && b.is_nan() {
1326 continue;
1327 }
1328 assert!((a - b).abs() <= 1e-12);
1329 }
1330 Ok(())
1331 }
1332
1333 #[test]
1334 fn historical_volatility_rejects_invalid_annualization_days() {
1335 let data = [100.0, 101.0, 102.0, 103.0];
1336 let input = HistoricalVolatilityInput::from_slice(
1337 &data,
1338 HistoricalVolatilityParams {
1339 lookback: Some(2),
1340 annualization_days: Some(0.0),
1341 },
1342 );
1343 let err = historical_volatility_with_kernel(&input, Kernel::Scalar).unwrap_err();
1344 assert!(matches!(
1345 err,
1346 HistoricalVolatilityError::InvalidAnnualizationDays { .. }
1347 ));
1348 }
1349
1350 #[test]
1351 fn historical_volatility_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1352 let close = load_close()?;
1353 let params = HistoricalVolatilityParams {
1354 lookback: Some(20),
1355 annualization_days: Some(250.0),
1356 };
1357 let input = HistoricalVolatilityInput::from_slice(&close, params.clone());
1358 let batch = historical_volatility_with_kernel(&input, Kernel::Scalar)?;
1359 let mut stream = HistoricalVolatilityStream::try_new(params)?;
1360 let mut streamed = Vec::with_capacity(close.len());
1361 for &value in &close {
1362 streamed.push(stream.update(value).unwrap_or(f64::NAN));
1363 }
1364 for (a, b) in streamed.iter().zip(batch.values.iter()) {
1365 if a.is_nan() && b.is_nan() {
1366 continue;
1367 }
1368 assert!((a - b).abs() <= 1e-10);
1369 }
1370 Ok(())
1371 }
1372
1373 #[test]
1374 fn historical_volatility_batch_matches_single() -> Result<(), Box<dyn Error>> {
1375 let close = load_close()?;
1376 let sweep = HistoricalVolatilityBatchRange {
1377 lookback: (20, 20, 0),
1378 annualization_days: (250.0, 250.0, 0.0),
1379 };
1380 let batch = historical_volatility_batch_with_kernel(&close, &sweep, Kernel::ScalarBatch)?;
1381 assert_eq!(batch.rows, 1);
1382 assert_eq!(batch.cols, close.len());
1383 let single = historical_volatility_with_kernel(
1384 &HistoricalVolatilityInput::from_slice(
1385 &close,
1386 HistoricalVolatilityParams {
1387 lookback: Some(20),
1388 annualization_days: Some(250.0),
1389 },
1390 ),
1391 Kernel::Scalar,
1392 )?;
1393 for (a, b) in batch.values.iter().zip(single.values.iter()) {
1394 if a.is_nan() && b.is_nan() {
1395 continue;
1396 }
1397 assert!((a - b).abs() <= 1e-12);
1398 }
1399 Ok(())
1400 }
1401
1402 #[test]
1403 fn historical_volatility_nan_window_recovers() -> Result<(), Box<dyn Error>> {
1404 let mut close = load_close()?;
1405 close[40] = f64::NAN;
1406 let out = historical_volatility_with_kernel(
1407 &HistoricalVolatilityInput::from_slice(
1408 &close,
1409 HistoricalVolatilityParams {
1410 lookback: Some(10),
1411 annualization_days: Some(250.0),
1412 },
1413 ),
1414 Kernel::Scalar,
1415 )?;
1416 assert!(out.values[40].is_nan());
1417 assert!(out.values[49].is_nan());
1418 assert!(out.values[50].is_nan());
1419 assert!(out.values[51].is_finite());
1420 Ok(())
1421 }
1422}