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 fn update(&mut self, value: f64) -> Option<f64> {
60 self.slope.update(value).map(|s| s.atan().to_degrees())
61 }
62
63 fn reset(&mut self) {
64 self.slope.reset();
65 }
66
67 fn warmup_period(&self) -> usize {
68 self.slope.warmup_period()
69 }
70
71 fn is_ready(&self) -> bool {
72 self.slope.is_ready()
73 }
74
75 fn name(&self) -> &'static str {
76 "LinRegAngle"
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::traits::BatchExt;
84 use approx::assert_relative_eq;
85
86 #[test]
87 fn unit_slope_is_forty_five_degrees() {
88 let mut angle = LinRegAngle::new(5).unwrap();
90 let out = angle.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
91 for (i, v) in out.iter().enumerate().take(4) {
92 assert!(v.is_none(), "index {i} must be None during warmup");
93 }
94 assert_relative_eq!(out[4].unwrap(), 45.0, epsilon = 1e-9);
95 assert_relative_eq!(out[5].unwrap(), 45.0, epsilon = 1e-9);
96 }
97
98 #[test]
99 fn reference_value_steep_slope() {
100 let mut angle = LinRegAngle::new(3).unwrap();
102 let out = angle.batch(&[1.0, 2.0, 9.0]);
103 assert_relative_eq!(out[2].unwrap(), 4.0_f64.atan().to_degrees(), epsilon = 1e-9);
104 }
105
106 #[test]
107 fn constant_series_has_zero_angle() {
108 let mut angle = LinRegAngle::new(8).unwrap();
109 for v in angle.batch(&[42.0; 20]).into_iter().flatten() {
110 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
111 }
112 }
113
114 #[test]
115 fn falling_series_has_negative_angle() {
116 let prices: Vec<f64> = (0..30).map(|i| 100.0 - f64::from(i)).collect();
117 let mut angle = LinRegAngle::new(10).unwrap();
118 for v in angle.batch(&prices).into_iter().flatten() {
119 assert!(v < 0.0, "a falling series must have a negative angle");
120 }
121 }
122
123 #[test]
124 fn stays_within_ninety_degrees() {
125 let prices: Vec<f64> = (0..60)
126 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 1000.0)
127 .collect();
128 let mut angle = LinRegAngle::new(14).unwrap();
129 for v in angle.batch(&prices).into_iter().flatten() {
130 assert!(v > -90.0 && v < 90.0, "angle {v} outside (-90, 90)");
131 }
132 }
133
134 #[test]
135 fn rejects_period_below_two() {
136 assert!(LinRegAngle::new(0).is_err());
137 assert!(LinRegAngle::new(1).is_err());
138 assert!(LinRegAngle::new(2).is_ok());
139 }
140
141 #[test]
145 fn accessors_and_metadata() {
146 let a = LinRegAngle::new(14).unwrap();
147 assert_eq!(a.period(), 14);
148 assert_eq!(a.warmup_period(), 14);
149 assert_eq!(a.name(), "LinRegAngle");
150 }
151
152 #[test]
153 fn reset_clears_state() {
154 let mut angle = LinRegAngle::new(5).unwrap();
155 angle.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
156 assert!(angle.is_ready());
157 angle.reset();
158 assert!(!angle.is_ready());
159 assert_eq!(angle.update(1.0), None);
160 }
161
162 #[test]
163 fn batch_equals_streaming() {
164 let prices: Vec<f64> = (0..60)
165 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 10.0)
166 .collect();
167 let mut a = LinRegAngle::new(14).unwrap();
168 let mut b = LinRegAngle::new(14).unwrap();
169 assert_eq!(
170 a.batch(&prices),
171 prices.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
172 );
173 }
174}