portfolio_analyzer/
indicators.rs1#[derive(Debug, Clone)]
4pub struct OHLCV {
5 pub open: f64,
6 pub high: f64,
7 pub low: f64,
8 pub close: f64,
9 pub volume: f64,
10}
11
12pub fn sma(data: &[f64], length: usize) -> Vec<f64> {
14 let n = data.len();
15 let mut out = vec![f64::NAN; n];
16 for i in (length - 1)..n {
17 let s: f64 = data[i + 1 - length..=i].iter().sum();
18 out[i] = s / length as f64;
19 }
20 out
21}
22
23pub fn ema(data: &[f64], span: usize) -> Vec<f64> {
24 let n = data.len();
25 let mut out = vec![f64::NAN; n];
26 let alpha = 2.0 / (span as f64 + 1.0);
27 let mut prev = f64::NAN;
28 for i in 0..n {
29 if data[i].is_nan() {
30 out[i] = prev;
31 } else if prev.is_nan() {
32 prev = data[i];
33 out[i] = data[i];
34 } else {
35 prev = alpha * data[i] + (1.0 - alpha) * prev;
36 out[i] = prev;
37 }
38 }
39 out
40}
41
42pub fn rolling_std(data: &[f64], length: usize) -> Vec<f64> {
43 let n = data.len();
44 let mut out = vec![f64::NAN; n];
45 for i in (length - 1)..n {
46 let window = &data[i + 1 - length..=i];
47 let mean = window.iter().sum::<f64>() / length as f64;
48 let var = window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / length as f64;
49 out[i] = var.sqrt();
50 }
51 out
52}
53
54fn true_range(bars: &[OHLCV]) -> Vec<f64> {
55 let n = bars.len();
56 let mut tr = vec![0.0f64; n];
57 for i in 0..n {
58 let hl = bars[i].high - bars[i].low;
59 if i == 0 {
60 tr[i] = hl;
61 } else {
62 let hc = (bars[i].high - bars[i - 1].close).abs();
63 let lc = (bars[i].low - bars[i - 1].close).abs();
64 tr[i] = hl.max(hc).max(lc);
65 }
66 }
67 tr
68}
69
70pub struct BB {
72 pub upper: Vec<f64>,
73 pub lower: Vec<f64>,
74 pub mid: Vec<f64>,
75}
76
77pub fn bollinger_bands(close: &[f64], length: usize, num_std: f64) -> BB {
78 let mid = sma(close, length);
79 let std = rolling_std(close, length);
80 let n = close.len();
81 let mut upper = vec![f64::NAN; n];
82 let mut lower = vec![f64::NAN; n];
83 for i in 0..n {
84 if !mid[i].is_nan() && !std[i].is_nan() {
85 upper[i] = mid[i] + num_std * std[i];
86 lower[i] = mid[i] - num_std * std[i];
87 }
88 }
89 BB { upper, lower, mid }
90}
91
92pub struct KC {
94 pub upper: Vec<f64>,
95 pub lower: Vec<f64>,
96 pub mid: Vec<f64>,
97}
98
99pub fn keltner_channels(bars: &[OHLCV], length: usize, factor: f64) -> KC {
100 let close: Vec<f64> = bars.iter().map(|b| b.close).collect();
101 let mid = sma(&close, length);
102 let tr = true_range(bars);
103 let atr = sma(&tr, length);
104 let n = bars.len();
105 let mut upper = vec![f64::NAN; n];
106 let mut lower = vec![f64::NAN; n];
107 for i in 0..n {
108 if !mid[i].is_nan() && !atr[i].is_nan() {
109 upper[i] = mid[i] + factor * atr[i];
110 lower[i] = mid[i] - factor * atr[i];
111 }
112 }
113 KC { upper, lower, mid }
114}
115
116pub fn compute_compression(bb: &BB, kc_high: &KC, kc_mid: &KC, kc_low: &KC) -> Vec<u8> {
118 let n = bb.upper.len();
119 let mut comp = vec![0u8; n];
120 for (i, state) in comp.iter_mut().enumerate() {
121 let bu = bb.upper[i];
122 if bu.is_nan() {
123 continue;
124 }
125 if bu <= kc_high.upper[i] {
130 *state = 3;
131 } else if bu <= kc_mid.upper[i] {
132 *state = 2;
133 } else if bu <= kc_low.upper[i] {
134 *state = 1;
135 }
136 }
137 comp
138}
139
140pub fn awesome_oscillator(bars: &[OHLCV]) -> Vec<f64> {
142 let hl2: Vec<f64> = bars.iter().map(|b| (b.high + b.low) / 2.0).collect();
143 let fast = sma(&hl2, 5);
144 let slow = sma(&hl2, 34);
145 fast.iter()
146 .zip(slow.iter())
147 .map(|(f, s)| {
148 if f.is_nan() || s.is_nan() {
149 f64::NAN
150 } else {
151 f - s
152 }
153 })
154 .collect()
155}
156
157pub fn smf_derivative(series: &[f64], window: usize) -> Vec<f64> {
159 let n = series.len();
160 if n == 0 || window == 0 {
161 return Vec::new();
162 }
163
164 let mean = sma(series, window);
169 let dev = series
170 .iter()
171 .zip(mean.iter())
172 .map(|(value, mean)| value - mean)
173 .collect::<Vec<_>>();
174
175 let covariance = |lag: usize| {
176 let products = (0..n)
177 .map(|i| {
178 if i < lag {
179 f64::NAN
180 } else {
181 dev[i] * dev[i - lag]
182 }
183 })
184 .collect::<Vec<_>>();
185 sma(&products, window)
186 };
187
188 let covariances = [
189 covariance(0),
190 covariance(1),
191 covariance(2),
192 covariance(3),
193 covariance(4),
194 ];
195 let mut result = vec![f64::NAN; n];
196 for i in 4..n {
197 let weights = [
198 covariances[0][i],
199 covariances[1][i],
200 covariances[2][i],
201 covariances[3][i],
202 covariances[4][i],
203 ];
204 if weights.iter().any(|weight| !weight.is_finite()) {
205 continue;
206 }
207 let sum_cov = weights.iter().sum::<f64>();
208 result[i] = if sum_cov == 0.0 {
209 series[i]
212 } else {
213 weights
214 .iter()
215 .enumerate()
216 .map(|(lag, weight)| weight * series[i - lag])
217 .sum::<f64>()
218 / sum_cov.abs()
219 };
220 }
221 result
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn bands(upper: Vec<f64>, lower: Vec<f64>) -> BB {
229 let n = upper.len();
230 BB {
231 upper,
232 lower,
233 mid: vec![0.0; n],
234 }
235 }
236
237 fn channels(upper: Vec<f64>, lower: Vec<f64>) -> KC {
238 let n = upper.len();
239 KC {
240 upper,
241 lower,
242 mid: vec![0.0; n],
243 }
244 }
245
246 #[test]
247 fn compression_matches_thinkscript_upper_band_definition() {
248 let bb = bands(vec![9.0, 12.0, 17.0, 25.0], vec![-100.0; 4]);
249 let high = channels(vec![10.0; 4], vec![100.0; 4]);
250 let mid = channels(vec![15.0; 4], vec![100.0; 4]);
251 let low = channels(vec![20.0; 4], vec![100.0; 4]);
252
253 assert_eq!(
254 compute_compression(&bb, &high, &mid, &low),
255 vec![3, 2, 1, 0]
256 );
257 }
258
259 #[test]
260 fn zero_covariance_smf_falls_back_to_the_input_first_difference() {
261 let differences = vec![2.0; 80];
262 let derivative = smf_derivative(&differences, 21);
263
264 assert_eq!(derivative[79], 2.0);
265 }
266
267 #[test]
268 fn smf_uses_historical_rolling_deviations_like_thinkscript() {
269 let series = (0..96)
270 .map(|i| (i as f64 * 0.37).sin() + i as f64 * 0.013)
271 .collect::<Vec<_>>();
272 let actual = smf_derivative(&series, 21);
273
274 let expected = [
277 (72, 1.729_684_523_073_122),
278 (84, 0.392_341_408_550_435_14),
279 (95, 1.127_093_313_500_873_4),
280 ];
281 for (index, expected) in expected {
282 assert!((actual[index] - expected).abs() < 1e-12);
283 }
284 }
285}