@ebowwa/quant-rust 0.1.0
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.
- package/README.md +161 -0
- package/bun-ffi.d.ts +54 -0
- package/dist/index.js +576 -0
- package/dist/src/index.d.ts +324 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.d.ts.map +1 -0
- package/native/README.md +62 -0
- package/native/darwin-arm64/libquant_rust.dylib +0 -0
- package/package.json +70 -0
- package/scripts/postinstall.cjs +85 -0
- package/src/ffi.rs +496 -0
- package/src/index.ts +1073 -0
- package/src/indicators/ma.rs +222 -0
- package/src/indicators/mod.rs +18 -0
- package/src/indicators/momentum.rs +353 -0
- package/src/indicators/sr.rs +195 -0
- package/src/indicators/trend.rs +351 -0
- package/src/indicators/volatility.rs +270 -0
- package/src/indicators/volume.rs +213 -0
- package/src/lib.rs +130 -0
- package/src/patterns/breakout.rs +431 -0
- package/src/patterns/chart.rs +772 -0
- package/src/patterns/mod.rs +394 -0
- package/src/patterns/sr.rs +423 -0
- package/src/prediction/amm.rs +338 -0
- package/src/prediction/arbitrage.rs +230 -0
- package/src/prediction/calibration.rs +317 -0
- package/src/prediction/kelly.rs +232 -0
- package/src/prediction/lmsr.rs +194 -0
- package/src/prediction/mod.rs +59 -0
- package/src/prediction/odds.rs +229 -0
- package/src/prediction/pnl.rs +254 -0
- package/src/prediction/risk.rs +228 -0
- package/src/risk/beta.rs +257 -0
- package/src/risk/drawdown.rs +256 -0
- package/src/risk/leverage.rs +201 -0
- package/src/risk/mod.rs +388 -0
- package/src/risk/portfolio.rs +287 -0
- package/src/risk/ratios.rs +290 -0
- package/src/risk/sizing.rs +194 -0
- package/src/risk/var.rs +222 -0
- package/src/stats/cdf.rs +257 -0
- package/src/stats/correlation.rs +225 -0
- package/src/stats/distribution.rs +194 -0
- package/src/stats/hypothesis.rs +177 -0
- package/src/stats/matrix.rs +346 -0
- package/src/stats/mod.rs +257 -0
- package/src/stats/regression.rs +239 -0
- package/src/stats/rolling.rs +193 -0
- package/src/stats/timeseries.rs +263 -0
- package/src/types.rs +224 -0
- package/src/utils/mod.rs +215 -0
- package/src/utils/normalize.rs +192 -0
- package/src/utils/price.rs +167 -0
- package/src/utils/quantiles.rs +177 -0
- package/src/utils/returns.rs +158 -0
- package/src/utils/rolling.rs +97 -0
- package/src/utils/stats.rs +154 -0
- package/types/index.ts +513 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//! Normalization Module
|
|
2
|
+
//!
|
|
3
|
+
//! Functions for normalizing and scaling data
|
|
4
|
+
|
|
5
|
+
use super::stats::{max, mean, min, std_dev};
|
|
6
|
+
|
|
7
|
+
/// Min-max normalization (0-1 scale)
|
|
8
|
+
///
|
|
9
|
+
/// Returns 0.5 for all values if the range is zero.
|
|
10
|
+
pub fn normalize_min_max(arr: &[f64]) -> Vec<f64> {
|
|
11
|
+
if arr.is_empty() {
|
|
12
|
+
return Vec::new();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let min_val = min(arr);
|
|
16
|
+
let max_val = max(arr);
|
|
17
|
+
let range_val = max_val - min_val;
|
|
18
|
+
|
|
19
|
+
if range_val == 0.0 {
|
|
20
|
+
return vec![0.5; arr.len()];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
arr.iter().map(|&v| (v - min_val) / range_val).collect()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Z-score normalization (standardization)
|
|
27
|
+
///
|
|
28
|
+
/// Returns 0 for all values if the standard deviation is zero.
|
|
29
|
+
pub fn normalize_z_score(arr: &[f64]) -> Vec<f64> {
|
|
30
|
+
if arr.is_empty() {
|
|
31
|
+
return Vec::new();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let avg = mean(arr);
|
|
35
|
+
let sd = std_dev(arr, true);
|
|
36
|
+
|
|
37
|
+
if sd == 0.0 {
|
|
38
|
+
return vec![0.0; arr.len()];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
arr.iter().map(|&v| (v - avg) / sd).collect()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Percentage change from first element
|
|
45
|
+
///
|
|
46
|
+
/// Returns an empty vector if the array is empty or the first element is zero.
|
|
47
|
+
pub fn percent_change(arr: &[f64]) -> Vec<f64> {
|
|
48
|
+
if arr.is_empty() || arr[0] == 0.0 {
|
|
49
|
+
return Vec::new();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let base = arr[0];
|
|
53
|
+
arr.iter().map(|&v| ((v - base) / base) * 100.0).collect()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Normalize to a specific range [new_min, new_max]
|
|
57
|
+
pub fn normalize_to_range(arr: &[f64], new_min: f64, new_max: f64) -> Vec<f64> {
|
|
58
|
+
if arr.is_empty() {
|
|
59
|
+
return Vec::new();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let min_val = min(arr);
|
|
63
|
+
let max_val = max(arr);
|
|
64
|
+
let range_val = max_val - min_val;
|
|
65
|
+
|
|
66
|
+
if range_val == 0.0 {
|
|
67
|
+
return vec![(new_min + new_max) / 2.0; arr.len()];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let new_range = new_max - new_min;
|
|
71
|
+
arr.iter()
|
|
72
|
+
.map(|&v| ((v - min_val) / range_val) * new_range + new_min)
|
|
73
|
+
.collect()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Decimal scaling normalization
|
|
77
|
+
///
|
|
78
|
+
/// Divides by 10^j where j is the number of digits in the max absolute value.
|
|
79
|
+
pub fn decimal_scale(arr: &[f64]) -> Vec<f64> {
|
|
80
|
+
if arr.is_empty() {
|
|
81
|
+
return Vec::new();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let max_abs = arr
|
|
85
|
+
.iter()
|
|
86
|
+
.map(|&v| v.abs())
|
|
87
|
+
.fold(0.0_f64, f64::max);
|
|
88
|
+
|
|
89
|
+
if max_abs == 0.0 {
|
|
90
|
+
return arr.to_vec();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let j = max_abs.log10().ceil() as i32;
|
|
94
|
+
let scale = 10_f64.powi(j);
|
|
95
|
+
|
|
96
|
+
arr.iter().map(|&v| v / scale).collect()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Robust scaling using median and IQR
|
|
100
|
+
///
|
|
101
|
+
/// Centers by median and scales by IQR. Good for data with outliers.
|
|
102
|
+
pub fn robust_scale(arr: &[f64]) -> Vec<f64> {
|
|
103
|
+
if arr.is_empty() {
|
|
104
|
+
return Vec::new();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
use super::quantiles::iqr;
|
|
108
|
+
use super::stats::median;
|
|
109
|
+
|
|
110
|
+
let med = median(arr);
|
|
111
|
+
let iqr_val = iqr(arr);
|
|
112
|
+
|
|
113
|
+
if iqr_val == 0.0 {
|
|
114
|
+
return vec![0.0; arr.len()];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
arr.iter().map(|&v| (v - med) / iqr_val).collect()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Softmax normalization
|
|
121
|
+
///
|
|
122
|
+
/// Converts values to probabilities that sum to 1.
|
|
123
|
+
pub fn softmax(arr: &[f64]) -> Vec<f64> {
|
|
124
|
+
if arr.is_empty() {
|
|
125
|
+
return Vec::new();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Subtract max for numerical stability
|
|
129
|
+
let max_val = max(arr);
|
|
130
|
+
|
|
131
|
+
let exp_sum: f64 = arr.iter().map(|&v| ((v - max_val).exp())).sum();
|
|
132
|
+
|
|
133
|
+
if exp_sum == 0.0 {
|
|
134
|
+
return vec![1.0 / arr.len() as f64; arr.len()];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
arr.iter()
|
|
138
|
+
.map(|&v| ((v - max_val).exp()) / exp_sum)
|
|
139
|
+
.collect()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[cfg(test)]
|
|
143
|
+
mod tests {
|
|
144
|
+
use super::*;
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn test_normalize_min_max() {
|
|
148
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
|
149
|
+
let result = normalize_min_max(&data);
|
|
150
|
+
assert_eq!(result, vec![0.0, 0.25, 0.5, 0.75, 1.0]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[test]
|
|
154
|
+
fn test_normalize_min_max_constant() {
|
|
155
|
+
let data = [5.0, 5.0, 5.0];
|
|
156
|
+
let result = normalize_min_max(&data);
|
|
157
|
+
assert_eq!(result, vec![0.5, 0.5, 0.5]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn test_normalize_z_score() {
|
|
162
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
|
163
|
+
let result = normalize_z_score(&data);
|
|
164
|
+
// Mean = 3, std_dev = sqrt(2) for population
|
|
165
|
+
assert!((result[2]).abs() < 1e-10); // middle element should be 0
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn test_percent_change() {
|
|
170
|
+
let data = [100.0, 110.0, 120.0];
|
|
171
|
+
let result = percent_change(&data);
|
|
172
|
+
assert_eq!(result, vec![0.0, 10.0, 20.0]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn test_normalize_to_range() {
|
|
177
|
+
let data = [0.0, 50.0, 100.0];
|
|
178
|
+
let result = normalize_to_range(&data, -1.0, 1.0);
|
|
179
|
+
assert_eq!(result, vec![-1.0, 0.0, 1.0]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn test_softmax() {
|
|
184
|
+
let data = [1.0, 2.0, 3.0];
|
|
185
|
+
let result = softmax(&data);
|
|
186
|
+
let sum: f64 = result.iter().sum();
|
|
187
|
+
assert!((sum - 1.0).abs() < 1e-10);
|
|
188
|
+
// Largest value should have highest probability
|
|
189
|
+
assert!(result[2] > result[1]);
|
|
190
|
+
assert!(result[1] > result[0]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//! Price Extraction Module
|
|
2
|
+
//!
|
|
3
|
+
//! Extract various price types from OHLCV data
|
|
4
|
+
|
|
5
|
+
use crate::types::OHLCV;
|
|
6
|
+
|
|
7
|
+
/// Extract close prices from OHLCV data
|
|
8
|
+
pub fn extract_close(data: &[OHLCV]) -> Vec<f64> {
|
|
9
|
+
let mut result = Vec::with_capacity(data.len());
|
|
10
|
+
for d in data {
|
|
11
|
+
result.push(d.close);
|
|
12
|
+
}
|
|
13
|
+
result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Extract open prices from OHLCV data
|
|
17
|
+
pub fn extract_open(data: &[OHLCV]) -> Vec<f64> {
|
|
18
|
+
let mut result = Vec::with_capacity(data.len());
|
|
19
|
+
for d in data {
|
|
20
|
+
result.push(d.open);
|
|
21
|
+
}
|
|
22
|
+
result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Extract high prices from OHLCV data
|
|
26
|
+
pub fn extract_high(data: &[OHLCV]) -> Vec<f64> {
|
|
27
|
+
let mut result = Vec::with_capacity(data.len());
|
|
28
|
+
for d in data {
|
|
29
|
+
result.push(d.high);
|
|
30
|
+
}
|
|
31
|
+
result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Extract low prices from OHLCV data
|
|
35
|
+
pub fn extract_low(data: &[OHLCV]) -> Vec<f64> {
|
|
36
|
+
let mut result = Vec::with_capacity(data.len());
|
|
37
|
+
for d in data {
|
|
38
|
+
result.push(d.low);
|
|
39
|
+
}
|
|
40
|
+
result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Extract volume from OHLCV data
|
|
44
|
+
pub fn extract_volume(data: &[OHLCV]) -> Vec<f64> {
|
|
45
|
+
let mut result = Vec::with_capacity(data.len());
|
|
46
|
+
for d in data {
|
|
47
|
+
result.push(d.volume);
|
|
48
|
+
}
|
|
49
|
+
result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Extract typical price (HLC/3)
|
|
53
|
+
pub fn extract_typical_price(data: &[OHLCV]) -> Vec<f64> {
|
|
54
|
+
let mut result = Vec::with_capacity(data.len());
|
|
55
|
+
for d in data {
|
|
56
|
+
result.push((d.high + d.low + d.close) / 3.0);
|
|
57
|
+
}
|
|
58
|
+
result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Extract weighted close price ((H + L + C*2)/4)
|
|
62
|
+
pub fn extract_weighted_close(data: &[OHLCV]) -> Vec<f64> {
|
|
63
|
+
let mut result = Vec::with_capacity(data.len());
|
|
64
|
+
for d in data {
|
|
65
|
+
result.push((d.high + d.low + d.close * 2.0) / 4.0);
|
|
66
|
+
}
|
|
67
|
+
result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Extract timestamps from OHLCV data
|
|
71
|
+
pub fn extract_timestamps(data: &[OHLCV]) -> Vec<i64> {
|
|
72
|
+
let mut result = Vec::with_capacity(data.len());
|
|
73
|
+
for d in data {
|
|
74
|
+
result.push(d.timestamp);
|
|
75
|
+
}
|
|
76
|
+
result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Extract all price components as separate arrays
|
|
80
|
+
pub fn extract_ohlcv(data: &[OHLCV]) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
|
|
81
|
+
let len = data.len();
|
|
82
|
+
let mut opens = Vec::with_capacity(len);
|
|
83
|
+
let mut highs = Vec::with_capacity(len);
|
|
84
|
+
let mut lows = Vec::with_capacity(len);
|
|
85
|
+
let mut closes = Vec::with_capacity(len);
|
|
86
|
+
let mut volumes = Vec::with_capacity(len);
|
|
87
|
+
|
|
88
|
+
for d in data {
|
|
89
|
+
opens.push(d.open);
|
|
90
|
+
highs.push(d.high);
|
|
91
|
+
lows.push(d.low);
|
|
92
|
+
closes.push(d.close);
|
|
93
|
+
volumes.push(d.volume);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
(opens, highs, lows, closes, volumes)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[cfg(test)]
|
|
100
|
+
mod tests {
|
|
101
|
+
use super::*;
|
|
102
|
+
|
|
103
|
+
fn create_test_data() -> Vec<OHLCV> {
|
|
104
|
+
vec![
|
|
105
|
+
OHLCV { timestamp: 1000, open: 10.0, high: 12.0, low: 9.0, close: 11.0, volume: 1000.0 },
|
|
106
|
+
OHLCV { timestamp: 2000, open: 11.0, high: 13.0, low: 10.0, close: 12.0, volume: 1500.0 },
|
|
107
|
+
OHLCV { timestamp: 3000, open: 12.0, high: 14.0, low: 11.0, close: 13.0, volume: 2000.0 },
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn test_extract_close() {
|
|
113
|
+
let data = create_test_data();
|
|
114
|
+
assert_eq!(extract_close(&data), vec![11.0, 12.0, 13.0]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn test_extract_open() {
|
|
119
|
+
let data = create_test_data();
|
|
120
|
+
assert_eq!(extract_open(&data), vec![10.0, 11.0, 12.0]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn test_extract_high() {
|
|
125
|
+
let data = create_test_data();
|
|
126
|
+
assert_eq!(extract_high(&data), vec![12.0, 13.0, 14.0]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn test_extract_low() {
|
|
131
|
+
let data = create_test_data();
|
|
132
|
+
assert_eq!(extract_low(&data), vec![9.0, 10.0, 11.0]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn test_extract_volume() {
|
|
137
|
+
let data = create_test_data();
|
|
138
|
+
assert_eq!(extract_volume(&data), vec![1000.0, 1500.0, 2000.0]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn test_extract_typical_price() {
|
|
143
|
+
let data = create_test_data();
|
|
144
|
+
let result = extract_typical_price(&data);
|
|
145
|
+
// (12 + 9 + 11) / 3 = 10.666...
|
|
146
|
+
assert!((result[0] - 10.666666666666666).abs() < 1e-10);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn test_extract_weighted_close() {
|
|
151
|
+
let data = create_test_data();
|
|
152
|
+
let result = extract_weighted_close(&data);
|
|
153
|
+
// (12 + 9 + 11*2) / 4 = 10.75
|
|
154
|
+
assert!((result[0] - 10.75).abs() < 1e-10);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn test_extract_ohlcv() {
|
|
159
|
+
let data = create_test_data();
|
|
160
|
+
let (o, h, l, c, v) = extract_ohlcv(&data);
|
|
161
|
+
assert_eq!(o, vec![10.0, 11.0, 12.0]);
|
|
162
|
+
assert_eq!(h, vec![12.0, 13.0, 14.0]);
|
|
163
|
+
assert_eq!(l, vec![9.0, 10.0, 11.0]);
|
|
164
|
+
assert_eq!(c, vec![11.0, 12.0, 13.0]);
|
|
165
|
+
assert_eq!(v, vec![1000.0, 1500.0, 2000.0]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//! Quantile Calculations Module
|
|
2
|
+
//!
|
|
3
|
+
//! Functions for calculating quantiles, quartiles, and IQR
|
|
4
|
+
|
|
5
|
+
/// Calculate a specific quantile (0.0 to 1.0)
|
|
6
|
+
///
|
|
7
|
+
/// Uses linear interpolation between values.
|
|
8
|
+
pub fn quantile(arr: &[f64], q: f64) -> f64 {
|
|
9
|
+
if arr.is_empty() || q < 0.0 || q > 1.0 {
|
|
10
|
+
return 0.0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let mut sorted: Vec<f64> = arr.to_vec();
|
|
14
|
+
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
|
15
|
+
|
|
16
|
+
let pos = (sorted.len() - 1) as f64 * q;
|
|
17
|
+
let base = pos.floor() as usize;
|
|
18
|
+
let rest = pos - base as f64;
|
|
19
|
+
|
|
20
|
+
if base + 1 < sorted.len() {
|
|
21
|
+
sorted[base] + rest * (sorted[base + 1] - sorted[base])
|
|
22
|
+
} else {
|
|
23
|
+
sorted[base]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Calculate quartiles (Q1, Q2/Median, Q3)
|
|
28
|
+
///
|
|
29
|
+
/// Returns a tuple of (q1, q2, q3).
|
|
30
|
+
pub fn quartiles(arr: &[f64]) -> (f64, f64, f64) {
|
|
31
|
+
let q1 = quantile(arr, 0.25);
|
|
32
|
+
let q2 = quantile(arr, 0.50);
|
|
33
|
+
let q3 = quantile(arr, 0.75);
|
|
34
|
+
(q1, q2, q3)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Calculate interquartile range (Q3 - Q1)
|
|
38
|
+
pub fn iqr(arr: &[f64]) -> f64 {
|
|
39
|
+
let (q1, _, q3) = quartiles(arr);
|
|
40
|
+
q3 - q1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Calculate percentiles (0-100)
|
|
44
|
+
pub fn percentile(arr: &[f64], p: f64) -> f64 {
|
|
45
|
+
quantile(arr, p / 100.0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Calculate all five-number summary: (min, q1, median, q3, max)
|
|
49
|
+
pub fn five_number_summary(arr: &[f64]) -> (f64, f64, f64, f64, f64) {
|
|
50
|
+
use super::stats::{max, min};
|
|
51
|
+
|
|
52
|
+
let minimum = min(arr);
|
|
53
|
+
let (q1, median, q3) = quartiles(arr);
|
|
54
|
+
let maximum = max(arr);
|
|
55
|
+
|
|
56
|
+
(minimum, q1, median, q3, maximum)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Calculate deciles (10th, 20th, ..., 90th percentiles)
|
|
60
|
+
///
|
|
61
|
+
/// Returns a vector of 9 values.
|
|
62
|
+
pub fn deciles(arr: &[f64]) -> Vec<f64> {
|
|
63
|
+
(1..=9).map(|i| quantile(arr, i as f64 / 10.0)).collect()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Detect outliers using the IQR method
|
|
67
|
+
///
|
|
68
|
+
/// Values below Q1 - 1.5*IQR or above Q3 + 1.5*IQR are considered outliers.
|
|
69
|
+
/// Returns a vector of booleans where true indicates an outlier.
|
|
70
|
+
pub fn detect_outliers_iqr(arr: &[f64]) -> Vec<bool> {
|
|
71
|
+
if arr.is_empty() {
|
|
72
|
+
return Vec::new();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let (q1, _, q3) = quartiles(arr);
|
|
76
|
+
let iqr_val = q3 - q1;
|
|
77
|
+
let lower_bound = q1 - 1.5 * iqr_val;
|
|
78
|
+
let upper_bound = q3 + 1.5 * iqr_val;
|
|
79
|
+
|
|
80
|
+
arr.iter().map(|&v| v < lower_bound || v > upper_bound).collect()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Remove outliers using the IQR method
|
|
84
|
+
///
|
|
85
|
+
/// Returns a new vector with outliers removed.
|
|
86
|
+
pub fn remove_outliers_iqr(arr: &[f64]) -> Vec<f64> {
|
|
87
|
+
let outliers = detect_outliers_iqr(arr);
|
|
88
|
+
arr.iter()
|
|
89
|
+
.zip(outliers.iter())
|
|
90
|
+
.filter_map(|(&v, &is_outlier)| {
|
|
91
|
+
if is_outlier {
|
|
92
|
+
None
|
|
93
|
+
} else {
|
|
94
|
+
Some(v)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.collect()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[cfg(test)]
|
|
101
|
+
mod tests {
|
|
102
|
+
use super::*;
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn test_quantile() {
|
|
106
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
|
107
|
+
|
|
108
|
+
// Q1 (25th percentile)
|
|
109
|
+
assert!((quantile(&data, 0.25) - 2.0).abs() < 1e-10);
|
|
110
|
+
// Median (50th percentile)
|
|
111
|
+
assert!((quantile(&data, 0.50) - 3.0).abs() < 1e-10);
|
|
112
|
+
// Q3 (75th percentile)
|
|
113
|
+
assert!((quantile(&data, 0.75) - 4.0).abs() < 1e-10);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn test_quartiles() {
|
|
118
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
|
119
|
+
let (q1, q2, q3) = quartiles(&data);
|
|
120
|
+
|
|
121
|
+
assert!((q1 - 2.75).abs() < 1e-10);
|
|
122
|
+
assert!((q2 - 4.5).abs() < 1e-10);
|
|
123
|
+
assert!((q3 - 6.25).abs() < 1e-10);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn test_iqr() {
|
|
128
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
|
129
|
+
let iqr_val = iqr(&data);
|
|
130
|
+
// IQR = Q3 - Q1 = 6.25 - 2.75 = 3.5
|
|
131
|
+
assert!((iqr_val - 3.5).abs() < 1e-10);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn test_percentile() {
|
|
136
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
|
137
|
+
assert!((percentile(&data, 50.0) - 3.0).abs() < 1e-10);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn test_five_number_summary() {
|
|
142
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
|
143
|
+
let (min, q1, med, q3, max) = five_number_summary(&data);
|
|
144
|
+
|
|
145
|
+
assert_eq!(min, 1.0);
|
|
146
|
+
assert_eq!(max, 5.0);
|
|
147
|
+
assert!((med - 3.0).abs() < 1e-10);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn test_deciles() {
|
|
152
|
+
let data: Vec<f64> = (1..=100).map(|i| i as f64).collect();
|
|
153
|
+
let decs = deciles(&data);
|
|
154
|
+
|
|
155
|
+
assert_eq!(decs.len(), 9);
|
|
156
|
+
// 10th percentile should be around 10.9
|
|
157
|
+
assert!((decs[0] - 10.9).abs() < 0.1);
|
|
158
|
+
// 90th percentile should be around 90.1
|
|
159
|
+
assert!((decs[8] - 90.1).abs() < 0.1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn test_detect_outliers_iqr() {
|
|
164
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0, 100.0]; // 100 is an outlier
|
|
165
|
+
let outliers = detect_outliers_iqr(&data);
|
|
166
|
+
|
|
167
|
+
assert!(!outliers[0]); // 1.0 is not an outlier
|
|
168
|
+
assert!(outliers[5]); // 100.0 is an outlier
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn test_empty_array() {
|
|
173
|
+
let data: [f64; 0] = [];
|
|
174
|
+
assert_eq!(quantile(&data, 0.5), 0.0);
|
|
175
|
+
assert_eq!(iqr(&data), 0.0);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//! Returns Calculations Module
|
|
2
|
+
//!
|
|
3
|
+
//! Functions for calculating and converting returns
|
|
4
|
+
|
|
5
|
+
/// Calculate simple returns (P1 - P0) / P0
|
|
6
|
+
///
|
|
7
|
+
/// Returns an empty vector if prices has fewer than 2 elements.
|
|
8
|
+
pub fn simple_returns(prices: &[f64]) -> Vec<f64> {
|
|
9
|
+
if prices.len() < 2 {
|
|
10
|
+
return Vec::new();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let mut result = Vec::with_capacity(prices.len() - 1);
|
|
14
|
+
for i in 1..prices.len() {
|
|
15
|
+
if prices[i - 1] != 0.0 {
|
|
16
|
+
result.push((prices[i] - prices[i - 1]) / prices[i - 1]);
|
|
17
|
+
} else {
|
|
18
|
+
result.push(f64::NAN);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
result
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Calculate log returns ln(P1 / P0)
|
|
25
|
+
///
|
|
26
|
+
/// Returns an empty vector if prices has fewer than 2 elements.
|
|
27
|
+
pub fn log_returns(prices: &[f64]) -> Vec<f64> {
|
|
28
|
+
if prices.len() < 2 {
|
|
29
|
+
return Vec::new();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let mut result = Vec::with_capacity(prices.len() - 1);
|
|
33
|
+
for i in 1..prices.len() {
|
|
34
|
+
if prices[i - 1] > 0.0 && prices[i] > 0.0 {
|
|
35
|
+
result.push((prices[i] / prices[i - 1]).ln());
|
|
36
|
+
} else {
|
|
37
|
+
result.push(f64::NAN);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Convert returns to prices from an initial price
|
|
44
|
+
pub fn returns_to_prices(initial: f64, returns: &[f64]) -> Vec<f64> {
|
|
45
|
+
let mut prices = Vec::with_capacity(returns.len() + 1);
|
|
46
|
+
prices.push(initial);
|
|
47
|
+
|
|
48
|
+
for &ret in returns {
|
|
49
|
+
let last = prices.last().unwrap_or(&initial);
|
|
50
|
+
prices.push(last * (1.0 + ret));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
prices
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Calculate cumulative returns
|
|
57
|
+
///
|
|
58
|
+
/// Returns a vector where the first element is 0 (no return yet),
|
|
59
|
+
/// and each subsequent element is the cumulative return up to that point.
|
|
60
|
+
pub fn cumulative_returns(returns: &[f64]) -> Vec<f64> {
|
|
61
|
+
let mut cum_ret = Vec::with_capacity(returns.len() + 1);
|
|
62
|
+
cum_ret.push(0.0);
|
|
63
|
+
|
|
64
|
+
let mut cumulative = 1.0;
|
|
65
|
+
for &ret in returns {
|
|
66
|
+
cumulative *= 1.0 + ret;
|
|
67
|
+
cum_ret.push(cumulative - 1.0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
cum_ret
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Calculate absolute returns (price differences)
|
|
74
|
+
pub fn absolute_returns(prices: &[f64]) -> Vec<f64> {
|
|
75
|
+
if prices.len() < 2 {
|
|
76
|
+
return Vec::new();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let mut result = Vec::with_capacity(prices.len() - 1);
|
|
80
|
+
for i in 1..prices.len() {
|
|
81
|
+
result.push(prices[i] - prices[i - 1]);
|
|
82
|
+
}
|
|
83
|
+
result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Annualize returns
|
|
87
|
+
///
|
|
88
|
+
/// Given a periodic return and the number of periods per year,
|
|
89
|
+
/// calculate the annualized return.
|
|
90
|
+
pub fn annualize_return(periodic_return: f64, periods_per_year: f64) -> f64 {
|
|
91
|
+
(1.0 + periodic_return).powf(periods_per_year) - 1.0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// De-annualize returns
|
|
95
|
+
///
|
|
96
|
+
/// Convert an annual return to a periodic return.
|
|
97
|
+
pub fn deannualize_return(annual_return: f64, periods_per_year: f64) -> f64 {
|
|
98
|
+
(1.0 + annual_return).powf(1.0 / periods_per_year) - 1.0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[cfg(test)]
|
|
102
|
+
mod tests {
|
|
103
|
+
use super::*;
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn test_simple_returns() {
|
|
107
|
+
let prices = [100.0, 110.0, 121.0, 133.1];
|
|
108
|
+
let returns = simple_returns(&prices);
|
|
109
|
+
assert_eq!(returns.len(), 3);
|
|
110
|
+
assert!((returns[0] - 0.10).abs() < 1e-10);
|
|
111
|
+
assert!((returns[1] - 0.10).abs() < 1e-10);
|
|
112
|
+
assert!((returns[2] - 0.10).abs() < 1e-10);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn test_log_returns() {
|
|
117
|
+
let prices = [100.0, 110.0];
|
|
118
|
+
let returns = log_returns(&prices);
|
|
119
|
+
assert_eq!(returns.len(), 1);
|
|
120
|
+
// ln(110/100) = ln(1.1) ~ 0.0953
|
|
121
|
+
assert!((returns[0] - 0.09531017980432493).abs() < 1e-10);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
fn test_returns_to_prices() {
|
|
126
|
+
let returns = [0.10, 0.10, 0.10];
|
|
127
|
+
let prices = returns_to_prices(100.0, &returns);
|
|
128
|
+
let expected = vec![100.0, 110.0, 121.0, 133.1];
|
|
129
|
+
assert_eq!(prices.len(), expected.len());
|
|
130
|
+
for (a, b) in prices.iter().zip(expected.iter()) {
|
|
131
|
+
assert!((a - b).abs() < 1e-9, "Expected {}, got {}", b, a);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn test_cumulative_returns() {
|
|
137
|
+
let returns = [0.10, 0.10, 0.10];
|
|
138
|
+
let cum = cumulative_returns(&returns);
|
|
139
|
+
assert_eq!(cum[0], 0.0);
|
|
140
|
+
// After 3x 10% gains: (1.1)^3 - 1 = 0.331
|
|
141
|
+
assert!((cum[3] - 0.331).abs() < 1e-10);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn test_empty_returns() {
|
|
146
|
+
assert!(simple_returns(&[100.0]).is_empty());
|
|
147
|
+
assert!(log_returns(&[100.0]).is_empty());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn test_annualize_return() {
|
|
152
|
+
// 1% monthly return annualized
|
|
153
|
+
let monthly = 0.01;
|
|
154
|
+
let annual = annualize_return(monthly, 12.0);
|
|
155
|
+
// (1.01)^12 - 1 ~ 0.1268
|
|
156
|
+
assert!((annual - 0.12682503013196972).abs() < 1e-10);
|
|
157
|
+
}
|
|
158
|
+
}
|