wickra_core/indicators/
cfo.rs1use crate::error::{Error, Result};
4use crate::indicators::linreg::LinearRegression;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
32pub struct Cfo {
33 period: usize,
34 linreg: LinearRegression,
35 current: Option<f64>,
36}
37
38impl Cfo {
39 pub fn new(period: usize) -> Result<Self> {
42 if period == 0 {
43 return Err(Error::PeriodZero);
44 }
45 Ok(Self {
46 period,
47 linreg: LinearRegression::new(period)?,
48 current: None,
49 })
50 }
51
52 pub const fn period(&self) -> usize {
54 self.period
55 }
56}
57
58impl Indicator for Cfo {
59 type Input = f64;
60 type Output = f64;
61
62 fn update(&mut self, input: f64) -> Option<f64> {
63 if !input.is_finite() {
64 return None;
65 }
66 let forecast = self.linreg.update(input)?;
67 if input == 0.0 {
70 return self.current;
71 }
72 let value = 100.0 * (input - forecast) / input;
73 self.current = Some(value);
74 Some(value)
75 }
76
77 fn reset(&mut self) {
78 self.linreg.reset();
79 self.current = None;
80 }
81
82 fn warmup_period(&self) -> usize {
83 self.period
84 }
85
86 fn is_ready(&self) -> bool {
87 self.current.is_some()
88 }
89
90 fn name(&self) -> &'static str {
91 "CFO"
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::traits::BatchExt;
99 use approx::assert_relative_eq;
100
101 #[test]
102 fn rejects_zero_period() {
103 assert!(matches!(Cfo::new(0), Err(Error::PeriodZero)));
104 }
105
106 #[test]
107 fn accessors_and_metadata() {
108 let cfo = Cfo::new(14).unwrap();
109 assert_eq!(cfo.period(), 14);
110 assert_eq!(cfo.warmup_period(), 14);
111 assert_eq!(cfo.name(), "CFO");
112 }
113
114 #[test]
115 fn constant_series_yields_zero() {
116 let mut cfo = Cfo::new(5).unwrap();
119 let out = cfo.batch(&[42.0_f64; 30]);
120 for v in out.iter().skip(4).flatten() {
121 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
122 }
123 }
124
125 #[test]
126 fn perfect_linear_series_yields_zero() {
127 let mut cfo = Cfo::new(5).unwrap();
130 let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 2.0).collect();
131 let out = cfo.batch(&prices);
132 for v in out.iter().skip(4).flatten() {
133 assert_relative_eq!(*v, 0.0, epsilon = 1e-9);
134 }
135 }
136
137 #[test]
138 fn warmup_emits_first_value_at_period() {
139 let mut cfo = Cfo::new(3).unwrap();
140 for i in 1..=2 {
141 assert_eq!(cfo.update(f64::from(i)), None);
142 }
143 assert!(cfo.update(3.0).is_some());
144 }
145
146 #[test]
147 fn batch_equals_streaming() {
148 let prices: Vec<f64> = (1..=80)
149 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
150 .collect();
151 let mut a = Cfo::new(14).unwrap();
152 let mut b = Cfo::new(14).unwrap();
153 assert_eq!(
154 a.batch(&prices),
155 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
156 );
157 }
158
159 #[test]
160 fn reset_clears_state() {
161 let mut cfo = Cfo::new(5).unwrap();
162 cfo.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
163 assert!(cfo.is_ready());
164 cfo.reset();
165 assert!(!cfo.is_ready());
166 assert_eq!(cfo.update(1.0), None);
167 }
168
169 #[test]
170 fn zero_close_holds_value() {
171 let mut cfo = Cfo::new(3).unwrap();
172 cfo.batch(&[1.0_f64, 2.0, 3.0]);
173 let before = cfo.current;
174 assert_eq!(cfo.update(0.0), before);
175 }
176}