portfolio_analyzer/sd2/
primitives.rs1pub use crate::indicators::{ema, rolling_std, sma, OHLCV};
18
19pub fn true_range(bars: &[OHLCV]) -> Vec<f64> {
21 (0..bars.len())
22 .map(|i| {
23 let b = &bars[i];
24 if i == 0 {
25 b.high - b.low
26 } else {
27 let pc = bars[i - 1].close;
28 (b.high - b.low).max((b.high - pc).abs()).max((b.low - pc).abs())
29 }
30 })
31 .collect()
32}
33
34pub fn atr(bars: &[OHLCV], length: usize) -> Vec<f64> {
37 sma(&true_range(bars), length)
38}
39
40pub fn highest(data: &[f64], length: usize) -> Vec<f64> {
42 rolling(data, length, f64::max, f64::NEG_INFINITY)
43}
44pub fn lowest(data: &[f64], length: usize) -> Vec<f64> {
45 rolling(data, length, f64::min, f64::INFINITY)
46}
47fn rolling(data: &[f64], length: usize, f: fn(f64, f64) -> f64, init: f64) -> Vec<f64> {
48 let mut out = vec![f64::NAN; data.len()];
49 for i in (length.saturating_sub(1))..data.len() {
50 out[i] = data[i + 1 - length..=i].iter().copied().fold(init, f);
51 }
52 out
53}
54
55pub fn linreg_value(data: &[f64], length: usize) -> Vec<f64> {
58 let mut out = vec![f64::NAN; data.len()];
59 let n = length as f64;
60 let sx = (0..length).map(|x| x as f64).sum::<f64>();
61 let sxx = (0..length).map(|x| (x * x) as f64).sum::<f64>();
62 for i in (length - 1)..data.len() {
63 let w = &data[i + 1 - length..=i];
64 if w.iter().any(|v| !v.is_finite()) {
65 continue;
66 }
67 let sy: f64 = w.iter().sum();
68 let sxy: f64 = w.iter().enumerate().map(|(x, y)| x as f64 * y).sum();
69 let slope = (n * sxy - sx * sy) / (n * sxx - sx * sx);
70 let intercept = (sy - slope * sx) / n;
71 out[i] = intercept + slope * (n - 1.0);
72 }
73 out
74}
75
76pub fn rsi(close: &[f64], length: usize) -> Vec<f64> {
78 let n = close.len();
79 let mut out = vec![f64::NAN; n];
80 let (mut up, mut down) = (0.0, 0.0);
81 let a = 1.0 / length as f64;
82 for i in 1..n {
83 let d = close[i] - close[i - 1];
84 let (u, dn) = (d.max(0.0), (-d).max(0.0));
85 if i <= length {
86 up += u / length as f64;
87 down += dn / length as f64;
88 } else {
89 up = up * (1.0 - a) + u * a;
90 down = down * (1.0 - a) + dn * a;
91 }
92 if i >= length {
93 out[i] = if up + down == 0.0 { 50.0 } else { 100.0 * up / (up + down) };
94 }
95 }
96 out
97}
98
99pub fn macd_hist(close: &[f64]) -> Vec<f64> {
101 let line: Vec<f64> = ema(close, 12).iter().zip(ema(close, 26)).map(|(a, b)| a - b).collect();
102 let signal = ema(&line, 9);
103 line.iter().zip(signal).map(|(l, s)| l - s).collect()
104}
105
106pub fn diff(data: &[f64], k: usize) -> Vec<f64> {
108 (0..data.len())
109 .map(|i| if i >= k { data[i] - data[i - k] } else { f64::NAN })
110 .collect()
111}
112
113pub fn ratio(num: &[f64], den: &[f64]) -> Vec<f64> {
115 num.iter()
116 .zip(den)
117 .map(|(a, b)| if *b > 0.0 { a / b } else { f64::NAN })
118 .collect()
119}
120
121pub fn squash(x: f64, scale: f64) -> f64 {
124 if x.is_finite() {
125 (x / scale).tanh()
126 } else {
127 0.0
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn linreg_of_a_straight_line_is_its_endpoint() {
137 let line: Vec<f64> = (0..30).map(|x| 2.0 * x as f64 + 1.0).collect();
138 let v = linreg_value(&line, 20);
139 assert!((v[29] - 59.0).abs() < 1e-9);
140 }
141
142 #[test]
143 fn rsi_of_a_rising_series_is_100() {
144 let up: Vec<f64> = (0..40).map(|x| x as f64).collect();
145 assert_eq!(rsi(&up, 14)[39], 100.0);
146 }
147}