use crate::error::Result;
use crate::indicators::linreg_slope::LinRegSlope;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct LinRegAngle {
slope: LinRegSlope,
}
impl LinRegAngle {
pub fn new(period: usize) -> Result<Self> {
Ok(Self {
slope: LinRegSlope::new(period)?,
})
}
pub const fn period(&self) -> usize {
self.slope.period()
}
}
impl Indicator for LinRegAngle {
type Input = f64;
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
self.slope.update(value).map(|s| s.atan().to_degrees())
}
fn reset(&mut self) {
self.slope.reset();
}
fn warmup_period(&self) -> usize {
self.slope.warmup_period()
}
fn is_ready(&self) -> bool {
self.slope.is_ready()
}
fn name(&self) -> &'static str {
"LinRegAngle"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn unit_slope_is_forty_five_degrees() {
let mut angle = LinRegAngle::new(5).unwrap();
let out = angle.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
for (i, v) in out.iter().enumerate().take(4) {
assert!(v.is_none(), "index {i} must be None during warmup");
}
assert_relative_eq!(out[4].unwrap(), 45.0, epsilon = 1e-9);
assert_relative_eq!(out[5].unwrap(), 45.0, epsilon = 1e-9);
}
#[test]
fn reference_value_steep_slope() {
let mut angle = LinRegAngle::new(3).unwrap();
let out = angle.batch(&[1.0, 2.0, 9.0]);
assert_relative_eq!(out[2].unwrap(), 4.0_f64.atan().to_degrees(), epsilon = 1e-9);
}
#[test]
fn constant_series_has_zero_angle() {
let mut angle = LinRegAngle::new(8).unwrap();
for v in angle.batch(&[42.0; 20]).into_iter().flatten() {
assert_relative_eq!(v, 0.0, epsilon = 1e-9);
}
}
#[test]
fn falling_series_has_negative_angle() {
let prices: Vec<f64> = (0..30).map(|i| 100.0 - f64::from(i)).collect();
let mut angle = LinRegAngle::new(10).unwrap();
for v in angle.batch(&prices).into_iter().flatten() {
assert!(v < 0.0, "a falling series must have a negative angle");
}
}
#[test]
fn stays_within_ninety_degrees() {
let prices: Vec<f64> = (0..60)
.map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 1000.0)
.collect();
let mut angle = LinRegAngle::new(14).unwrap();
for v in angle.batch(&prices).into_iter().flatten() {
assert!(v > -90.0 && v < 90.0, "angle {v} outside (-90, 90)");
}
}
#[test]
fn rejects_period_below_two() {
assert!(LinRegAngle::new(0).is_err());
assert!(LinRegAngle::new(1).is_err());
assert!(LinRegAngle::new(2).is_ok());
}
#[test]
fn accessors_and_metadata() {
let a = LinRegAngle::new(14).unwrap();
assert_eq!(a.period(), 14);
assert_eq!(a.warmup_period(), 14);
assert_eq!(a.name(), "LinRegAngle");
}
#[test]
fn reset_clears_state() {
let mut angle = LinRegAngle::new(5).unwrap();
angle.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
assert!(angle.is_ready());
angle.reset();
assert!(!angle.is_ready());
assert_eq!(angle.update(1.0), None);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (0..60)
.map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 10.0)
.collect();
let mut a = LinRegAngle::new(14).unwrap();
let mut b = LinRegAngle::new(14).unwrap();
assert_eq!(
a.batch(&prices),
prices.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}