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)]
122pub struct LinRegState {
123 params: LinRegParams,
124 ring: RingF64,
125 sums: Option<LinRegSums>,
127 last: Option<LinRegBar>,
128}
129
130#[derive(Clone, Copy, Debug)]
131struct LinRegSums {
132 sum_y: f64,
133 sum_xy: f64,
134 sum_y2: f64,
135}
136
137impl LinRegState {
138 pub fn new(params: LinRegParams) -> FinanceResult<Self> {
139 let _ = ValidatedLinReg::new(params)?;
140 Ok(Self {
141 params,
142 ring: RingF64::with_capacity(params.period),
143 sums: None,
144 last: None,
145 })
146 }
147
148 pub fn from_history(params: LinRegParams, series: &[f64]) -> FinanceResult<Self> {
149 let mut s = Self::new(params)?;
150 let _ = s.push_bars(series)?;
151 Ok(s)
152 }
153
154 pub fn params(&self) -> LinRegParams {
155 self.params
156 }
157
158 pub fn reset(&mut self) {
159 self.ring.clear();
160 self.sums = None;
161 self.last = None;
162 }
163
164 pub fn push(&mut self, y: f64) -> FinanceResult<Option<LinRegBar>> {
165 require_finite("series", y)?;
166 let n = self.params.period;
167 let nf = n as f64;
168
169 if let Some(mut s) = self.sums {
170 let y_old = self.ring.oldest().unwrap();
171 let sum_y_old = s.sum_y;
172 s.sum_xy = s.sum_xy - sum_y_old + y_old + (nf - 1.0) * y;
175 s.sum_y = sum_y_old - y_old + y;
176 s.sum_y2 = s.sum_y2 - y_old * y_old + y * y;
177 let _ = self.ring.push(y);
178 self.sums = Some(s);
179 let bar = fit_ols_from_sums(n, s.sum_y, s.sum_xy, s.sum_y2);
180 self.last = Some(bar);
181 Ok(Some(bar))
182 } else {
183 let _ = self.ring.push(y);
184 if !self.ring.is_full() {
185 self.last = None;
186 return Ok(None);
187 }
188 let mut ordered = Vec::with_capacity(n);
189 self.ring.copy_ordered(&mut ordered);
190 let mut sum_y = 0.0;
191 let mut sum_xy = 0.0;
192 let mut sum_y2 = 0.0;
193 for (i, &yi) in ordered.iter().enumerate() {
194 let x = i as f64;
195 sum_y += yi;
196 sum_xy += x * yi;
197 sum_y2 += yi * yi;
198 }
199 self.sums = Some(LinRegSums {
200 sum_y,
201 sum_xy,
202 sum_y2,
203 });
204 let bar = fit_ols_from_sums(n, sum_y, sum_xy, sum_y2);
205 self.last = Some(bar);
206 Ok(Some(bar))
207 }
208 }
209
210 pub fn push_bars(&mut self, series: &[f64]) -> FinanceResult<Vec<Option<LinRegBar>>> {
211 let mut out = Vec::with_capacity(series.len());
212 for &y in series {
213 out.push(self.push(y)?);
214 }
215 Ok(out)
216 }
217
218 pub fn last(&self) -> Option<LinRegBar> {
219 self.last
220 }
221}
222
223#[derive(Clone, Debug)]
224pub struct LinRegSolution {
225 series: Vec<Option<LinRegBar>>,
226 y: Vec<f64>,
227 params: LinRegParams,
228 formula: String,
229}
230
231impl LinRegSolution {
232 pub fn series(&self) -> &[Option<LinRegBar>] {
233 &self.series
234 }
235 pub fn formula(&self) -> &str {
236 &self.formula
237 }
238 pub fn params(&self) -> LinRegParams {
239 self.params
240 }
241
242 pub fn print_table(&self) {
243 self.print_table_locale_opt(None, None);
244 }
245
246 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
247 self.print_table_locale_opt(Some(locale), Some(precision));
248 }
249
250 fn print_table_locale_opt(
251 &self,
252 locale: Option<&num_format::Locale>,
253 precision: Option<usize>,
254 ) {
255 let columns = columns_with_strings(&[
256 ("period", "i", true),
257 ("y", "f", true),
258 ("slope", "f", true),
259 ("angle_deg", "f", true),
260 ("r2", "f", true),
261 ]);
262 let data = self
263 .y
264 .iter()
265 .enumerate()
266 .map(|(i, y)| {
267 let (slope, ang, r2) = match self.series[i] {
268 Some(b) => (
269 b.slope.to_string(),
270 b.angle_degrees.to_string(),
271 b.r_squared.to_string(),
272 ),
273 None => ("n/a".to_string(), "n/a".to_string(), "n/a".to_string()),
274 };
275 vec![i.to_string(), y.to_string(), slope, ang, r2]
276 })
277 .collect();
278 print_table_locale_opt(&columns, data, locale, precision);
279 }
280}
281
282pub fn linear_regression(
284 series: &[f64],
285 params: LinRegParams,
286) -> FinanceResult<Vec<Option<LinRegBar>>> {
287 ValidatedLinReg::new(params)?.compute(series)
288}
289
290pub fn linear_regression_solution(
291 series: &[f64],
292 params: LinRegParams,
293) -> FinanceResult<LinRegSolution> {
294 let s = linear_regression(series, params)?;
295 Ok(LinRegSolution {
296 series: s,
297 y: series.to_vec(),
298 params,
299 formula: format!(
300 "OLS over last {}: y = intercept + slope*x, x=0..N-1; angle=atan(slope)",
301 params.period
302 ),
303 })
304}
305
306fn linear_regression_validated(
307 series: &[f64],
308 eng: ValidatedLinReg,
309) -> FinanceResult<Vec<Option<LinRegBar>>> {
310 validate_series("series", series)?;
311 let mut st = LinRegState::new(eng.params)?;
312 st.push_bars(series)
313}
314
315fn fit_ols_from_sums(n: usize, sum_y: f64, sum_xy: f64, sum_y2: f64) -> LinRegBar {
317 let nf = n as f64;
318 let sum_x = (nf - 1.0) * nf / 2.0;
321 let sum_xx = (nf - 1.0) * nf * (2.0 * nf - 1.0) / 6.0;
322 let denom = nf * sum_xx - sum_x * sum_x;
323 let slope = if denom.abs() < 1e-18 {
324 0.0
325 } else {
326 (nf * sum_xy - sum_x * sum_y) / denom
327 };
328 let intercept = (sum_y - slope * sum_x) / nf;
329
330 let mean_y = sum_y / nf;
331 let ss_tot = sum_y2 - nf * mean_y * mean_y;
332 let ss_res = sum_y2
334 + nf * intercept * intercept
335 + slope * slope * sum_xx
336 + 2.0 * intercept * slope * sum_x
337 - 2.0 * intercept * sum_y
338 - 2.0 * slope * sum_xy;
339 let r_squared = if ss_tot <= 1e-18 {
340 1.0
341 } else {
342 (1.0 - ss_res / ss_tot).clamp(0.0, 1.0)
343 };
344
345 let angle_radians = slope.atan();
346 LinRegBar {
347 slope,
348 intercept,
349 r_squared,
350 angle_radians,
351 angle_degrees: angle_radians.to_degrees(),
352 }
353}
354
355#[cfg(test)]
357fn fit_ols(y: &[f64]) -> LinRegBar {
358 let mut sum_y = 0.0;
359 let mut sum_xy = 0.0;
360 let mut sum_y2 = 0.0;
361 for (i, &yi) in y.iter().enumerate() {
362 sum_y += yi;
363 sum_xy += (i as f64) * yi;
364 sum_y2 += yi * yi;
365 }
366 fit_ols_from_sums(y.len(), sum_y, sum_xy, sum_y2)
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn perfect_line_slope_one() {
375 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
376 let s = linear_regression(&y, LinRegParams::new(5)).unwrap();
377 let b = s[4].unwrap();
378 assert!((b.slope - 1.0).abs() < 1e-12);
379 assert!((b.r_squared - 1.0).abs() < 1e-12);
380 assert!((b.intercept - 1.0).abs() < 1e-12);
381 }
382
383 #[test]
384 fn state_parity() {
385 let y: Vec<f64> = (0..40).map(|i| 100.0 + i as f64 * 0.25).collect();
386 let batch = linear_regression(&y, LinRegParams::period_20()).unwrap();
387 let st = LinRegState::from_history(LinRegParams::period_20(), &y).unwrap();
388 let b = batch.last().unwrap().unwrap();
389 let s = st.last().unwrap();
390 assert!((b.slope - s.slope).abs() < 1e-12);
391 }
392
393 #[test]
394 fn period_one_err() {
395 assert!(ValidatedLinReg::new(LinRegParams::new(1)).is_err());
396 }
397
398 #[test]
399 fn flat_series_zero_slope() {
400 let y = vec![42.0; 20];
401 let b = linear_regression(&y, LinRegParams::new(10))
402 .unwrap()
403 .last()
404 .unwrap()
405 .unwrap();
406 assert!(b.slope.abs() < 1e-12);
407 assert!((b.r_squared - 1.0).abs() < 1e-12);
408 }
409
410 #[test]
411 fn angle_matches_atan_slope() {
412 let y = [0.0, 1.0, 2.0, 3.0, 4.0];
413 let b = linear_regression(&y, LinRegParams::new(5))
414 .unwrap()
415 .last()
416 .unwrap()
417 .unwrap();
418 assert!((b.angle_radians - b.slope.atan()).abs() < 1e-15);
419 assert!((b.angle_degrees - b.angle_radians.to_degrees()).abs() < 1e-12);
420 }
421
422 #[test]
423 fn works_on_highs_not_only_closes() {
424 let highs = [10.0, 11.0, 12.5, 12.0, 13.0, 14.0, 15.0];
426 let s = linear_regression(&highs, LinRegParams::new(5)).unwrap();
427 assert!(s[6].unwrap().slope > 0.0);
428 }
429
430 #[test]
431 fn empty_err() {
432 assert!(linear_regression(&[], LinRegParams::new(5)).is_err());
433 }
434}