1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, 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, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::collections::VecDeque;
26use std::convert::AsRef;
27use std::error::Error;
28use thiserror::Error;
29
30impl<'a> AsRef<[f64]> for HistoricalVolatilityRankInput<'a> {
31 #[inline(always)]
32 fn as_ref(&self) -> &[f64] {
33 match &self.data {
34 HistoricalVolatilityRankData::Slice(slice) => slice,
35 HistoricalVolatilityRankData::Candles { candles } => candles.close.as_slice(),
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
41pub enum HistoricalVolatilityRankData<'a> {
42 Candles { candles: &'a Candles },
43 Slice(&'a [f64]),
44}
45
46#[derive(Debug, Clone)]
47pub struct HistoricalVolatilityRankOutput {
48 pub hvr: Vec<f64>,
49 pub hv: Vec<f64>,
50}
51
52#[derive(Debug, Clone)]
53#[cfg_attr(
54 all(target_arch = "wasm32", feature = "wasm"),
55 derive(Serialize, Deserialize)
56)]
57pub struct HistoricalVolatilityRankParams {
58 pub hv_length: Option<usize>,
59 pub rank_length: Option<usize>,
60 pub annualization_days: Option<f64>,
61 pub bar_days: Option<f64>,
62}
63
64impl Default for HistoricalVolatilityRankParams {
65 fn default() -> Self {
66 Self {
67 hv_length: Some(10),
68 rank_length: Some(52 * 7),
69 annualization_days: Some(365.0),
70 bar_days: Some(1.0),
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
76pub struct HistoricalVolatilityRankInput<'a> {
77 pub data: HistoricalVolatilityRankData<'a>,
78 pub params: HistoricalVolatilityRankParams,
79}
80
81impl<'a> HistoricalVolatilityRankInput<'a> {
82 #[inline]
83 pub fn from_candles(candles: &'a Candles, params: HistoricalVolatilityRankParams) -> Self {
84 Self {
85 data: HistoricalVolatilityRankData::Candles { candles },
86 params,
87 }
88 }
89
90 #[inline]
91 pub fn from_slice(slice: &'a [f64], params: HistoricalVolatilityRankParams) -> Self {
92 Self {
93 data: HistoricalVolatilityRankData::Slice(slice),
94 params,
95 }
96 }
97
98 #[inline]
99 pub fn with_default_candles(candles: &'a Candles) -> Self {
100 Self::from_candles(candles, HistoricalVolatilityRankParams::default())
101 }
102
103 #[inline]
104 pub fn get_hv_length(&self) -> usize {
105 self.params.hv_length.unwrap_or(10)
106 }
107
108 #[inline]
109 pub fn get_rank_length(&self) -> usize {
110 self.params.rank_length.unwrap_or(52 * 7)
111 }
112
113 #[inline]
114 pub fn get_annualization_days(&self) -> f64 {
115 self.params.annualization_days.unwrap_or(365.0)
116 }
117
118 #[inline]
119 pub fn get_bar_days(&self) -> f64 {
120 self.params.bar_days.unwrap_or(1.0)
121 }
122}
123
124#[derive(Copy, Clone, Debug)]
125pub struct HistoricalVolatilityRankBuilder {
126 hv_length: Option<usize>,
127 rank_length: Option<usize>,
128 annualization_days: Option<f64>,
129 bar_days: Option<f64>,
130 kernel: Kernel,
131}
132
133impl Default for HistoricalVolatilityRankBuilder {
134 fn default() -> Self {
135 Self {
136 hv_length: None,
137 rank_length: None,
138 annualization_days: None,
139 bar_days: None,
140 kernel: Kernel::Auto,
141 }
142 }
143}
144
145impl HistoricalVolatilityRankBuilder {
146 #[inline(always)]
147 pub fn new() -> Self {
148 Self::default()
149 }
150
151 #[inline(always)]
152 pub fn hv_length(mut self, value: usize) -> Self {
153 self.hv_length = Some(value);
154 self
155 }
156
157 #[inline(always)]
158 pub fn rank_length(mut self, value: usize) -> Self {
159 self.rank_length = Some(value);
160 self
161 }
162
163 #[inline(always)]
164 pub fn annualization_days(mut self, value: f64) -> Self {
165 self.annualization_days = Some(value);
166 self
167 }
168
169 #[inline(always)]
170 pub fn bar_days(mut self, value: f64) -> Self {
171 self.bar_days = Some(value);
172 self
173 }
174
175 #[inline(always)]
176 pub fn kernel(mut self, value: Kernel) -> Self {
177 self.kernel = value;
178 self
179 }
180
181 #[inline(always)]
182 pub fn apply(
183 self,
184 candles: &Candles,
185 ) -> Result<HistoricalVolatilityRankOutput, HistoricalVolatilityRankError> {
186 let params = HistoricalVolatilityRankParams {
187 hv_length: self.hv_length,
188 rank_length: self.rank_length,
189 annualization_days: self.annualization_days,
190 bar_days: self.bar_days,
191 };
192 historical_volatility_rank_with_kernel(
193 &HistoricalVolatilityRankInput::from_candles(candles, params),
194 self.kernel,
195 )
196 }
197
198 #[inline(always)]
199 pub fn apply_slice(
200 self,
201 data: &[f64],
202 ) -> Result<HistoricalVolatilityRankOutput, HistoricalVolatilityRankError> {
203 let params = HistoricalVolatilityRankParams {
204 hv_length: self.hv_length,
205 rank_length: self.rank_length,
206 annualization_days: self.annualization_days,
207 bar_days: self.bar_days,
208 };
209 historical_volatility_rank_with_kernel(
210 &HistoricalVolatilityRankInput::from_slice(data, params),
211 self.kernel,
212 )
213 }
214
215 #[inline(always)]
216 pub fn into_stream(
217 self,
218 ) -> Result<HistoricalVolatilityRankStream, HistoricalVolatilityRankError> {
219 HistoricalVolatilityRankStream::try_new(HistoricalVolatilityRankParams {
220 hv_length: self.hv_length,
221 rank_length: self.rank_length,
222 annualization_days: self.annualization_days,
223 bar_days: self.bar_days,
224 })
225 }
226}
227
228#[derive(Debug, Error)]
229pub enum HistoricalVolatilityRankError {
230 #[error("historical_volatility_rank: Input data slice is empty.")]
231 EmptyInputData,
232 #[error("historical_volatility_rank: All values are NaN or non-positive.")]
233 AllValuesNaN,
234 #[error(
235 "historical_volatility_rank: Invalid hv_length: hv_length = {hv_length}, data length = {data_len}"
236 )]
237 InvalidHvLength { hv_length: usize, data_len: usize },
238 #[error("historical_volatility_rank: Invalid rank_length: rank_length = {rank_length}")]
239 InvalidRankLength { rank_length: usize },
240 #[error(
241 "historical_volatility_rank: Not enough valid data: needed = {needed}, valid = {valid}"
242 )]
243 NotEnoughValidData { needed: usize, valid: usize },
244 #[error(
245 "historical_volatility_rank: Invalid annualization_days: {annualization_days}. Must be positive and finite."
246 )]
247 InvalidAnnualizationDays { annualization_days: f64 },
248 #[error(
249 "historical_volatility_rank: Invalid bar_days: {bar_days}. Must be positive and finite."
250 )]
251 InvalidBarDays { bar_days: f64 },
252 #[error(
253 "historical_volatility_rank: Output length mismatch: expected = {expected}, got = {got}"
254 )]
255 OutputLengthMismatch { expected: usize, got: usize },
256 #[error("historical_volatility_rank: Invalid range: start={start}, end={end}, step={step}")]
257 InvalidRange {
258 start: String,
259 end: String,
260 step: String,
261 },
262 #[error("historical_volatility_rank: Invalid kernel for batch: {0:?}")]
263 InvalidKernelForBatch(Kernel),
264 #[error(
265 "historical_volatility_rank: Output length mismatch: dst = {dst_len}, expected = {expected_len}"
266 )]
267 MismatchedOutputLen { dst_len: usize, expected_len: usize },
268 #[error("historical_volatility_rank: Invalid input: {msg}")]
269 InvalidInput { msg: String },
270}
271
272#[derive(Debug, Clone)]
273pub struct HistoricalVolatilityRankStream {
274 hv_length: usize,
275 rank_length: usize,
276 annualization_scale: f64,
277 prev_close: Option<f64>,
278 returns: Vec<Option<f64>>,
279 returns_sum: f64,
280 returns_sumsq: f64,
281 returns_valid: usize,
282 returns_idx: usize,
283 returns_count: usize,
284 hv_ring: Vec<Option<f64>>,
285 hv_valid: usize,
286 hv_idx: usize,
287 hv_count: usize,
288 min_q: VecDeque<(usize, f64)>,
289 max_q: VecDeque<(usize, f64)>,
290 tick: usize,
291}
292
293impl HistoricalVolatilityRankStream {
294 #[inline(always)]
295 pub fn try_new(
296 params: HistoricalVolatilityRankParams,
297 ) -> Result<Self, HistoricalVolatilityRankError> {
298 let hv_length = params.hv_length.unwrap_or(10);
299 if hv_length == 0 {
300 return Err(HistoricalVolatilityRankError::InvalidHvLength {
301 hv_length,
302 data_len: 0,
303 });
304 }
305 let rank_length = params.rank_length.unwrap_or(52 * 7);
306 if rank_length == 0 {
307 return Err(HistoricalVolatilityRankError::InvalidRankLength { rank_length });
308 }
309 let annualization_days = params.annualization_days.unwrap_or(365.0);
310 if !annualization_days.is_finite() || annualization_days <= 0.0 {
311 return Err(HistoricalVolatilityRankError::InvalidAnnualizationDays {
312 annualization_days,
313 });
314 }
315 let bar_days = params.bar_days.unwrap_or(1.0);
316 if !bar_days.is_finite() || bar_days <= 0.0 {
317 return Err(HistoricalVolatilityRankError::InvalidBarDays { bar_days });
318 }
319
320 Ok(Self {
321 hv_length,
322 rank_length,
323 annualization_scale: (annualization_days / bar_days).sqrt(),
324 prev_close: None,
325 returns: vec![None; hv_length],
326 returns_sum: 0.0,
327 returns_sumsq: 0.0,
328 returns_valid: 0,
329 returns_idx: 0,
330 returns_count: 0,
331 hv_ring: vec![None; rank_length],
332 hv_valid: 0,
333 hv_idx: 0,
334 hv_count: 0,
335 min_q: VecDeque::with_capacity(rank_length),
336 max_q: VecDeque::with_capacity(rank_length),
337 tick: 0,
338 })
339 }
340
341 #[inline(always)]
342 pub fn update(&mut self, close: f64) -> Option<(f64, f64)> {
343 let valid_close = close.is_finite() && close > 0.0;
344 let ret = match (self.prev_close, valid_close) {
345 (Some(prev), true) => Some((close / prev).ln()),
346 _ => None,
347 };
348
349 self.prev_close = if valid_close { Some(close) } else { None };
350
351 if self.returns_count == self.hv_length {
352 if let Some(old) = self.returns[self.returns_idx] {
353 self.returns_sum -= old;
354 self.returns_sumsq -= old * old;
355 self.returns_valid -= 1;
356 }
357 } else {
358 self.returns_count += 1;
359 }
360
361 self.returns[self.returns_idx] = ret;
362 if let Some(value) = ret {
363 self.returns_sum += value;
364 self.returns_sumsq += value * value;
365 self.returns_valid += 1;
366 }
367 self.returns_idx += 1;
368 if self.returns_idx == self.hv_length {
369 self.returns_idx = 0;
370 }
371
372 let hv = if self.returns_count == self.hv_length && self.returns_valid == self.hv_length {
373 let n = self.hv_length as f64;
374 let mean = self.returns_sum / n;
375 let mut var = (self.returns_sumsq / n) - mean * mean;
376 if var < 0.0 {
377 var = 0.0;
378 }
379 Some(100.0 * var.sqrt() * self.annualization_scale)
380 } else {
381 None
382 };
383
384 let current_tick = self.tick;
385 self.tick += 1;
386
387 if self.hv_count == self.rank_length {
388 if self.hv_ring[self.hv_idx].is_some() {
389 self.hv_valid -= 1;
390 }
391 } else {
392 self.hv_count += 1;
393 }
394
395 self.hv_ring[self.hv_idx] = hv;
396 if let Some(value) = hv {
397 self.hv_valid += 1;
398 while let Some((_, tail)) = self.min_q.back() {
399 if *tail <= value {
400 break;
401 }
402 self.min_q.pop_back();
403 }
404 self.min_q.push_back((current_tick, value));
405 while let Some((_, tail)) = self.max_q.back() {
406 if *tail >= value {
407 break;
408 }
409 self.max_q.pop_back();
410 }
411 self.max_q.push_back((current_tick, value));
412 }
413 self.hv_idx += 1;
414 if self.hv_idx == self.rank_length {
415 self.hv_idx = 0;
416 }
417
418 let window_start = (current_tick + 1).saturating_sub(self.rank_length);
419 while let Some((idx, _)) = self.min_q.front() {
420 if *idx >= window_start {
421 break;
422 }
423 self.min_q.pop_front();
424 }
425 while let Some((idx, _)) = self.max_q.front() {
426 if *idx >= window_start {
427 break;
428 }
429 self.max_q.pop_front();
430 }
431
432 hv.map(|hv_value| {
433 let hvr = if self.hv_count == self.rank_length && self.hv_valid == self.rank_length {
434 let min_v = self.min_q.front().map(|(_, v)| *v).unwrap_or(hv_value);
435 let max_v = self.max_q.front().map(|(_, v)| *v).unwrap_or(hv_value);
436 let range = max_v - min_v;
437 if !range.is_finite() || range <= 0.0 {
438 0.0
439 } else {
440 100.0 * (hv_value - min_v) / range
441 }
442 } else {
443 f64::NAN
444 };
445 (hvr, hv_value)
446 })
447 }
448
449 #[inline(always)]
450 pub fn get_hv_warmup_period(&self) -> usize {
451 self.hv_length
452 }
453
454 #[inline(always)]
455 pub fn get_hvr_warmup_period(&self) -> usize {
456 self.hv_length + self.rank_length - 1
457 }
458}
459
460#[derive(Clone)]
461struct ReturnPrefixes {
462 sum: Vec<f64>,
463 sumsq: Vec<f64>,
464 invalid: Vec<u32>,
465}
466
467#[inline(always)]
468fn is_valid_price(value: f64) -> bool {
469 value.is_finite() && value > 0.0
470}
471
472#[inline(always)]
473fn longest_valid_run(data: &[f64]) -> usize {
474 let mut best = 0usize;
475 let mut cur = 0usize;
476 for &value in data {
477 if is_valid_price(value) {
478 cur += 1;
479 if cur > best {
480 best = cur;
481 }
482 } else {
483 cur = 0;
484 }
485 }
486 best
487}
488
489#[inline(always)]
490fn build_return_prefixes(close: &[f64]) -> ReturnPrefixes {
491 let len = close.len();
492 let mut sum = vec![0.0; len + 1];
493 let mut sumsq = vec![0.0; len + 1];
494 let mut invalid = vec![0u32; len + 1];
495
496 for i in 0..len {
497 let ret = if i > 0 && is_valid_price(close[i]) && is_valid_price(close[i - 1]) {
498 Some((close[i] / close[i - 1]).ln())
499 } else {
500 None
501 };
502
503 if let Some(value) = ret {
504 sum[i + 1] = sum[i] + value;
505 sumsq[i + 1] = sumsq[i] + value * value;
506 invalid[i + 1] = invalid[i];
507 } else {
508 sum[i + 1] = sum[i];
509 sumsq[i + 1] = sumsq[i];
510 invalid[i + 1] = invalid[i] + 1;
511 }
512 }
513
514 ReturnPrefixes {
515 sum,
516 sumsq,
517 invalid,
518 }
519}
520
521#[inline(always)]
522fn compute_hv_row_from_prefixes(
523 prefixes: &ReturnPrefixes,
524 len: usize,
525 hv_length: usize,
526 annualization_scale: f64,
527 out_hv: &mut [f64],
528) {
529 if hv_length >= len {
530 return;
531 }
532
533 let n = hv_length as f64;
534 for i in hv_length..len {
535 let start = i + 1 - hv_length;
536 if prefixes.invalid[i + 1] - prefixes.invalid[start] != 0 {
537 continue;
538 }
539
540 let sum = prefixes.sum[i + 1] - prefixes.sum[start];
541 let sumsq = prefixes.sumsq[i + 1] - prefixes.sumsq[start];
542 let mean = sum / n;
543 let mut var = (sumsq / n) - mean * mean;
544 if var < 0.0 {
545 var = 0.0;
546 }
547 out_hv[i] = 100.0 * var.sqrt() * annualization_scale;
548 }
549}
550
551#[inline(always)]
552fn compute_hvr_row_from_hv(hv: &[f64], rank_length: usize, out_hvr: &mut [f64]) {
553 let len = hv.len();
554 if rank_length == 0 || rank_length > len {
555 return;
556 }
557
558 let mut invalid = vec![0u32; len + 1];
559 for i in 0..len {
560 invalid[i + 1] = invalid[i] + u32::from(!hv[i].is_finite());
561 }
562
563 let mut min_q: VecDeque<usize> = VecDeque::with_capacity(rank_length);
564 let mut max_q: VecDeque<usize> = VecDeque::with_capacity(rank_length);
565
566 for i in 0..len {
567 let value = hv[i];
568 if value.is_finite() {
569 while let Some(&idx) = min_q.back() {
570 if hv[idx] <= value {
571 break;
572 }
573 min_q.pop_back();
574 }
575 min_q.push_back(i);
576
577 while let Some(&idx) = max_q.back() {
578 if hv[idx] >= value {
579 break;
580 }
581 max_q.pop_back();
582 }
583 max_q.push_back(i);
584 }
585
586 if i + 1 < rank_length {
587 continue;
588 }
589
590 let start = i + 1 - rank_length;
591 while let Some(&idx) = min_q.front() {
592 if idx >= start {
593 break;
594 }
595 min_q.pop_front();
596 }
597 while let Some(&idx) = max_q.front() {
598 if idx >= start {
599 break;
600 }
601 max_q.pop_front();
602 }
603
604 if invalid[i + 1] - invalid[start] != 0 {
605 continue;
606 }
607
608 let min_v = hv[*min_q.front().unwrap()];
609 let max_v = hv[*max_q.front().unwrap()];
610 let range = max_v - min_v;
611 out_hvr[i] = if !range.is_finite() || range <= 0.0 {
612 0.0
613 } else {
614 100.0 * (value - min_v) / range
615 };
616 }
617}
618
619#[inline(always)]
620fn validate_common(
621 data: &[f64],
622 hv_length: usize,
623 rank_length: usize,
624 annualization_days: f64,
625 bar_days: f64,
626) -> Result<(), HistoricalVolatilityRankError> {
627 let len = data.len();
628 if len == 0 {
629 return Err(HistoricalVolatilityRankError::EmptyInputData);
630 }
631 if hv_length == 0 || hv_length >= len {
632 return Err(HistoricalVolatilityRankError::InvalidHvLength {
633 hv_length,
634 data_len: len,
635 });
636 }
637 if rank_length == 0 {
638 return Err(HistoricalVolatilityRankError::InvalidRankLength { rank_length });
639 }
640 if !annualization_days.is_finite() || annualization_days <= 0.0 {
641 return Err(HistoricalVolatilityRankError::InvalidAnnualizationDays { annualization_days });
642 }
643 if !bar_days.is_finite() || bar_days <= 0.0 {
644 return Err(HistoricalVolatilityRankError::InvalidBarDays { bar_days });
645 }
646
647 let max_run = longest_valid_run(data);
648 if max_run == 0 {
649 return Err(HistoricalVolatilityRankError::AllValuesNaN);
650 }
651 if max_run <= hv_length {
652 return Err(HistoricalVolatilityRankError::NotEnoughValidData {
653 needed: hv_length + 1,
654 valid: max_run,
655 });
656 }
657 Ok(())
658}
659
660#[inline]
661pub fn historical_volatility_rank(
662 input: &HistoricalVolatilityRankInput,
663) -> Result<HistoricalVolatilityRankOutput, HistoricalVolatilityRankError> {
664 historical_volatility_rank_with_kernel(input, Kernel::Auto)
665}
666
667pub fn historical_volatility_rank_with_kernel(
668 input: &HistoricalVolatilityRankInput,
669 kernel: Kernel,
670) -> Result<HistoricalVolatilityRankOutput, HistoricalVolatilityRankError> {
671 let data: &[f64] = input.as_ref();
672 let hv_length = input.get_hv_length();
673 let rank_length = input.get_rank_length();
674 let annualization_days = input.get_annualization_days();
675 let bar_days = input.get_bar_days();
676 validate_common(data, hv_length, rank_length, annualization_days, bar_days)?;
677
678 let len = data.len();
679 let mut hvr = alloc_with_nan_prefix(len, hv_length + rank_length - 1);
680 let mut hv = alloc_with_nan_prefix(len, hv_length);
681 historical_volatility_rank_into_slice(&mut hvr, &mut hv, input, kernel)?;
682 Ok(HistoricalVolatilityRankOutput { hvr, hv })
683}
684
685pub fn historical_volatility_rank_into_slice(
686 dst_hvr: &mut [f64],
687 dst_hv: &mut [f64],
688 input: &HistoricalVolatilityRankInput,
689 kernel: Kernel,
690) -> Result<(), HistoricalVolatilityRankError> {
691 let data: &[f64] = input.as_ref();
692 let len = data.len();
693 if dst_hvr.len() != len {
694 return Err(HistoricalVolatilityRankError::MismatchedOutputLen {
695 dst_len: dst_hvr.len(),
696 expected_len: len,
697 });
698 }
699 if dst_hv.len() != len {
700 return Err(HistoricalVolatilityRankError::MismatchedOutputLen {
701 dst_len: dst_hv.len(),
702 expected_len: len,
703 });
704 }
705
706 let hv_length = input.get_hv_length();
707 let rank_length = input.get_rank_length();
708 let annualization_days = input.get_annualization_days();
709 let bar_days = input.get_bar_days();
710 validate_common(data, hv_length, rank_length, annualization_days, bar_days)?;
711
712 let _chosen = match kernel {
713 Kernel::Auto => detect_best_kernel(),
714 other => other,
715 };
716
717 dst_hvr.fill(f64::NAN);
718 dst_hv.fill(f64::NAN);
719
720 let prefixes = build_return_prefixes(data);
721 let scale = (annualization_days / bar_days).sqrt();
722 compute_hv_row_from_prefixes(&prefixes, len, hv_length, scale, dst_hv);
723 compute_hvr_row_from_hv(dst_hv, rank_length, dst_hvr);
724 Ok(())
725}
726
727#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
728#[inline]
729pub fn historical_volatility_rank_into(
730 input: &HistoricalVolatilityRankInput,
731 out_hvr: &mut [f64],
732 out_hv: &mut [f64],
733) -> Result<(), HistoricalVolatilityRankError> {
734 historical_volatility_rank_into_slice(out_hvr, out_hv, input, Kernel::Auto)
735}
736
737#[derive(Debug, Clone)]
738#[cfg_attr(
739 all(target_arch = "wasm32", feature = "wasm"),
740 derive(Serialize, Deserialize)
741)]
742pub struct HistoricalVolatilityRankBatchRange {
743 pub hv_length: (usize, usize, usize),
744 pub rank_length: (usize, usize, usize),
745 pub annualization_days: (f64, f64, f64),
746 pub bar_days: (f64, f64, f64),
747}
748
749impl Default for HistoricalVolatilityRankBatchRange {
750 fn default() -> Self {
751 Self {
752 hv_length: (10, 252, 1),
753 rank_length: (52 * 7, 52 * 7, 0),
754 annualization_days: (365.0, 365.0, 0.0),
755 bar_days: (1.0, 1.0, 0.0),
756 }
757 }
758}
759
760#[derive(Debug, Clone, Default)]
761pub struct HistoricalVolatilityRankBatchBuilder {
762 range: HistoricalVolatilityRankBatchRange,
763 kernel: Kernel,
764}
765
766impl HistoricalVolatilityRankBatchBuilder {
767 pub fn new() -> Self {
768 Self::default()
769 }
770
771 pub fn kernel(mut self, value: Kernel) -> Self {
772 self.kernel = value;
773 self
774 }
775
776 #[inline]
777 pub fn hv_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
778 self.range.hv_length = (start, end, step);
779 self
780 }
781
782 #[inline]
783 pub fn hv_length_static(mut self, value: usize) -> Self {
784 self.range.hv_length = (value, value, 0);
785 self
786 }
787
788 #[inline]
789 pub fn rank_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
790 self.range.rank_length = (start, end, step);
791 self
792 }
793
794 #[inline]
795 pub fn rank_length_static(mut self, value: usize) -> Self {
796 self.range.rank_length = (value, value, 0);
797 self
798 }
799
800 #[inline]
801 pub fn annualization_days_range(mut self, start: f64, end: f64, step: f64) -> Self {
802 self.range.annualization_days = (start, end, step);
803 self
804 }
805
806 #[inline]
807 pub fn annualization_days_static(mut self, value: f64) -> Self {
808 self.range.annualization_days = (value, value, 0.0);
809 self
810 }
811
812 #[inline]
813 pub fn bar_days_range(mut self, start: f64, end: f64, step: f64) -> Self {
814 self.range.bar_days = (start, end, step);
815 self
816 }
817
818 #[inline]
819 pub fn bar_days_static(mut self, value: f64) -> Self {
820 self.range.bar_days = (value, value, 0.0);
821 self
822 }
823
824 pub fn apply_slice(
825 self,
826 data: &[f64],
827 ) -> Result<HistoricalVolatilityRankBatchOutput, HistoricalVolatilityRankError> {
828 historical_volatility_rank_batch_with_kernel(data, &self.range, self.kernel)
829 }
830
831 pub fn apply_candles(
832 self,
833 candles: &Candles,
834 ) -> Result<HistoricalVolatilityRankBatchOutput, HistoricalVolatilityRankError> {
835 self.apply_slice(&candles.close)
836 }
837}
838
839#[derive(Debug, Clone)]
840pub struct HistoricalVolatilityRankBatchOutput {
841 pub hvr: Vec<f64>,
842 pub hv: Vec<f64>,
843 pub combos: Vec<HistoricalVolatilityRankParams>,
844 pub rows: usize,
845 pub cols: usize,
846}
847
848impl HistoricalVolatilityRankBatchOutput {
849 pub fn row_for_params(&self, params: &HistoricalVolatilityRankParams) -> Option<usize> {
850 let hv_length = params.hv_length.unwrap_or(10);
851 let rank_length = params.rank_length.unwrap_or(52 * 7);
852 let annualization_days = params.annualization_days.unwrap_or(365.0);
853 let bar_days = params.bar_days.unwrap_or(1.0);
854 self.combos.iter().position(|combo| {
855 combo.hv_length.unwrap_or(10) == hv_length
856 && combo.rank_length.unwrap_or(52 * 7) == rank_length
857 && (combo.annualization_days.unwrap_or(365.0) - annualization_days).abs() < 1e-12
858 && (combo.bar_days.unwrap_or(1.0) - bar_days).abs() < 1e-12
859 })
860 }
861
862 pub fn hvr_for(&self, params: &HistoricalVolatilityRankParams) -> Option<&[f64]> {
863 self.row_for_params(params).and_then(|row| {
864 let start = row.checked_mul(self.cols)?;
865 self.hvr.get(start..start + self.cols)
866 })
867 }
868
869 pub fn hv_for(&self, params: &HistoricalVolatilityRankParams) -> Option<&[f64]> {
870 self.row_for_params(params).and_then(|row| {
871 let start = row.checked_mul(self.cols)?;
872 self.hv.get(start..start + self.cols)
873 })
874 }
875}
876
877#[inline(always)]
878fn expand_grid_checked(
879 range: &HistoricalVolatilityRankBatchRange,
880) -> Result<Vec<HistoricalVolatilityRankParams>, HistoricalVolatilityRankError> {
881 fn axis_usize(
882 (start, end, step): (usize, usize, usize),
883 ) -> Result<Vec<usize>, HistoricalVolatilityRankError> {
884 if step == 0 || start == end {
885 return Ok(vec![start]);
886 }
887
888 let mut out = Vec::new();
889 if start < end {
890 let mut cur = start;
891 while cur <= end {
892 out.push(cur);
893 let next = cur.saturating_add(step.max(1));
894 if next == cur {
895 break;
896 }
897 cur = next;
898 }
899 } else {
900 let mut cur = start;
901 loop {
902 out.push(cur);
903 if cur == end {
904 break;
905 }
906 let next = cur.saturating_sub(step.max(1));
907 if next == cur || next < end {
908 break;
909 }
910 cur = next;
911 }
912 }
913
914 if out.is_empty() {
915 return Err(HistoricalVolatilityRankError::InvalidRange {
916 start: start.to_string(),
917 end: end.to_string(),
918 step: step.to_string(),
919 });
920 }
921 Ok(out)
922 }
923
924 fn axis_f64(
925 (start, end, step): (f64, f64, f64),
926 ) -> Result<Vec<f64>, HistoricalVolatilityRankError> {
927 if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
928 return Ok(vec![start]);
929 }
930
931 let mut out = Vec::new();
932 if start < end {
933 let step = step.abs();
934 let mut cur = start;
935 while cur <= end + 1e-12 {
936 out.push(cur);
937 cur += step;
938 }
939 } else {
940 let step = step.abs();
941 let mut cur = start;
942 while cur >= end - 1e-12 {
943 out.push(cur);
944 cur -= step;
945 }
946 }
947
948 if out.is_empty() {
949 return Err(HistoricalVolatilityRankError::InvalidRange {
950 start: start.to_string(),
951 end: end.to_string(),
952 step: step.to_string(),
953 });
954 }
955 Ok(out)
956 }
957
958 let hv_lengths = axis_usize(range.hv_length)?;
959 if hv_lengths.iter().any(|&value| value == 0) {
960 return Err(HistoricalVolatilityRankError::InvalidHvLength {
961 hv_length: 0,
962 data_len: 0,
963 });
964 }
965 let rank_lengths = axis_usize(range.rank_length)?;
966 if rank_lengths.iter().any(|&value| value == 0) {
967 return Err(HistoricalVolatilityRankError::InvalidRankLength { rank_length: 0 });
968 }
969 let annualization_days = axis_f64(range.annualization_days)?;
970 let bar_days = axis_f64(range.bar_days)?;
971
972 let cap = hv_lengths
973 .len()
974 .checked_mul(rank_lengths.len())
975 .and_then(|v| v.checked_mul(annualization_days.len()))
976 .and_then(|v| v.checked_mul(bar_days.len()))
977 .ok_or_else(|| HistoricalVolatilityRankError::InvalidInput {
978 msg: "historical_volatility_rank: parameter grid size overflow".to_string(),
979 })?;
980
981 let mut out = Vec::with_capacity(cap);
982 for &hv_length in &hv_lengths {
983 for &rank_length in &rank_lengths {
984 for &annualization_day in &annualization_days {
985 for &bar_day in &bar_days {
986 out.push(HistoricalVolatilityRankParams {
987 hv_length: Some(hv_length),
988 rank_length: Some(rank_length),
989 annualization_days: Some(annualization_day),
990 bar_days: Some(bar_day),
991 });
992 }
993 }
994 }
995 }
996 Ok(out)
997}
998
999pub fn historical_volatility_rank_batch_with_kernel(
1000 data: &[f64],
1001 sweep: &HistoricalVolatilityRankBatchRange,
1002 kernel: Kernel,
1003) -> Result<HistoricalVolatilityRankBatchOutput, HistoricalVolatilityRankError> {
1004 let batch_kernel = match kernel {
1005 Kernel::Auto => detect_best_batch_kernel(),
1006 other if other.is_batch() => other,
1007 other => return Err(HistoricalVolatilityRankError::InvalidKernelForBatch(other)),
1008 };
1009 historical_volatility_rank_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
1010}
1011
1012#[inline(always)]
1013pub fn historical_volatility_rank_batch_slice(
1014 data: &[f64],
1015 sweep: &HistoricalVolatilityRankBatchRange,
1016 kernel: Kernel,
1017) -> Result<HistoricalVolatilityRankBatchOutput, HistoricalVolatilityRankError> {
1018 historical_volatility_rank_batch_inner(data, sweep, kernel, false)
1019}
1020
1021#[inline(always)]
1022pub fn historical_volatility_rank_batch_par_slice(
1023 data: &[f64],
1024 sweep: &HistoricalVolatilityRankBatchRange,
1025 kernel: Kernel,
1026) -> Result<HistoricalVolatilityRankBatchOutput, HistoricalVolatilityRankError> {
1027 historical_volatility_rank_batch_inner(data, sweep, kernel, true)
1028}
1029
1030#[inline(always)]
1031fn historical_volatility_rank_batch_inner(
1032 data: &[f64],
1033 sweep: &HistoricalVolatilityRankBatchRange,
1034 kernel: Kernel,
1035 parallel: bool,
1036) -> Result<HistoricalVolatilityRankBatchOutput, HistoricalVolatilityRankError> {
1037 let combos = expand_grid_checked(sweep)?;
1038 if data.is_empty() {
1039 return Err(HistoricalVolatilityRankError::EmptyInputData);
1040 }
1041
1042 let max_run = longest_valid_run(data);
1043 if max_run == 0 {
1044 return Err(HistoricalVolatilityRankError::AllValuesNaN);
1045 }
1046
1047 let max_hv_length = combos
1048 .iter()
1049 .map(|params| params.hv_length.unwrap_or(10))
1050 .max()
1051 .unwrap_or(0);
1052 if max_hv_length >= data.len() {
1053 return Err(HistoricalVolatilityRankError::InvalidHvLength {
1054 hv_length: max_hv_length,
1055 data_len: data.len(),
1056 });
1057 }
1058 if max_run <= max_hv_length {
1059 return Err(HistoricalVolatilityRankError::NotEnoughValidData {
1060 needed: max_hv_length + 1,
1061 valid: max_run,
1062 });
1063 }
1064
1065 let rows = combos.len();
1066 let cols = data.len();
1067 let total =
1068 rows.checked_mul(cols)
1069 .ok_or_else(|| HistoricalVolatilityRankError::InvalidInput {
1070 msg: "historical_volatility_rank: rows*cols overflow in batch".to_string(),
1071 })?;
1072
1073 let mut hvr_mu = make_uninit_matrix(rows, cols);
1074 let mut hv_mu = make_uninit_matrix(rows, cols);
1075 let hvr_warmups: Vec<usize> = combos
1076 .iter()
1077 .map(|params| params.hv_length.unwrap_or(10) + params.rank_length.unwrap_or(52 * 7) - 1)
1078 .collect();
1079 let hv_warmups: Vec<usize> = combos
1080 .iter()
1081 .map(|params| params.hv_length.unwrap_or(10))
1082 .collect();
1083
1084 init_matrix_prefixes(&mut hvr_mu, cols, &hvr_warmups);
1085 init_matrix_prefixes(&mut hv_mu, cols, &hv_warmups);
1086
1087 let mut hvr = unsafe {
1088 Vec::from_raw_parts(
1089 hvr_mu.as_mut_ptr() as *mut f64,
1090 hvr_mu.len(),
1091 hvr_mu.capacity(),
1092 )
1093 };
1094 let mut hv = unsafe {
1095 Vec::from_raw_parts(
1096 hv_mu.as_mut_ptr() as *mut f64,
1097 hv_mu.len(),
1098 hv_mu.capacity(),
1099 )
1100 };
1101 std::mem::forget(hvr_mu);
1102 std::mem::forget(hv_mu);
1103
1104 debug_assert_eq!(hvr.len(), total);
1105 debug_assert_eq!(hv.len(), total);
1106
1107 historical_volatility_rank_batch_inner_into(data, sweep, kernel, parallel, &mut hvr, &mut hv)?;
1108
1109 Ok(HistoricalVolatilityRankBatchOutput {
1110 hvr,
1111 hv,
1112 combos,
1113 rows,
1114 cols,
1115 })
1116}
1117
1118#[inline(always)]
1119fn historical_volatility_rank_batch_inner_into(
1120 data: &[f64],
1121 sweep: &HistoricalVolatilityRankBatchRange,
1122 kernel: Kernel,
1123 parallel: bool,
1124 out_hvr: &mut [f64],
1125 out_hv: &mut [f64],
1126) -> Result<Vec<HistoricalVolatilityRankParams>, HistoricalVolatilityRankError> {
1127 let combos = expand_grid_checked(sweep)?;
1128 let len = data.len();
1129 if len == 0 {
1130 return Err(HistoricalVolatilityRankError::EmptyInputData);
1131 }
1132
1133 let total = combos.len().checked_mul(len).ok_or_else(|| {
1134 HistoricalVolatilityRankError::InvalidInput {
1135 msg: "historical_volatility_rank: rows*cols overflow in batch_into".to_string(),
1136 }
1137 })?;
1138 if out_hvr.len() != total {
1139 return Err(HistoricalVolatilityRankError::MismatchedOutputLen {
1140 dst_len: out_hvr.len(),
1141 expected_len: total,
1142 });
1143 }
1144 if out_hv.len() != total {
1145 return Err(HistoricalVolatilityRankError::MismatchedOutputLen {
1146 dst_len: out_hv.len(),
1147 expected_len: total,
1148 });
1149 }
1150
1151 let max_run = longest_valid_run(data);
1152 if max_run == 0 {
1153 return Err(HistoricalVolatilityRankError::AllValuesNaN);
1154 }
1155 let max_hv_length = combos
1156 .iter()
1157 .map(|params| params.hv_length.unwrap_or(10))
1158 .max()
1159 .unwrap_or(0);
1160 if max_hv_length >= len {
1161 return Err(HistoricalVolatilityRankError::InvalidHvLength {
1162 hv_length: max_hv_length,
1163 data_len: len,
1164 });
1165 }
1166 if max_run <= max_hv_length {
1167 return Err(HistoricalVolatilityRankError::NotEnoughValidData {
1168 needed: max_hv_length + 1,
1169 valid: max_run,
1170 });
1171 }
1172
1173 let _chosen = match kernel {
1174 Kernel::Auto => detect_best_kernel(),
1175 other => other,
1176 };
1177
1178 let prefixes = build_return_prefixes(data);
1179 let worker = |row: usize, dst_hvr: &mut [f64], dst_hv: &mut [f64]| {
1180 dst_hvr.fill(f64::NAN);
1181 dst_hv.fill(f64::NAN);
1182 let params = &combos[row];
1183 let scale =
1184 (params.annualization_days.unwrap_or(365.0) / params.bar_days.unwrap_or(1.0)).sqrt();
1185 compute_hv_row_from_prefixes(
1186 &prefixes,
1187 len,
1188 params.hv_length.unwrap_or(10),
1189 scale,
1190 dst_hv,
1191 );
1192 compute_hvr_row_from_hv(dst_hv, params.rank_length.unwrap_or(52 * 7), dst_hvr);
1193 };
1194
1195 #[cfg(not(target_arch = "wasm32"))]
1196 if parallel {
1197 out_hvr
1198 .par_chunks_mut(len)
1199 .zip(out_hv.par_chunks_mut(len))
1200 .enumerate()
1201 .for_each(|(row, (dst_hvr, dst_hv))| worker(row, dst_hvr, dst_hv));
1202 } else {
1203 for (row, (dst_hvr, dst_hv)) in out_hvr
1204 .chunks_mut(len)
1205 .zip(out_hv.chunks_mut(len))
1206 .enumerate()
1207 {
1208 worker(row, dst_hvr, dst_hv);
1209 }
1210 }
1211
1212 #[cfg(target_arch = "wasm32")]
1213 {
1214 let _ = parallel;
1215 for (row, (dst_hvr, dst_hv)) in out_hvr
1216 .chunks_mut(len)
1217 .zip(out_hv.chunks_mut(len))
1218 .enumerate()
1219 {
1220 worker(row, dst_hvr, dst_hv);
1221 }
1222 }
1223
1224 Ok(combos)
1225}
1226
1227#[inline(always)]
1228pub fn expand_grid_historical_volatility_rank(
1229 range: &HistoricalVolatilityRankBatchRange,
1230) -> Vec<HistoricalVolatilityRankParams> {
1231 expand_grid_checked(range).unwrap_or_default()
1232}
1233
1234#[cfg(feature = "python")]
1235#[pyfunction(name = "historical_volatility_rank")]
1236#[pyo3(signature = (data, hv_length=10, rank_length=52*7, annualization_days=365.0, bar_days=1.0, kernel=None))]
1237pub fn historical_volatility_rank_py<'py>(
1238 py: Python<'py>,
1239 data: PyReadonlyArray1<'py, f64>,
1240 hv_length: usize,
1241 rank_length: usize,
1242 annualization_days: f64,
1243 bar_days: f64,
1244 kernel: Option<&str>,
1245) -> PyResult<(
1246 Bound<'py, numpy::PyArray1<f64>>,
1247 Bound<'py, numpy::PyArray1<f64>>,
1248)> {
1249 let slice_in = data.as_slice()?;
1250 let kern = validate_kernel(kernel, false)?;
1251 let input = HistoricalVolatilityRankInput::from_slice(
1252 slice_in,
1253 HistoricalVolatilityRankParams {
1254 hv_length: Some(hv_length),
1255 rank_length: Some(rank_length),
1256 annualization_days: Some(annualization_days),
1257 bar_days: Some(bar_days),
1258 },
1259 );
1260 let out = py
1261 .allow_threads(|| historical_volatility_rank_with_kernel(&input, kern))
1262 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1263 Ok((out.hvr.into_pyarray(py), out.hv.into_pyarray(py)))
1264}
1265
1266#[cfg(feature = "python")]
1267#[pyclass(name = "HistoricalVolatilityRankStream")]
1268pub struct HistoricalVolatilityRankStreamPy {
1269 stream: HistoricalVolatilityRankStream,
1270}
1271
1272#[cfg(feature = "python")]
1273#[pymethods]
1274impl HistoricalVolatilityRankStreamPy {
1275 #[new]
1276 fn new(
1277 hv_length: usize,
1278 rank_length: usize,
1279 annualization_days: f64,
1280 bar_days: f64,
1281 ) -> PyResult<Self> {
1282 let stream = HistoricalVolatilityRankStream::try_new(HistoricalVolatilityRankParams {
1283 hv_length: Some(hv_length),
1284 rank_length: Some(rank_length),
1285 annualization_days: Some(annualization_days),
1286 bar_days: Some(bar_days),
1287 })
1288 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1289 Ok(Self { stream })
1290 }
1291
1292 fn update(&mut self, close: f64) -> Option<(f64, f64)> {
1293 self.stream.update(close)
1294 }
1295}
1296
1297#[cfg(feature = "python")]
1298#[pyfunction(name = "historical_volatility_rank_batch")]
1299#[pyo3(signature = (data, hv_length_range=(10,10,0), rank_length_range=(52*7,52*7,0), annualization_days_range=(365.0,365.0,0.0), bar_days_range=(1.0,1.0,0.0), kernel=None))]
1300pub fn historical_volatility_rank_batch_py<'py>(
1301 py: Python<'py>,
1302 data: PyReadonlyArray1<'py, f64>,
1303 hv_length_range: (usize, usize, usize),
1304 rank_length_range: (usize, usize, usize),
1305 annualization_days_range: (f64, f64, f64),
1306 bar_days_range: (f64, f64, f64),
1307 kernel: Option<&str>,
1308) -> PyResult<Bound<'py, PyDict>> {
1309 let slice_in = data.as_slice()?;
1310 let kern = validate_kernel(kernel, true)?;
1311 let sweep = HistoricalVolatilityRankBatchRange {
1312 hv_length: hv_length_range,
1313 rank_length: rank_length_range,
1314 annualization_days: annualization_days_range,
1315 bar_days: bar_days_range,
1316 };
1317
1318 let output = py
1319 .allow_threads(|| historical_volatility_rank_batch_with_kernel(slice_in, &sweep, kern))
1320 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1321
1322 let rows = output.rows;
1323 let cols = output.cols;
1324 let dict = PyDict::new(py);
1325 dict.set_item("hvr", output.hvr.into_pyarray(py).reshape((rows, cols))?)?;
1326 dict.set_item("hv", output.hv.into_pyarray(py).reshape((rows, cols))?)?;
1327 dict.set_item(
1328 "hv_lengths",
1329 output
1330 .combos
1331 .iter()
1332 .map(|params| params.hv_length.unwrap_or(10) as u64)
1333 .collect::<Vec<_>>()
1334 .into_pyarray(py),
1335 )?;
1336 dict.set_item(
1337 "rank_lengths",
1338 output
1339 .combos
1340 .iter()
1341 .map(|params| params.rank_length.unwrap_or(52 * 7) as u64)
1342 .collect::<Vec<_>>()
1343 .into_pyarray(py),
1344 )?;
1345 dict.set_item(
1346 "annualization_days",
1347 output
1348 .combos
1349 .iter()
1350 .map(|params| params.annualization_days.unwrap_or(365.0))
1351 .collect::<Vec<_>>()
1352 .into_pyarray(py),
1353 )?;
1354 dict.set_item(
1355 "bar_days",
1356 output
1357 .combos
1358 .iter()
1359 .map(|params| params.bar_days.unwrap_or(1.0))
1360 .collect::<Vec<_>>()
1361 .into_pyarray(py),
1362 )?;
1363 dict.set_item("rows", rows)?;
1364 dict.set_item("cols", cols)?;
1365 Ok(dict)
1366}
1367
1368#[cfg(feature = "python")]
1369pub fn register_historical_volatility_rank_module(
1370 m: &Bound<'_, pyo3::types::PyModule>,
1371) -> PyResult<()> {
1372 m.add_function(wrap_pyfunction!(historical_volatility_rank_py, m)?)?;
1373 m.add_function(wrap_pyfunction!(historical_volatility_rank_batch_py, m)?)?;
1374 m.add_class::<HistoricalVolatilityRankStreamPy>()?;
1375 Ok(())
1376}
1377
1378#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1379#[wasm_bindgen(js_name = historical_volatility_rank_js)]
1380pub fn historical_volatility_rank_js(
1381 data: &[f64],
1382 hv_length: usize,
1383 rank_length: usize,
1384 annualization_days: f64,
1385 bar_days: f64,
1386) -> Result<JsValue, JsValue> {
1387 let input = HistoricalVolatilityRankInput::from_slice(
1388 data,
1389 HistoricalVolatilityRankParams {
1390 hv_length: Some(hv_length),
1391 rank_length: Some(rank_length),
1392 annualization_days: Some(annualization_days),
1393 bar_days: Some(bar_days),
1394 },
1395 );
1396 let out = historical_volatility_rank_with_kernel(&input, Kernel::Auto)
1397 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1398
1399 let obj = js_sys::Object::new();
1400 js_sys::Reflect::set(
1401 &obj,
1402 &JsValue::from_str("hvr"),
1403 &serde_wasm_bindgen::to_value(&out.hvr).unwrap(),
1404 )?;
1405 js_sys::Reflect::set(
1406 &obj,
1407 &JsValue::from_str("hv"),
1408 &serde_wasm_bindgen::to_value(&out.hv).unwrap(),
1409 )?;
1410 Ok(obj.into())
1411}
1412
1413#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1414#[derive(Debug, Clone, Serialize, Deserialize)]
1415pub struct HistoricalVolatilityRankBatchConfig {
1416 pub hv_length_range: Vec<usize>,
1417 pub rank_length_range: Vec<usize>,
1418 pub annualization_days_range: Vec<f64>,
1419 pub bar_days_range: Vec<f64>,
1420}
1421
1422#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1423#[wasm_bindgen(js_name = historical_volatility_rank_batch_js)]
1424pub fn historical_volatility_rank_batch_js(
1425 data: &[f64],
1426 config: JsValue,
1427) -> Result<JsValue, JsValue> {
1428 let config: HistoricalVolatilityRankBatchConfig = serde_wasm_bindgen::from_value(config)
1429 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1430
1431 if config.hv_length_range.len() != 3 {
1432 return Err(JsValue::from_str(
1433 "Invalid config: hv_length_range must have exactly 3 elements [start, end, step]",
1434 ));
1435 }
1436 if config.rank_length_range.len() != 3 {
1437 return Err(JsValue::from_str(
1438 "Invalid config: rank_length_range must have exactly 3 elements [start, end, step]",
1439 ));
1440 }
1441 if config.annualization_days_range.len() != 3 {
1442 return Err(JsValue::from_str(
1443 "Invalid config: annualization_days_range must have exactly 3 elements [start, end, step]",
1444 ));
1445 }
1446 if config.bar_days_range.len() != 3 {
1447 return Err(JsValue::from_str(
1448 "Invalid config: bar_days_range must have exactly 3 elements [start, end, step]",
1449 ));
1450 }
1451
1452 let sweep = HistoricalVolatilityRankBatchRange {
1453 hv_length: (
1454 config.hv_length_range[0],
1455 config.hv_length_range[1],
1456 config.hv_length_range[2],
1457 ),
1458 rank_length: (
1459 config.rank_length_range[0],
1460 config.rank_length_range[1],
1461 config.rank_length_range[2],
1462 ),
1463 annualization_days: (
1464 config.annualization_days_range[0],
1465 config.annualization_days_range[1],
1466 config.annualization_days_range[2],
1467 ),
1468 bar_days: (
1469 config.bar_days_range[0],
1470 config.bar_days_range[1],
1471 config.bar_days_range[2],
1472 ),
1473 };
1474
1475 let out = historical_volatility_rank_batch_with_kernel(data, &sweep, Kernel::Auto)
1476 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1477
1478 let obj = js_sys::Object::new();
1479 js_sys::Reflect::set(
1480 &obj,
1481 &JsValue::from_str("hvr"),
1482 &serde_wasm_bindgen::to_value(&out.hvr).unwrap(),
1483 )?;
1484 js_sys::Reflect::set(
1485 &obj,
1486 &JsValue::from_str("hv"),
1487 &serde_wasm_bindgen::to_value(&out.hv).unwrap(),
1488 )?;
1489 js_sys::Reflect::set(
1490 &obj,
1491 &JsValue::from_str("rows"),
1492 &JsValue::from_f64(out.rows as f64),
1493 )?;
1494 js_sys::Reflect::set(
1495 &obj,
1496 &JsValue::from_str("cols"),
1497 &JsValue::from_f64(out.cols as f64),
1498 )?;
1499 js_sys::Reflect::set(
1500 &obj,
1501 &JsValue::from_str("combos"),
1502 &serde_wasm_bindgen::to_value(&out.combos).unwrap(),
1503 )?;
1504 Ok(obj.into())
1505}
1506
1507#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1508#[wasm_bindgen]
1509pub fn historical_volatility_rank_alloc(len: usize) -> *mut f64 {
1510 let mut vec = Vec::<f64>::with_capacity(2 * len);
1511 let ptr = vec.as_mut_ptr();
1512 std::mem::forget(vec);
1513 ptr
1514}
1515
1516#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1517#[wasm_bindgen]
1518pub fn historical_volatility_rank_free(ptr: *mut f64, len: usize) {
1519 if !ptr.is_null() {
1520 unsafe {
1521 let _ = Vec::from_raw_parts(ptr, 2 * len, 2 * len);
1522 }
1523 }
1524}
1525
1526#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1527#[wasm_bindgen]
1528pub fn historical_volatility_rank_into(
1529 in_ptr: *const f64,
1530 out_ptr: *mut f64,
1531 len: usize,
1532 hv_length: usize,
1533 rank_length: usize,
1534 annualization_days: f64,
1535 bar_days: f64,
1536) -> Result<(), JsValue> {
1537 if in_ptr.is_null() || out_ptr.is_null() {
1538 return Err(JsValue::from_str(
1539 "null pointer passed to historical_volatility_rank_into",
1540 ));
1541 }
1542 unsafe {
1543 let data = std::slice::from_raw_parts(in_ptr, len);
1544 let out = std::slice::from_raw_parts_mut(out_ptr, 2 * len);
1545 let (dst_hvr, dst_hv) = out.split_at_mut(len);
1546 let input = HistoricalVolatilityRankInput::from_slice(
1547 data,
1548 HistoricalVolatilityRankParams {
1549 hv_length: Some(hv_length),
1550 rank_length: Some(rank_length),
1551 annualization_days: Some(annualization_days),
1552 bar_days: Some(bar_days),
1553 },
1554 );
1555 historical_volatility_rank_into_slice(dst_hvr, dst_hv, &input, Kernel::Auto)
1556 .map_err(|e| JsValue::from_str(&e.to_string()))
1557 }
1558}
1559
1560#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1561#[wasm_bindgen]
1562pub fn historical_volatility_rank_batch_into(
1563 in_ptr: *const f64,
1564 out_ptr: *mut f64,
1565 len: usize,
1566 hv_length_start: usize,
1567 hv_length_end: usize,
1568 hv_length_step: usize,
1569 rank_length_start: usize,
1570 rank_length_end: usize,
1571 rank_length_step: usize,
1572 annualization_days_start: f64,
1573 annualization_days_end: f64,
1574 annualization_days_step: f64,
1575 bar_days_start: f64,
1576 bar_days_end: f64,
1577 bar_days_step: f64,
1578) -> Result<usize, JsValue> {
1579 if in_ptr.is_null() || out_ptr.is_null() {
1580 return Err(JsValue::from_str(
1581 "null pointer passed to historical_volatility_rank_batch_into",
1582 ));
1583 }
1584
1585 let sweep = HistoricalVolatilityRankBatchRange {
1586 hv_length: (hv_length_start, hv_length_end, hv_length_step),
1587 rank_length: (rank_length_start, rank_length_end, rank_length_step),
1588 annualization_days: (
1589 annualization_days_start,
1590 annualization_days_end,
1591 annualization_days_step,
1592 ),
1593 bar_days: (bar_days_start, bar_days_end, bar_days_step),
1594 };
1595 let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1596 let rows = combos.len();
1597 let total = rows
1598 .checked_mul(len)
1599 .and_then(|v| v.checked_mul(2))
1600 .ok_or_else(|| {
1601 JsValue::from_str("rows*cols overflow in historical_volatility_rank_batch_into")
1602 })?;
1603
1604 unsafe {
1605 let data = std::slice::from_raw_parts(in_ptr, len);
1606 let out = std::slice::from_raw_parts_mut(out_ptr, total);
1607 let split = rows * len;
1608 let (dst_hvr, dst_hv) = out.split_at_mut(split);
1609 historical_volatility_rank_batch_inner_into(
1610 data,
1611 &sweep,
1612 Kernel::Auto,
1613 false,
1614 dst_hvr,
1615 dst_hv,
1616 )
1617 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1618 }
1619
1620 Ok(rows)
1621}
1622
1623#[cfg(test)]
1624mod tests {
1625 use super::*;
1626 use crate::indicators::dispatch::{
1627 compute_cpu, IndicatorComputeRequest, IndicatorDataRef, ParamKV, ParamValue,
1628 };
1629
1630 fn sample_close(len: usize) -> Vec<f64> {
1631 (0..len)
1632 .map(|i| {
1633 let x = i as f64;
1634 100.0 + 0.3 * x + 1.5 * (x * 0.07).sin() + 0.5 * (x * 0.03).cos()
1635 })
1636 .collect()
1637 }
1638
1639 fn naive_hvr(
1640 close: &[f64],
1641 hv_length: usize,
1642 rank_length: usize,
1643 annualization_days: f64,
1644 bar_days: f64,
1645 ) -> (Vec<f64>, Vec<f64>) {
1646 let len = close.len();
1647 let mut hvr = vec![f64::NAN; len];
1648 let mut hv = vec![f64::NAN; len];
1649 let scale = (annualization_days / bar_days).sqrt();
1650
1651 for i in hv_length..len {
1652 let start = i + 1 - hv_length;
1653 let mut returns = Vec::with_capacity(hv_length);
1654 for j in start..=i {
1655 if !is_valid_price(close[j]) || !is_valid_price(close[j - 1]) {
1656 returns.clear();
1657 break;
1658 }
1659 returns.push((close[j] / close[j - 1]).ln());
1660 }
1661 if returns.len() == hv_length {
1662 let mean = returns.iter().sum::<f64>() / hv_length as f64;
1663 let var = returns
1664 .iter()
1665 .map(|v| {
1666 let d = *v - mean;
1667 d * d
1668 })
1669 .sum::<f64>()
1670 / hv_length as f64;
1671 hv[i] = 100.0 * var.sqrt() * scale;
1672 }
1673 }
1674
1675 for i in (hv_length + rank_length - 1)..len {
1676 let start = i + 1 - rank_length;
1677 let window = &hv[start..=i];
1678 if window.iter().all(|v| v.is_finite()) {
1679 let min_v = window.iter().fold(f64::INFINITY, |a, &b| a.min(b));
1680 let max_v = window.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1681 let range = max_v - min_v;
1682 hvr[i] = if range <= 0.0 {
1683 0.0
1684 } else {
1685 100.0 * (hv[i] - min_v) / range
1686 };
1687 }
1688 }
1689
1690 (hvr, hv)
1691 }
1692
1693 fn assert_series_close(left: &[f64], right: &[f64], tol: f64) {
1694 assert_eq!(left.len(), right.len());
1695 for (a, b) in left.iter().zip(right.iter()) {
1696 if a.is_nan() || b.is_nan() {
1697 assert!(a.is_nan() && b.is_nan());
1698 } else {
1699 assert!((a - b).abs() <= tol, "left={a} right={b}");
1700 }
1701 }
1702 }
1703
1704 #[test]
1705 fn historical_volatility_rank_matches_naive() -> Result<(), Box<dyn Error>> {
1706 let close = sample_close(256);
1707 let input = HistoricalVolatilityRankInput::from_slice(
1708 &close,
1709 HistoricalVolatilityRankParams {
1710 hv_length: Some(10),
1711 rank_length: Some(20),
1712 annualization_days: Some(365.0),
1713 bar_days: Some(1.0),
1714 },
1715 );
1716 let out = historical_volatility_rank_with_kernel(&input, Kernel::Scalar)?;
1717 let (expected_hvr, expected_hv) = naive_hvr(&close, 10, 20, 365.0, 1.0);
1718
1719 assert_series_close(&out.hvr, &expected_hvr, 1e-8);
1720 assert_series_close(&out.hv, &expected_hv, 1e-10);
1721 Ok(())
1722 }
1723
1724 #[test]
1725 fn historical_volatility_rank_into_matches_api() -> Result<(), Box<dyn Error>> {
1726 let close = sample_close(192);
1727 let input = HistoricalVolatilityRankInput::from_slice(
1728 &close,
1729 HistoricalVolatilityRankParams {
1730 hv_length: Some(12),
1731 rank_length: Some(30),
1732 annualization_days: Some(252.0),
1733 bar_days: Some(1.0),
1734 },
1735 );
1736 let baseline = historical_volatility_rank_with_kernel(&input, Kernel::Auto)?;
1737 let mut hvr = vec![0.0; close.len()];
1738 let mut hv = vec![0.0; close.len()];
1739 historical_volatility_rank_into_slice(&mut hvr, &mut hv, &input, Kernel::Auto)?;
1740 assert_series_close(&baseline.hvr, &hvr, 1e-10);
1741 assert_series_close(&baseline.hv, &hv, 1e-10);
1742 Ok(())
1743 }
1744
1745 #[test]
1746 fn historical_volatility_rank_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1747 let close = sample_close(300);
1748 let params = HistoricalVolatilityRankParams {
1749 hv_length: Some(10),
1750 rank_length: Some(28),
1751 annualization_days: Some(365.0),
1752 bar_days: Some(1.0),
1753 };
1754 let batch = historical_volatility_rank(&HistoricalVolatilityRankInput::from_slice(
1755 &close,
1756 params.clone(),
1757 ))?;
1758
1759 let mut stream = HistoricalVolatilityRankStream::try_new(params)?;
1760 let mut hvr = Vec::with_capacity(close.len());
1761 let mut hv = Vec::with_capacity(close.len());
1762 for &value in &close {
1763 if let Some((hvr_value, hv_value)) = stream.update(value) {
1764 hvr.push(hvr_value);
1765 hv.push(hv_value);
1766 } else {
1767 hvr.push(f64::NAN);
1768 hv.push(f64::NAN);
1769 }
1770 }
1771
1772 assert_series_close(&batch.hvr, &hvr, 1e-8);
1773 assert_series_close(&batch.hv, &hv, 1e-8);
1774 Ok(())
1775 }
1776
1777 #[test]
1778 fn historical_volatility_rank_batch_single_matches_single() -> Result<(), Box<dyn Error>> {
1779 let close = sample_close(220);
1780 let batch = historical_volatility_rank_batch_with_kernel(
1781 &close,
1782 &HistoricalVolatilityRankBatchRange {
1783 hv_length: (10, 10, 0),
1784 rank_length: (20, 20, 0),
1785 annualization_days: (365.0, 365.0, 0.0),
1786 bar_days: (1.0, 1.0, 0.0),
1787 },
1788 Kernel::ScalarBatch,
1789 )?;
1790 let single = historical_volatility_rank(&HistoricalVolatilityRankInput::from_slice(
1791 &close,
1792 HistoricalVolatilityRankParams {
1793 hv_length: Some(10),
1794 rank_length: Some(20),
1795 annualization_days: Some(365.0),
1796 bar_days: Some(1.0),
1797 },
1798 ))?;
1799
1800 assert_eq!(batch.rows, 1);
1801 assert_eq!(batch.cols, close.len());
1802 assert_series_close(&batch.hvr, &single.hvr, 1e-8);
1803 assert_series_close(&batch.hv, &single.hv, 1e-8);
1804 Ok(())
1805 }
1806
1807 #[test]
1808 fn historical_volatility_rank_rejects_invalid_params() {
1809 let close = sample_close(32);
1810 let bad_hv = HistoricalVolatilityRankInput::from_slice(
1811 &close,
1812 HistoricalVolatilityRankParams {
1813 hv_length: Some(0),
1814 ..HistoricalVolatilityRankParams::default()
1815 },
1816 );
1817 assert!(matches!(
1818 historical_volatility_rank(&bad_hv),
1819 Err(HistoricalVolatilityRankError::InvalidHvLength { .. })
1820 ));
1821
1822 let bad_rank = HistoricalVolatilityRankInput::from_slice(
1823 &close,
1824 HistoricalVolatilityRankParams {
1825 hv_length: Some(10),
1826 rank_length: Some(0),
1827 annualization_days: Some(365.0),
1828 bar_days: Some(1.0),
1829 },
1830 );
1831 assert!(matches!(
1832 historical_volatility_rank(&bad_rank),
1833 Err(HistoricalVolatilityRankError::InvalidRankLength { .. })
1834 ));
1835 }
1836
1837 #[test]
1838 fn historical_volatility_rank_dispatch_compute_returns_hvr() -> Result<(), Box<dyn Error>> {
1839 let close = sample_close(180);
1840 let params = [
1841 ParamKV {
1842 key: "hv_length",
1843 value: ParamValue::Int(10),
1844 },
1845 ParamKV {
1846 key: "rank_length",
1847 value: ParamValue::Int(20),
1848 },
1849 ];
1850 let out = compute_cpu(IndicatorComputeRequest {
1851 indicator_id: "historical_volatility_rank",
1852 output_id: Some("hvr"),
1853 data: IndicatorDataRef::Slice { values: &close },
1854 params: ¶ms,
1855 kernel: Kernel::Auto,
1856 })?;
1857 assert_eq!(out.output_id, "hvr");
1858 Ok(())
1859 }
1860}