@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,222 @@
1
+ //! Moving Average Indicators
2
+ //!
3
+ //! Various moving average implementations for trend analysis.
4
+
5
+ use crate::utils::{mean, rolling};
6
+
7
+ /// Simple Moving Average (SMA)
8
+ ///
9
+ /// Calculates the unweighted mean of the previous `period` data points.
10
+ ///
11
+ /// # Arguments
12
+ /// * `data` - Price data slice
13
+ /// * `period` - Number of periods to average
14
+ ///
15
+ /// # Returns
16
+ /// A vector of SMA values. Returns empty vector if data length < period.
17
+ ///
18
+ /// # Example
19
+ /// ```
20
+ /// use quant_rust::indicators::sma;
21
+ /// let prices = vec![10.0, 12.0, 14.0, 16.0, 18.0];
22
+ /// let result = sma(&prices, 3);
23
+ /// assert_eq!(result, vec![12.0, 14.0, 16.0]);
24
+ /// ```
25
+ pub fn sma(data: &[f64], period: usize) -> Vec<f64> {
26
+ if data.len() < period || period == 0 {
27
+ return Vec::new();
28
+ }
29
+ rolling(data, period, |slice| mean(slice))
30
+ }
31
+
32
+ /// Exponential Moving Average (EMA)
33
+ ///
34
+ /// A weighted moving average that gives more weight to recent prices.
35
+ /// Uses the formula: EMA = (Price - prev_EMA) * multiplier + prev_EMA
36
+ /// where multiplier = 2 / (period + 1)
37
+ ///
38
+ /// # Arguments
39
+ /// * `data` - Price data slice
40
+ /// * `period` - EMA period
41
+ ///
42
+ /// # Returns
43
+ /// A vector of EMA values. First value is SMA of initial period.
44
+ pub fn ema(data: &[f64], period: usize) -> Vec<f64> {
45
+ if data.len() < period || period == 0 {
46
+ return Vec::new();
47
+ }
48
+
49
+ let multiplier = 2.0 / (period + 1) as f64;
50
+ let mut result = Vec::with_capacity(data.len() - period + 1);
51
+
52
+ // First EMA value is SMA
53
+ let mut prev_ema = mean(&data[0..period]);
54
+ result.push(prev_ema);
55
+
56
+ // Calculate EMA for remaining values
57
+ for i in period..data.len() {
58
+ let current_ema = (data[i] - prev_ema) * multiplier + prev_ema;
59
+ result.push(current_ema);
60
+ prev_ema = current_ema;
61
+ }
62
+
63
+ result
64
+ }
65
+
66
+ /// Weighted Moving Average (WMA)
67
+ ///
68
+ /// A moving average where each data point is multiplied by a weight factor.
69
+ /// More recent data points have higher weights.
70
+ ///
71
+ /// # Arguments
72
+ /// * `data` - Price data slice
73
+ /// * `period` - WMA period
74
+ ///
75
+ /// # Returns
76
+ /// A vector of WMA values.
77
+ pub fn wma(data: &[f64], period: usize) -> Vec<f64> {
78
+ if data.len() < period || period == 0 {
79
+ return Vec::new();
80
+ }
81
+
82
+ rolling(data, period, |slice| {
83
+ let n = slice.len();
84
+ let weighted_sum: f64 = slice
85
+ .iter()
86
+ .enumerate()
87
+ .map(|(i, &val)| val * (i + 1) as f64)
88
+ .sum();
89
+ let weight_sum: f64 = (1..=n).map(|i| i as f64).sum();
90
+ weighted_sum / weight_sum
91
+ })
92
+ }
93
+
94
+ /// Hull Moving Average (HMA)
95
+ ///
96
+ /// Developed by Alan Hull to reduce lag while maintaining smoothness.
97
+ /// Uses weighted moving averages of different periods to achieve this.
98
+ ///
99
+ /// # Arguments
100
+ /// * `data` - Price data slice
101
+ /// * `period` - HMA period
102
+ ///
103
+ /// # Returns
104
+ /// A vector of HMA values.
105
+ pub fn hma(data: &[f64], period: usize) -> Vec<f64> {
106
+ if data.len() < period || period == 0 {
107
+ return Vec::new();
108
+ }
109
+
110
+ let half_period = period / 2;
111
+ let sqrt_period = (period as f64).sqrt().floor() as usize;
112
+
113
+ if half_period == 0 || sqrt_period == 0 {
114
+ return Vec::new();
115
+ }
116
+
117
+ let wma_half = wma(data, half_period);
118
+ let wma_full = wma(data, period);
119
+
120
+ // Calculate raw HMA: 2 * WMA(half) - WMA(full)
121
+ let offset = period - half_period;
122
+ let mut raw_hma = Vec::new();
123
+
124
+ for i in 0..wma_half.len() {
125
+ if i + offset < wma_full.len() {
126
+ raw_hma.push(2.0 * wma_half[i] - wma_full[i + offset]);
127
+ }
128
+ }
129
+
130
+ // Apply WMA with sqrt(period) to smooth
131
+ wma(&raw_hma, sqrt_period)
132
+ }
133
+
134
+ /// Volume Weighted Moving Average (VWMA)
135
+ ///
136
+ /// A moving average that incorporates volume into the calculation.
137
+ /// VWMA = Sum(Price * Volume) / Sum(Volume)
138
+ ///
139
+ /// # Arguments
140
+ /// * `prices` - Price data slice
141
+ /// * `volumes` - Volume data slice
142
+ /// * `period` - VWMA period
143
+ ///
144
+ /// # Returns
145
+ /// A vector of VWMA values.
146
+ pub fn vwma(prices: &[f64], volumes: &[f64], period: usize) -> Vec<f64> {
147
+ if prices.len() < period || volumes.len() < period || period == 0 {
148
+ return Vec::new();
149
+ }
150
+
151
+ let mut result = Vec::new();
152
+
153
+ for i in (period - 1)..prices.len() {
154
+ let start = i + 1 - period;
155
+ let end = i + 1;
156
+
157
+ let pv_sum: f64 = prices[start..end]
158
+ .iter()
159
+ .zip(volumes[start..end].iter())
160
+ .map(|(&p, &v)| p * v)
161
+ .sum();
162
+ let v_sum: f64 = volumes[start..end].iter().sum();
163
+
164
+ result.push(if v_sum > 0.0 {
165
+ pv_sum / v_sum
166
+ } else {
167
+ prices[i]
168
+ });
169
+ }
170
+
171
+ result
172
+ }
173
+
174
+ #[cfg(test)]
175
+ mod tests {
176
+ use super::*;
177
+
178
+ #[test]
179
+ fn test_sma() {
180
+ let prices = vec![10.0, 12.0, 14.0, 16.0, 18.0];
181
+ let result = sma(&prices, 3);
182
+ assert_eq!(result.len(), 3);
183
+ assert!((result[0] - 12.0).abs() < 1e-10);
184
+ assert!((result[1] - 14.0).abs() < 1e-10);
185
+ assert!((result[2] - 16.0).abs() < 1e-10);
186
+ }
187
+
188
+ #[test]
189
+ fn test_sma_empty() {
190
+ let prices = vec![10.0, 12.0];
191
+ let result = sma(&prices, 5);
192
+ assert!(result.is_empty());
193
+ }
194
+
195
+ #[test]
196
+ fn test_ema() {
197
+ let prices = vec![10.0, 12.0, 14.0, 16.0, 18.0, 20.0];
198
+ let result = ema(&prices, 3);
199
+ assert_eq!(result.len(), 4);
200
+ // First EMA should be SMA of first 3 values
201
+ assert!((result[0] - 12.0).abs() < 1e-10);
202
+ }
203
+
204
+ #[test]
205
+ fn test_wma() {
206
+ let prices = vec![10.0, 20.0, 30.0];
207
+ let result = wma(&prices, 3);
208
+ assert_eq!(result.len(), 1);
209
+ // WMA = (10*1 + 20*2 + 30*3) / (1+2+3) = 140/6 = 23.333...
210
+ assert!((result[0] - 23.333333333333332).abs() < 1e-10);
211
+ }
212
+
213
+ #[test]
214
+ fn test_vwma() {
215
+ let prices = vec![10.0, 12.0, 14.0, 16.0];
216
+ let volumes = vec![100.0, 200.0, 100.0, 150.0];
217
+ let result = vwma(&prices, &volumes, 3);
218
+ assert_eq!(result.len(), 2);
219
+ // VWMA for first window: (10*100 + 12*200 + 14*100) / (100+200+100) = 4800/400 = 12.0
220
+ assert!((result[0] - 12.0).abs() < 1e-10);
221
+ }
222
+ }
@@ -0,0 +1,18 @@
1
+ //! Technical Indicators
2
+ //!
3
+ //! Implementations of classic and modern technical indicators for price analysis.
4
+
5
+ pub mod ma;
6
+ pub mod momentum;
7
+ pub mod volatility;
8
+ pub mod trend;
9
+ pub mod sr;
10
+ pub mod volume;
11
+
12
+ // Re-export all types and functions
13
+ pub use ma::*;
14
+ pub use momentum::*;
15
+ pub use volatility::*;
16
+ pub use trend::*;
17
+ pub use sr::*;
18
+ pub use volume::*;
@@ -0,0 +1,353 @@
1
+ //! Momentum Indicators
2
+ //!
3
+ //! Indicators that measure the speed and strength of price movements.
4
+
5
+ use crate::utils::{mean, max, min};
6
+
7
+ /// MACD (Moving Average Convergence Divergence) result
8
+ #[derive(Debug, Clone, PartialEq)]
9
+ pub struct MACDResult {
10
+ /// MACD line (fast EMA - slow EMA)
11
+ pub macd: Vec<f64>,
12
+ /// Signal line (EMA of MACD)
13
+ pub signal: Vec<f64>,
14
+ /// Histogram (MACD - Signal)
15
+ pub histogram: Vec<f64>,
16
+ }
17
+
18
+ /// Stochastic Oscillator result
19
+ #[derive(Debug, Clone, PartialEq)]
20
+ pub struct StochasticResult {
21
+ /// %K line (fast stochastic)
22
+ pub k: Vec<f64>,
23
+ /// %D line (SMA of %K)
24
+ pub d: Vec<f64>,
25
+ }
26
+
27
+ /// Relative Strength Index (RSI)
28
+ ///
29
+ /// Measures the speed and magnitude of recent price changes.
30
+ /// RSI ranges from 0 to 100, with readings above 70 indicating overbought
31
+ /// and below 30 indicating oversold conditions.
32
+ ///
33
+ /// # Arguments
34
+ /// * `data` - Price data slice
35
+ /// * `period` - RSI period (default: 14)
36
+ ///
37
+ /// # Returns
38
+ /// A vector of RSI values (0-100).
39
+ pub fn rsi(data: &[f64], period: usize) -> Vec<f64> {
40
+ if data.len() < period + 1 || period == 0 {
41
+ return Vec::new();
42
+ }
43
+
44
+ let mut gains: Vec<f64> = Vec::with_capacity(data.len() - 1);
45
+ let mut losses: Vec<f64> = Vec::with_capacity(data.len() - 1);
46
+
47
+ // Calculate price changes
48
+ for i in 1..data.len() {
49
+ let change = data[i] - data[i - 1];
50
+ gains.push(if change > 0.0 { change } else { 0.0 });
51
+ losses.push(if change < 0.0 { change.abs() } else { 0.0 });
52
+ }
53
+
54
+ // First average
55
+ let mut avg_gain = mean(&gains[0..period]);
56
+ let mut avg_loss = mean(&losses[0..period]);
57
+
58
+ let mut result = Vec::new();
59
+
60
+ // First RSI
61
+ if avg_loss == 0.0 {
62
+ result.push(100.0);
63
+ } else {
64
+ let rs = avg_gain / avg_loss;
65
+ result.push(100.0 - 100.0 / (1.0 + rs));
66
+ }
67
+
68
+ // Subsequent values using smoothed average
69
+ for i in period..gains.len() {
70
+ avg_gain = (avg_gain * (period - 1) as f64 + gains[i]) / period as f64;
71
+ avg_loss = (avg_loss * (period - 1) as f64 + losses[i]) / period as f64;
72
+
73
+ if avg_loss == 0.0 {
74
+ result.push(100.0);
75
+ } else {
76
+ let rs = avg_gain / avg_loss;
77
+ result.push(100.0 - 100.0 / (1.0 + rs));
78
+ }
79
+ }
80
+
81
+ result
82
+ }
83
+
84
+ /// MACD (Moving Average Convergence Divergence)
85
+ ///
86
+ /// A trend-following momentum indicator showing the relationship
87
+ /// between two EMAs of price.
88
+ ///
89
+ /// # Arguments
90
+ /// * `data` - Price data slice
91
+ /// * `fast` - Fast EMA period (default: 12)
92
+ /// * `slow` - Slow EMA period (default: 26)
93
+ /// * `signal` - Signal line period (default: 9)
94
+ ///
95
+ /// # Returns
96
+ /// A MACDResult containing MACD line, signal line, and histogram.
97
+ pub fn macd(data: &[f64], fast: usize, slow: usize, signal: usize) -> MACDResult {
98
+ if data.len() < slow || fast == 0 || slow == 0 || signal == 0 {
99
+ return MACDResult {
100
+ macd: Vec::new(),
101
+ signal: Vec::new(),
102
+ histogram: Vec::new(),
103
+ };
104
+ }
105
+
106
+ let fast_ema = super::ema(data, fast);
107
+ let slow_ema = super::ema(data, slow);
108
+
109
+ // Align arrays (slow EMA starts later)
110
+ let offset = slow - fast;
111
+ let mut macd_line: Vec<f64> = Vec::with_capacity(slow_ema.len());
112
+
113
+ for i in 0..slow_ema.len() {
114
+ if i + offset < fast_ema.len() {
115
+ macd_line.push(fast_ema[i + offset] - slow_ema[i]);
116
+ }
117
+ }
118
+
119
+ let signal_line = super::ema(&macd_line, signal);
120
+
121
+ // Align histogram with signal
122
+ let signal_offset = signal - 1;
123
+ let mut histogram: Vec<f64> = Vec::new();
124
+
125
+ for i in 0..signal_line.len() {
126
+ if i + signal_offset < macd_line.len() {
127
+ histogram.push(macd_line[i + signal_offset] - signal_line[i]);
128
+ }
129
+ }
130
+
131
+ // Align all outputs
132
+ let aligned_macd = macd_line[signal_offset..signal_offset + histogram.len()].to_vec();
133
+
134
+ MACDResult {
135
+ macd: aligned_macd,
136
+ signal: signal_line,
137
+ histogram,
138
+ }
139
+ }
140
+
141
+ /// Stochastic Oscillator
142
+ ///
143
+ /// A momentum indicator comparing a particular closing price
144
+ /// to a range of its prices over a certain period.
145
+ ///
146
+ /// # Arguments
147
+ /// * `high` - High prices
148
+ /// * `low` - Low prices
149
+ /// * `close` - Close prices
150
+ /// * `k_period` - %K period (default: 14)
151
+ /// * `d_period` - %D period (default: 3)
152
+ ///
153
+ /// # Returns
154
+ /// A StochasticResult containing %K and %D lines.
155
+ pub fn stochastic(
156
+ high: &[f64],
157
+ low: &[f64],
158
+ close: &[f64],
159
+ k_period: usize,
160
+ d_period: usize,
161
+ ) -> StochasticResult {
162
+ if high.len() < k_period || k_period == 0 || d_period == 0 {
163
+ return StochasticResult {
164
+ k: Vec::new(),
165
+ d: Vec::new(),
166
+ };
167
+ }
168
+
169
+ let mut k_values: Vec<f64> = Vec::new();
170
+
171
+ for i in (k_period - 1)..close.len() {
172
+ let start = i + 1 - k_period;
173
+ let end = i + 1;
174
+
175
+ let highest_high = max(&high[start..end]);
176
+ let lowest_low = min(&low[start..end]);
177
+
178
+ if highest_high.is_nan() || lowest_low.is_nan() {
179
+ k_values.push(50.0);
180
+ continue;
181
+ }
182
+
183
+ if (highest_high - lowest_low).abs() < 1e-10 {
184
+ k_values.push(50.0); // Neutral if range is 0
185
+ } else {
186
+ k_values.push((close[i] - lowest_low) / (highest_high - lowest_low) * 100.0);
187
+ }
188
+ }
189
+
190
+ let d_values = super::sma(&k_values, d_period);
191
+
192
+ // Align K with D
193
+ let offset = d_period - 1;
194
+ let aligned_k = if offset < k_values.len() && d_values.len() > 0 {
195
+ k_values[offset..offset + d_values.len()].to_vec()
196
+ } else {
197
+ Vec::new()
198
+ };
199
+
200
+ StochasticResult {
201
+ k: aligned_k,
202
+ d: d_values,
203
+ }
204
+ }
205
+
206
+ /// Williams %R
207
+ ///
208
+ /// A momentum indicator measuring overbought/oversold levels.
209
+ /// Ranges from -100 to 0, with readings above -20 overbought
210
+ /// and below -80 oversold.
211
+ ///
212
+ /// # Arguments
213
+ /// * `high` - High prices
214
+ /// * `low` - Low prices
215
+ /// * `close` - Close prices
216
+ /// * `period` - Lookback period (default: 14)
217
+ ///
218
+ /// # Returns
219
+ /// A vector of Williams %R values (-100 to 0).
220
+ pub fn williams_r(high: &[f64], low: &[f64], close: &[f64], period: usize) -> Vec<f64> {
221
+ if high.len() < period || period == 0 {
222
+ return Vec::new();
223
+ }
224
+
225
+ let mut result = Vec::new();
226
+
227
+ for i in (period - 1)..close.len() {
228
+ let start = i + 1 - period;
229
+ let end = i + 1;
230
+
231
+ let highest_high = max(&high[start..end]);
232
+ let lowest_low = min(&low[start..end]);
233
+
234
+ if (highest_high - lowest_low).abs() < 1e-10 {
235
+ result.push(-50.0);
236
+ } else {
237
+ result.push((highest_high - close[i]) / (highest_high - lowest_low) * -100.0);
238
+ }
239
+ }
240
+
241
+ result
242
+ }
243
+
244
+ /// Rate of Change (ROC)
245
+ ///
246
+ /// Measures the percentage change in price from a previous period.
247
+ ///
248
+ /// # Arguments
249
+ /// * `data` - Price data slice
250
+ /// * `period` - Lookback period (default: 14)
251
+ ///
252
+ /// # Returns
253
+ /// A vector of ROC values (percentage).
254
+ pub fn roc(data: &[f64], period: usize) -> Vec<f64> {
255
+ if data.len() < period + 1 || period == 0 {
256
+ return Vec::new();
257
+ }
258
+
259
+ data[period..]
260
+ .iter()
261
+ .enumerate()
262
+ .map(|(i, &val)| {
263
+ let old_val = data[i];
264
+ if old_val != 0.0 {
265
+ ((val - old_val) / old_val) * 100.0
266
+ } else {
267
+ 0.0
268
+ }
269
+ })
270
+ .collect()
271
+ }
272
+
273
+ /// Momentum
274
+ ///
275
+ /// Measures the rate of price change.
276
+ /// Momentum = Current Price - Price N periods ago
277
+ ///
278
+ /// # Arguments
279
+ /// * `data` - Price data slice
280
+ /// * `period` - Lookback period (default: 14)
281
+ ///
282
+ /// # Returns
283
+ /// A vector of momentum values.
284
+ pub fn momentum(data: &[f64], period: usize) -> Vec<f64> {
285
+ if data.len() < period + 1 || period == 0 {
286
+ return Vec::new();
287
+ }
288
+
289
+ data[period..]
290
+ .iter()
291
+ .enumerate()
292
+ .map(|(i, &val)| val - data[i])
293
+ .collect()
294
+ }
295
+
296
+ #[cfg(test)]
297
+ mod tests {
298
+ use super::*;
299
+
300
+ #[test]
301
+ fn test_rsi() {
302
+ // RSI with all gains should be 100
303
+ let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0];
304
+ let result = rsi(&prices, 14);
305
+ assert!(!result.is_empty());
306
+ // All upward moves should give very high RSI
307
+ assert!(result[0] > 99.0);
308
+ }
309
+
310
+ #[test]
311
+ fn test_macd() {
312
+ let prices: Vec<f64> = (1..=50).map(|x| x as f64).collect();
313
+ let result = macd(&prices, 12, 26, 9);
314
+ // MACD line should have values
315
+ assert!(!result.macd.is_empty());
316
+ }
317
+
318
+ #[test]
319
+ fn test_stochastic() {
320
+ let high = vec![10.0, 12.0, 15.0, 14.0, 16.0, 18.0, 17.0, 19.0, 21.0, 20.0,
321
+ 22.0, 24.0, 23.0, 25.0, 27.0, 26.0, 28.0, 30.0, 29.0, 31.0,
322
+ 33.0, 32.0, 34.0, 36.0];
323
+ let low = vec![8.0, 9.0, 11.0, 10.0, 12.0, 14.0, 13.0, 15.0, 17.0, 16.0,
324
+ 18.0, 20.0, 19.0, 21.0, 23.0, 22.0, 24.0, 26.0, 25.0, 27.0,
325
+ 29.0, 28.0, 30.0, 32.0];
326
+ let close = vec![9.0, 11.0, 14.0, 12.0, 15.0, 17.0, 15.0, 18.0, 20.0, 18.0,
327
+ 21.0, 23.0, 21.0, 24.0, 26.0, 24.0, 27.0, 29.0, 27.0, 30.0,
328
+ 32.0, 30.0, 33.0, 35.0];
329
+
330
+ let result = stochastic(&high, &low, &close, 14, 3);
331
+ assert!(!result.k.is_empty());
332
+ assert!(!result.d.is_empty());
333
+ }
334
+
335
+ #[test]
336
+ fn test_roc() {
337
+ let prices = vec![100.0, 110.0, 105.0, 115.0];
338
+ let result = roc(&prices, 1);
339
+ assert_eq!(result.len(), 3);
340
+ assert!((result[0] - 10.0).abs() < 1e-10); // (110-100)/100 * 100
341
+ assert!((result[1] - (-4.5454)).abs() < 0.01); // (105-110)/110 * 100
342
+ }
343
+
344
+ #[test]
345
+ fn test_momentum() {
346
+ let prices = vec![100.0, 110.0, 105.0, 115.0];
347
+ let result = momentum(&prices, 1);
348
+ assert_eq!(result.len(), 3);
349
+ assert_eq!(result[0], 10.0);
350
+ assert_eq!(result[1], -5.0);
351
+ assert_eq!(result[2], 10.0);
352
+ }
353
+ }