finance_solution/stocks/ta/
linear_regression.rs1use crate::stocks::ta::common::validate_series;
50use crate::stocks::ta::ring::RingF64;
51use crate::util::error::{require_finite, FinanceResult};
52use crate::util::primitives::PeriodLength;
53use crate::{columns_with_strings, print_table_locale_opt};
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct LinRegParams {
58 pub period: usize,
59}
60
61impl LinRegParams {
62 pub const fn new(period: usize) -> Self {
63 Self { period }
64 }
65
66 pub const fn period_20() -> Self {
68 Self { period: 20 }
69 }
70
71 pub const fn period_50() -> Self {
72 Self { period: 50 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq)]
78pub struct LinRegBar {
79 pub slope: f64,
81 pub intercept: f64,
83 pub r_squared: f64,
85 pub angle_radians: f64,
87 pub angle_degrees: f64,
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub struct ValidatedLinReg {
94 params: LinRegParams,
95}
96
97impl ValidatedLinReg {
98 pub fn new(params: LinRegParams) -> FinanceResult<Self> {
99 PeriodLength::new(params.period)?;
100 if params.period < 2 {
101 return Err(crate::util::error::FinanceError::Unsolvable {
102 message: "linear regression period must be >= 2",
103 });
104 }
105 Ok(Self { params })
106 }
107
108 pub fn params(self) -> LinRegParams {
109 self.params
110 }
111
112 pub fn compute(self, series: &[f64]) -> FinanceResult<Vec<Option<LinRegBar>>> {
113 linear_regression_validated(series, self)
114 }
115}
116
117#[derive(Clone, Debug)]
119pub struct LinRegState {
120 params: LinRegParams,
121 ring: RingF64,
122 last: Option<LinRegBar>,
123 scratch: Vec<f64>,
124}
125
126impl LinRegState {
127 pub fn new(params: LinRegParams) -> FinanceResult<Self> {
128 let _ = ValidatedLinReg::new(params)?;
129 Ok(Self {
130 params,
131 ring: RingF64::with_capacity(params.period),
132 last: None,
133 scratch: Vec::with_capacity(params.period),
134 })
135 }
136
137 pub fn from_history(params: LinRegParams, series: &[f64]) -> FinanceResult<Self> {
138 let mut s = Self::new(params)?;
139 let _ = s.push_bars(series)?;
140 Ok(s)
141 }
142
143 pub fn params(&self) -> LinRegParams {
144 self.params
145 }
146
147 pub fn reset(&mut self) {
148 self.ring.clear();
149 self.last = None;
150 self.scratch.clear();
151 }
152
153 pub fn push(&mut self, y: f64) -> FinanceResult<Option<LinRegBar>> {
154 require_finite("series", y)?;
155 self.ring.push(y);
156 if !self.ring.is_full() {
157 self.last = None;
158 return Ok(None);
159 }
160 self.ring.copy_ordered(&mut self.scratch);
161 let bar = fit_ols(&self.scratch);
162 self.last = Some(bar);
163 Ok(Some(bar))
164 }
165
166 pub fn push_bars(&mut self, series: &[f64]) -> FinanceResult<Vec<Option<LinRegBar>>> {
167 let mut out = Vec::with_capacity(series.len());
168 for &y in series {
169 out.push(self.push(y)?);
170 }
171 Ok(out)
172 }
173
174 pub fn last(&self) -> Option<LinRegBar> {
175 self.last
176 }
177}
178
179#[derive(Clone, Debug)]
180pub struct LinRegSolution {
181 series: Vec<Option<LinRegBar>>,
182 y: Vec<f64>,
183 params: LinRegParams,
184 formula: String,
185}
186
187impl LinRegSolution {
188 pub fn series(&self) -> &[Option<LinRegBar>] {
189 &self.series
190 }
191 pub fn formula(&self) -> &str {
192 &self.formula
193 }
194 pub fn params(&self) -> LinRegParams {
195 self.params
196 }
197
198 pub fn print_table(&self) {
199 self.print_table_locale_opt(None, None);
200 }
201
202 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
203 self.print_table_locale_opt(Some(locale), Some(precision));
204 }
205
206 fn print_table_locale_opt(
207 &self,
208 locale: Option<&num_format::Locale>,
209 precision: Option<usize>,
210 ) {
211 let columns = columns_with_strings(&[
212 ("period", "i", true),
213 ("y", "f", true),
214 ("slope", "f", true),
215 ("angle_deg", "f", true),
216 ("r2", "f", true),
217 ]);
218 let data = self
219 .y
220 .iter()
221 .enumerate()
222 .map(|(i, y)| {
223 let (slope, ang, r2) = match self.series[i] {
224 Some(b) => (
225 b.slope.to_string(),
226 b.angle_degrees.to_string(),
227 b.r_squared.to_string(),
228 ),
229 None => ("n/a".to_string(), "n/a".to_string(), "n/a".to_string()),
230 };
231 vec![i.to_string(), y.to_string(), slope, ang, r2]
232 })
233 .collect();
234 print_table_locale_opt(&columns, data, locale, precision);
235 }
236}
237
238pub fn linear_regression(
240 series: &[f64],
241 params: LinRegParams,
242) -> FinanceResult<Vec<Option<LinRegBar>>> {
243 ValidatedLinReg::new(params)?.compute(series)
244}
245
246pub fn linear_regression_solution(
247 series: &[f64],
248 params: LinRegParams,
249) -> FinanceResult<LinRegSolution> {
250 let s = linear_regression(series, params)?;
251 Ok(LinRegSolution {
252 series: s,
253 y: series.to_vec(),
254 params,
255 formula: format!(
256 "OLS over last {}: y = intercept + slope*x, x=0..N-1; angle=atan(slope)",
257 params.period
258 ),
259 })
260}
261
262fn linear_regression_validated(
263 series: &[f64],
264 eng: ValidatedLinReg,
265) -> FinanceResult<Vec<Option<LinRegBar>>> {
266 validate_series("series", series)?;
267 let mut st = LinRegState::new(eng.params)?;
268 st.push_bars(series)
269}
270
271fn fit_ols(y: &[f64]) -> LinRegBar {
273 let n = y.len() as f64;
274 let mut sum_x = 0.0;
275 let mut sum_y = 0.0;
276 let mut sum_xx = 0.0;
277 let mut sum_xy = 0.0;
278 for (i, &yi) in y.iter().enumerate() {
279 let x = i as f64;
280 sum_x += x;
281 sum_y += yi;
282 sum_xx += x * x;
283 sum_xy += x * yi;
284 }
285 let denom = n * sum_xx - sum_x * sum_x;
286 let slope = if denom.abs() < 1e-18 {
287 0.0
288 } else {
289 (n * sum_xy - sum_x * sum_y) / denom
290 };
291 let intercept = (sum_y - slope * sum_x) / n;
292
293 let mean_y = sum_y / n;
295 let mut ss_tot = 0.0;
296 let mut ss_res = 0.0;
297 for (i, &yi) in y.iter().enumerate() {
298 let fit = intercept + slope * (i as f64);
299 ss_tot += (yi - mean_y) * (yi - mean_y);
300 ss_res += (yi - fit) * (yi - fit);
301 }
302 let r_squared = if ss_tot <= 1e-18 {
303 1.0 } else {
305 (1.0 - ss_res / ss_tot).clamp(0.0, 1.0)
306 };
307
308 let angle_radians = slope.atan();
309 let angle_degrees = angle_radians.to_degrees();
310 LinRegBar {
311 slope,
312 intercept,
313 r_squared,
314 angle_radians,
315 angle_degrees,
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn perfect_line_slope_one() {
325 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
326 let s = linear_regression(&y, LinRegParams::new(5)).unwrap();
327 let b = s[4].unwrap();
328 assert!((b.slope - 1.0).abs() < 1e-12);
329 assert!((b.r_squared - 1.0).abs() < 1e-12);
330 assert!((b.intercept - 1.0).abs() < 1e-12);
331 }
332
333 #[test]
334 fn state_parity() {
335 let y: Vec<f64> = (0..40).map(|i| 100.0 + i as f64 * 0.25).collect();
336 let batch = linear_regression(&y, LinRegParams::period_20()).unwrap();
337 let st = LinRegState::from_history(LinRegParams::period_20(), &y).unwrap();
338 let b = batch.last().unwrap().unwrap();
339 let s = st.last().unwrap();
340 assert!((b.slope - s.slope).abs() < 1e-12);
341 }
342
343 #[test]
344 fn period_one_err() {
345 assert!(ValidatedLinReg::new(LinRegParams::new(1)).is_err());
346 }
347
348 #[test]
349 fn flat_series_zero_slope() {
350 let y = vec![42.0; 20];
351 let b = linear_regression(&y, LinRegParams::new(10))
352 .unwrap()
353 .last()
354 .unwrap()
355 .unwrap();
356 assert!(b.slope.abs() < 1e-12);
357 assert!((b.r_squared - 1.0).abs() < 1e-12);
358 }
359
360 #[test]
361 fn angle_matches_atan_slope() {
362 let y = [0.0, 1.0, 2.0, 3.0, 4.0];
363 let b = linear_regression(&y, LinRegParams::new(5))
364 .unwrap()
365 .last()
366 .unwrap()
367 .unwrap();
368 assert!((b.angle_radians - b.slope.atan()).abs() < 1e-15);
369 assert!((b.angle_degrees - b.angle_radians.to_degrees()).abs() < 1e-12);
370 }
371
372 #[test]
373 fn works_on_highs_not_only_closes() {
374 let highs = [10.0, 11.0, 12.5, 12.0, 13.0, 14.0, 15.0];
376 let s = linear_regression(&highs, LinRegParams::new(5)).unwrap();
377 assert!(s[6].unwrap().slope > 0.0);
378 }
379
380 #[test]
381 fn empty_err() {
382 assert!(linear_regression(&[], LinRegParams::new(5)).is_err());
383 }
384}