@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.
Files changed (60) hide show
  1. package/README.md +161 -0
  2. package/bun-ffi.d.ts +54 -0
  3. package/dist/index.js +576 -0
  4. package/dist/src/index.d.ts +324 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/types/index.d.ts +403 -0
  7. package/dist/types/index.d.ts.map +1 -0
  8. package/native/README.md +62 -0
  9. package/native/darwin-arm64/libquant_rust.dylib +0 -0
  10. package/package.json +70 -0
  11. package/scripts/postinstall.cjs +85 -0
  12. package/src/ffi.rs +496 -0
  13. package/src/index.ts +1073 -0
  14. package/src/indicators/ma.rs +222 -0
  15. package/src/indicators/mod.rs +18 -0
  16. package/src/indicators/momentum.rs +353 -0
  17. package/src/indicators/sr.rs +195 -0
  18. package/src/indicators/trend.rs +351 -0
  19. package/src/indicators/volatility.rs +270 -0
  20. package/src/indicators/volume.rs +213 -0
  21. package/src/lib.rs +130 -0
  22. package/src/patterns/breakout.rs +431 -0
  23. package/src/patterns/chart.rs +772 -0
  24. package/src/patterns/mod.rs +394 -0
  25. package/src/patterns/sr.rs +423 -0
  26. package/src/prediction/amm.rs +338 -0
  27. package/src/prediction/arbitrage.rs +230 -0
  28. package/src/prediction/calibration.rs +317 -0
  29. package/src/prediction/kelly.rs +232 -0
  30. package/src/prediction/lmsr.rs +194 -0
  31. package/src/prediction/mod.rs +59 -0
  32. package/src/prediction/odds.rs +229 -0
  33. package/src/prediction/pnl.rs +254 -0
  34. package/src/prediction/risk.rs +228 -0
  35. package/src/risk/beta.rs +257 -0
  36. package/src/risk/drawdown.rs +256 -0
  37. package/src/risk/leverage.rs +201 -0
  38. package/src/risk/mod.rs +388 -0
  39. package/src/risk/portfolio.rs +287 -0
  40. package/src/risk/ratios.rs +290 -0
  41. package/src/risk/sizing.rs +194 -0
  42. package/src/risk/var.rs +222 -0
  43. package/src/stats/cdf.rs +257 -0
  44. package/src/stats/correlation.rs +225 -0
  45. package/src/stats/distribution.rs +194 -0
  46. package/src/stats/hypothesis.rs +177 -0
  47. package/src/stats/matrix.rs +346 -0
  48. package/src/stats/mod.rs +257 -0
  49. package/src/stats/regression.rs +239 -0
  50. package/src/stats/rolling.rs +193 -0
  51. package/src/stats/timeseries.rs +263 -0
  52. package/src/types.rs +224 -0
  53. package/src/utils/mod.rs +215 -0
  54. package/src/utils/normalize.rs +192 -0
  55. package/src/utils/price.rs +167 -0
  56. package/src/utils/quantiles.rs +177 -0
  57. package/src/utils/returns.rs +158 -0
  58. package/src/utils/rolling.rs +97 -0
  59. package/src/utils/stats.rs +154 -0
  60. 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
+ }