Skip to main content

vector_ta/indicators/
historical_volatility_rank.rs

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: &params,
1855            kernel: Kernel::Auto,
1856        })?;
1857        assert_eq!(out.output_id, "hvr");
1858        Ok(())
1859    }
1860}