vectorbt-rust 1.1.0__tar.gz → 1.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -301,7 +301,7 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
301
301
 
302
302
  [[package]]
303
303
  name = "vectorbt-rust"
304
- version = "1.1.0"
304
+ version = "1.1.2"
305
305
  dependencies = [
306
306
  "ndarray",
307
307
  "numpy",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vectorbt-rust"
3
- version = "1.1.0"
3
+ version = "1.1.2"
4
4
  edition = "2021"
5
5
  description = "Rust-accelerated kernels for vectorbt"
6
6
  license = "Apache-2.0 WITH Commons-Clause-1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectorbt-rust
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Financial and Insurance Industry
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "vectorbt-rust"
7
- version = "1.1.0"
7
+ version = "1.1.2"
8
8
  description = "Rust-accelerated kernels for vectorbt"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0 WITH Commons-Clause-1.0"}
@@ -18,6 +18,7 @@ pub(crate) const RANGE_OPEN: i64 = 0;
18
18
  pub(crate) const RANGE_CLOSED: i64 = 1;
19
19
  const DRAWDOWN_ACTIVE: i64 = 0;
20
20
  const DRAWDOWN_RECOVERED: i64 = 1;
21
+ const INV_COND_TOL: f64 = f64::EPSILON * 1e3;
21
22
 
22
23
  #[pyfunction]
23
24
  #[pyo3(signature = (a, seed=None))]
@@ -703,37 +704,97 @@ pub(crate) fn rolling_mean_2d_c(a: ArrayView2<'_, f64>, window: usize, minp: usi
703
704
  out
704
705
  }
705
706
 
707
+ #[derive(Clone, Copy, Default)]
708
+ struct RollingVarState {
709
+ count: usize,
710
+ mean: f64,
711
+ m2: f64,
712
+ compensation_add: f64,
713
+ compensation_remove: f64,
714
+ numerically_unstable: bool,
715
+ }
716
+
717
+ impl RollingVarState {
718
+ fn add(&mut self, val: f64) {
719
+ if val.is_nan() {
720
+ return;
721
+ }
722
+ let prev_m2 = self.m2;
723
+ self.count += 1;
724
+ let prev_mean = self.mean - self.compensation_add;
725
+ let y = val - self.compensation_add;
726
+ let delta = y - self.mean;
727
+ self.compensation_add = delta + self.mean - y;
728
+ self.mean += delta / self.count as f64;
729
+ self.m2 += (val - prev_mean) * (val - self.mean);
730
+ if prev_m2 * INV_COND_TOL > self.m2 {
731
+ self.numerically_unstable = true;
732
+ }
733
+ }
734
+
735
+ fn remove(&mut self, val: f64) {
736
+ if val.is_nan() {
737
+ return;
738
+ }
739
+ let prev_m2 = self.m2;
740
+ self.count -= 1;
741
+ if self.count > 0 {
742
+ let prev_mean = self.mean - self.compensation_remove;
743
+ let y = val - self.compensation_remove;
744
+ let delta = y - self.mean;
745
+ self.compensation_remove = delta + self.mean - y;
746
+ self.mean -= delta / self.count as f64;
747
+ self.m2 -= (val - prev_mean) * (val - self.mean);
748
+ if prev_m2 * INV_COND_TOL > self.m2 {
749
+ self.numerically_unstable = true;
750
+ }
751
+ } else {
752
+ self.mean = 0.0;
753
+ self.m2 = 0.0;
754
+ self.numerically_unstable = false;
755
+ }
756
+ }
757
+
758
+ fn reset(&mut self) {
759
+ *self = Self::default();
760
+ }
761
+
762
+ fn std(&self, minp: usize, ddof: usize) -> f64 {
763
+ if self.count >= minp && self.count > ddof {
764
+ (self.m2 / (self.count - ddof) as f64).sqrt()
765
+ } else {
766
+ f64::NAN
767
+ }
768
+ }
769
+ }
770
+
706
771
  pub(crate) fn rolling_std_2d_c(a: ArrayView2<'_, f64>, window: usize, minp: usize, ddof: usize) -> Array2<f64> {
707
772
  let (nrows, ncols) = a.dim();
708
773
  let src = a.as_slice().expect("standard-layout array must be sliceable");
709
774
  let mut out = Array2::<f64>::from_elem((nrows, ncols), f64::NAN);
710
775
  let dst = out.as_slice_mut().expect("owned array must be sliceable");
711
- let mut sums = vec![0.0f64; ncols];
712
- let mut sums_sq = vec![0.0f64; ncols];
713
- let mut counts = vec![0usize; ncols];
776
+ let minp = minp.max(1);
777
+ let mut states = vec![RollingVarState::default(); ncols];
714
778
  for row in 0..nrows {
715
779
  let row_start = row * ncols;
716
780
  for col in 0..ncols {
717
- let cur = src[row_start + col];
718
- if !cur.is_nan() {
719
- sums[col] += cur;
720
- sums_sq[col] += cur * cur;
721
- counts[col] += 1;
781
+ let state = &mut states[col];
782
+ let requires_recompute = row == 0 || window == 0;
783
+ if !requires_recompute && row >= window {
784
+ state.remove(src[(row - window) * ncols + col]);
722
785
  }
723
- if row >= window {
724
- let old = src[(row - window) * ncols + col];
725
- if !old.is_nan() {
726
- sums[col] -= old;
727
- sums_sq[col] -= old * old;
728
- counts[col] -= 1;
729
- }
786
+ if !requires_recompute {
787
+ state.add(src[row_start + col]);
730
788
  }
731
- let cnt = counts[col];
732
- if cnt >= minp && cnt > ddof {
733
- let mean = sums[col] / cnt as f64;
734
- let variance = (sums_sq[col] - 2.0 * sums[col] * mean + cnt as f64 * mean * mean) / (cnt - ddof) as f64;
735
- dst[row_start + col] = variance.abs().sqrt();
789
+ if requires_recompute || state.numerically_unstable {
790
+ state.reset();
791
+ let start = (row + 1).saturating_sub(window);
792
+ for j in start..=row {
793
+ state.add(src[j * ncols + col]);
794
+ }
795
+ state.numerically_unstable = false;
736
796
  }
797
+ dst[row_start + col] = state.std(minp, ddof);
737
798
  }
738
799
  }
739
800
  out
@@ -1129,29 +1190,25 @@ pub(crate) fn rolling_mean_1d(a: &[f64], window: usize, minp: usize) -> Vec<f64>
1129
1190
  pub(crate) fn rolling_std_1d(a: &[f64], window: usize, minp: usize, ddof: usize) -> Vec<f64> {
1130
1191
  let n = a.len();
1131
1192
  let mut out = vec![f64::NAN; n];
1132
- let mut sum = 0.0f64;
1133
- let mut sum_sq = 0.0f64;
1134
- let mut cnt = 0usize;
1193
+ let minp = minp.max(1);
1194
+ let mut state = RollingVarState::default();
1135
1195
  for i in 0..n {
1136
- let cur = a[i];
1137
- if !cur.is_nan() {
1138
- sum += cur;
1139
- sum_sq += cur * cur;
1140
- cnt += 1;
1141
- }
1142
- if i >= window {
1143
- let old = a[i - window];
1144
- if !old.is_nan() {
1145
- sum -= old;
1146
- sum_sq -= old * old;
1147
- cnt -= 1;
1196
+ let requires_recompute = i == 0 || window == 0;
1197
+ if !requires_recompute && i >= window {
1198
+ state.remove(a[i - window]);
1199
+ }
1200
+ if !requires_recompute {
1201
+ state.add(a[i]);
1202
+ }
1203
+ if requires_recompute || state.numerically_unstable {
1204
+ state.reset();
1205
+ let start = (i + 1).saturating_sub(window);
1206
+ for j in start..=i {
1207
+ state.add(a[j]);
1148
1208
  }
1209
+ state.numerically_unstable = false;
1149
1210
  }
1150
- if cnt >= minp && cnt > ddof {
1151
- let mean = sum / cnt as f64;
1152
- let variance = (sum_sq - 2.0 * sum * mean + cnt as f64 * mean * mean) / (cnt - ddof) as f64;
1153
- out[i] = variance.abs().sqrt();
1154
- }
1211
+ out[i] = state.std(minp, ddof);
1155
1212
  }
1156
1213
  out
1157
1214
  }
@@ -6,7 +6,6 @@ use ndarray::{Array2, ArrayView2};
6
6
  use numpy::{PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2};
7
7
  use pyo3::exceptions::PyValueError;
8
8
  use pyo3::prelude::*;
9
- use std::cmp::Ordering;
10
9
 
11
10
  fn validate_same_shape_2d(a: ArrayView2<'_, f64>, b: ArrayView2<'_, f64>, name: &str) -> PyResult<()> {
12
11
  if a.dim() != b.dim() {
@@ -762,17 +761,12 @@ pub(crate) fn value_at_risk_1d(returns: &[f64], cutoff: f64) -> f64 {
762
761
  }
763
762
 
764
763
  pub(crate) fn cond_value_at_risk_1d(returns: &[f64], cutoff: f64) -> f64 {
765
- if returns.is_empty() {
764
+ let mut vals: Vec<f64> = returns.iter().copied().filter(|value| !value.is_nan()).collect();
765
+ if vals.is_empty() {
766
766
  return f64::NAN;
767
767
  }
768
- let mut vals = returns.to_vec();
769
768
  let cutoff_index = ((vals.len() - 1) as f64 * cutoff) as usize;
770
- vals.select_nth_unstable_by(cutoff_index, |left, right| match (left.is_nan(), right.is_nan()) {
771
- (true, true) => Ordering::Equal,
772
- (true, false) => Ordering::Greater,
773
- (false, true) => Ordering::Less,
774
- (false, false) => left.partial_cmp(right).unwrap(),
775
- });
769
+ vals.select_nth_unstable_by(cutoff_index, |left, right| left.partial_cmp(right).unwrap());
776
770
  mean_strict(&vals[..cutoff_index + 1])
777
771
  }
778
772
 
File without changes
File without changes