@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,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
|
+
}
|