wickra_core/indicators/
linreg_angle.rs1use crate::error::Result;
4use crate::indicators::linreg_slope::LinRegSlope;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
33pub struct LinRegAngle {
34 slope: LinRegSlope,
35}
36
37impl LinRegAngle {
38 pub fn new(period: usize) -> Result<Self> {
44 Ok(Self {
45 slope: LinRegSlope::new(period)?,
46 })
47 }
48
49 pub const fn period(&self) -> usize {
51 self.slope.period()
52 }
53}
54
55impl Indicator for LinRegAngle {
56 type Input = f64;
57 type Output = f64;
58
59 #[inline]
60 fn update(&mut self, value: f64) -> Option<f64> {
61 if !value.is_finite() {
62 return None;
63 }
64 self.slope.update(value).map(|s| s.atan().to_degrees())
65 }
66
67 fn reset(&mut self) {
68 self.slope.reset();
69 }
70
71 #[inline]
72 fn warmup_period(&self) -> usize {
73 self.slope.warmup_period()
74 }
75
76 #[inline]
77 fn is_ready(&self) -> bool {
78 self.slope.is_ready()
79 }
80
81 #[inline]
82 fn name(&self) -> &'static str {
83 "LinRegAngle"
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::traits::BatchExt;
91 use approx::assert_relative_eq;
92
93 #[test]
94 fn unit_slope_is_forty_five_degrees() {
95 let mut angle = LinRegAngle::new(5).unwrap();
97 let out = angle.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
98 for (i, v) in out.iter().enumerate().take(4) {
99 assert!(v.is_none(), "index {i} must be None during warmup");
100 }
101 assert_relative_eq!(out[4].unwrap(), 45.0, epsilon = 1e-9);
102 assert_relative_eq!(out[5].unwrap(), 45.0, epsilon = 1e-9);
103 }
104
105 #[test]
106 fn reference_value_steep_slope() {
107 let mut angle = LinRegAngle::new(3).unwrap();
109 let out = angle.batch(&[1.0, 2.0, 9.0]);
110 assert_relative_eq!(out[2].unwrap(), 4.0_f64.atan().to_degrees(), epsilon = 1e-9);
111 }
112
113 #[test]
114 fn constant_series_has_zero_angle() {
115 let mut angle = LinRegAngle::new(8).unwrap();
116 for v in angle.batch(&[42.0; 20]).into_iter().flatten() {
117 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
118 }
119 }
120
121 #[test]
122 fn falling_series_has_negative_angle() {
123 let prices: Vec<f64> = (0..30).map(|i| 100.0 - f64::from(i)).collect();
124 let mut angle = LinRegAngle::new(10).unwrap();
125 for v in angle.batch(&prices).into_iter().flatten() {
126 assert!(v < 0.0, "a falling series must have a negative angle");
127 }
128 }
129
130 #[test]
131 fn stays_within_ninety_degrees() {
132 let prices: Vec<f64> = (0..60)
133 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 1000.0)
134 .collect();
135 let mut angle = LinRegAngle::new(14).unwrap();
136 for v in angle.batch(&prices).into_iter().flatten() {
137 assert!(v > -90.0 && v < 90.0, "angle {v} outside (-90, 90)");
138 }
139 }
140
141 #[test]
142 fn rejects_period_below_two() {
143 assert!(LinRegAngle::new(0).is_err());
144 assert!(LinRegAngle::new(1).is_err());
145 assert!(LinRegAngle::new(2).is_ok());
146 }
147
148 #[test]
152 fn accessors_and_metadata() {
153 let a = LinRegAngle::new(14).unwrap();
154 assert_eq!(a.period(), 14);
155 assert_eq!(a.warmup_period(), 14);
156 assert_eq!(a.name(), "LinRegAngle");
157 }
158
159 #[test]
160 fn reset_clears_state() {
161 let mut angle = LinRegAngle::new(5).unwrap();
162 angle.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
163 assert!(angle.is_ready());
164 angle.reset();
165 assert!(!angle.is_ready());
166 assert_eq!(angle.update(1.0), None);
167 }
168
169 #[test]
170 fn batch_equals_streaming() {
171 let prices: Vec<f64> = (0..60)
172 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 10.0)
173 .collect();
174 let mut a = LinRegAngle::new(14).unwrap();
175 let mut b = LinRegAngle::new(14).unwrap();
176 assert_eq!(
177 a.batch(&prices),
178 prices.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
179 );
180 }
181}