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 let forecast = self.linreg.update(input)?;
64 if input == 0.0 {
67 return self.current;
68 }
69 let value = 100.0 * (input - forecast) / input;
70 self.current = Some(value);
71 Some(value)
72 }
73
74 fn reset(&mut self) {
75 self.linreg.reset();
76 self.current = None;
77 }
78
79 fn warmup_period(&self) -> usize {
80 self.period
81 }
82
83 fn is_ready(&self) -> bool {
84 self.current.is_some()
85 }
86
87 fn name(&self) -> &'static str {
88 "CFO"
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::traits::BatchExt;
96 use approx::assert_relative_eq;
97
98 #[test]
99 fn rejects_zero_period() {
100 assert!(matches!(Cfo::new(0), Err(Error::PeriodZero)));
101 }
102
103 #[test]
104 fn accessors_and_metadata() {
105 let cfo = Cfo::new(14).unwrap();
106 assert_eq!(cfo.period(), 14);
107 assert_eq!(cfo.warmup_period(), 14);
108 assert_eq!(cfo.name(), "CFO");
109 }
110
111 #[test]
112 fn constant_series_yields_zero() {
113 let mut cfo = Cfo::new(5).unwrap();
116 let out = cfo.batch(&[42.0_f64; 30]);
117 for v in out.iter().skip(4).flatten() {
118 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
119 }
120 }
121
122 #[test]
123 fn perfect_linear_series_yields_zero() {
124 let mut cfo = Cfo::new(5).unwrap();
127 let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 2.0).collect();
128 let out = cfo.batch(&prices);
129 for v in out.iter().skip(4).flatten() {
130 assert_relative_eq!(*v, 0.0, epsilon = 1e-9);
131 }
132 }
133
134 #[test]
135 fn warmup_emits_first_value_at_period() {
136 let mut cfo = Cfo::new(3).unwrap();
137 for i in 1..=2 {
138 assert_eq!(cfo.update(f64::from(i)), None);
139 }
140 assert!(cfo.update(3.0).is_some());
141 }
142
143 #[test]
144 fn batch_equals_streaming() {
145 let prices: Vec<f64> = (1..=80)
146 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
147 .collect();
148 let mut a = Cfo::new(14).unwrap();
149 let mut b = Cfo::new(14).unwrap();
150 assert_eq!(
151 a.batch(&prices),
152 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
153 );
154 }
155
156 #[test]
157 fn reset_clears_state() {
158 let mut cfo = Cfo::new(5).unwrap();
159 cfo.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
160 assert!(cfo.is_ready());
161 cfo.reset();
162 assert!(!cfo.is_ready());
163 assert_eq!(cfo.update(1.0), None);
164 }
165
166 #[test]
167 fn zero_close_holds_value() {
168 let mut cfo = Cfo::new(3).unwrap();
169 cfo.batch(&[1.0_f64, 2.0, 3.0]);
170 let before = cfo.current;
171 assert_eq!(cfo.update(0.0), before);
172 }
173}