wickra_core/indicators/
tsf_oscillator.rs1use crate::error::{Error, Result};
4use crate::indicators::tsf::Tsf;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
40pub struct TsfOscillator {
41 period: usize,
42 tsf: Tsf,
43 current: Option<f64>,
44}
45
46impl TsfOscillator {
47 pub fn new(period: usize) -> Result<Self> {
53 if period < 2 {
54 return Err(Error::InvalidPeriod {
55 message: "TSF oscillator needs period >= 2",
56 });
57 }
58 Ok(Self {
59 period,
60 tsf: Tsf::new(period)?,
61 current: None,
62 })
63 }
64
65 pub const fn period(&self) -> usize {
67 self.period
68 }
69}
70
71impl Indicator for TsfOscillator {
72 type Input = f64;
73 type Output = f64;
74
75 fn update(&mut self, input: f64) -> Option<f64> {
76 if !input.is_finite() {
77 return None;
78 }
79 let forecast = self.tsf.update(input)?;
80 if input == 0.0 {
83 return self.current;
84 }
85 let value = 100.0 * (input - forecast) / input;
86 self.current = Some(value);
87 Some(value)
88 }
89
90 fn reset(&mut self) {
91 self.tsf.reset();
92 self.current = None;
93 }
94
95 fn warmup_period(&self) -> usize {
96 self.period
97 }
98
99 fn is_ready(&self) -> bool {
100 self.current.is_some()
101 }
102
103 fn name(&self) -> &'static str {
104 "TsfOscillator"
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::traits::BatchExt;
112 use approx::assert_relative_eq;
113
114 #[test]
115 fn rejects_short_period() {
116 assert!(matches!(
117 TsfOscillator::new(1),
118 Err(Error::InvalidPeriod { .. })
119 ));
120 assert!(matches!(
121 TsfOscillator::new(0),
122 Err(Error::InvalidPeriod { .. })
123 ));
124 }
125
126 #[test]
127 fn accessors_and_metadata() {
128 let osc = TsfOscillator::new(14).unwrap();
129 assert_eq!(osc.period(), 14);
130 assert_eq!(osc.warmup_period(), 14);
131 assert_eq!(osc.name(), "TsfOscillator");
132 assert!(!osc.is_ready());
133 }
134
135 #[test]
136 fn reference_value() {
137 let mut osc = TsfOscillator::new(3).unwrap();
140 let out = osc.batch(&[1.0_f64, 2.0, 9.0]);
141 assert!(out[0].is_none());
142 assert!(out[1].is_none());
143 assert_relative_eq!(out[2].unwrap(), -100.0 / 3.0, epsilon = 1e-9);
144 assert!(osc.is_ready());
145 }
146
147 #[test]
148 fn constant_series_yields_zero() {
149 let mut osc = TsfOscillator::new(5).unwrap();
152 let out = osc.batch(&[42.0_f64; 30]);
153 for v in out.iter().skip(4).flatten() {
154 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
155 }
156 }
157
158 #[test]
159 fn linear_uptrend_reads_negative() {
160 let mut osc = TsfOscillator::new(5).unwrap();
164 let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 2.0).collect();
165 let out = osc.batch(&prices);
166 for v in out.iter().skip(4).flatten() {
167 assert!(*v < 0.0, "uptrend forecast overshoots close, got {v}");
168 }
169 }
170
171 #[test]
172 fn warmup_emits_first_value_at_period() {
173 let mut osc = TsfOscillator::new(3).unwrap();
174 assert_eq!(osc.update(1.0), None);
175 assert_eq!(osc.update(2.0), None);
176 assert!(osc.update(3.0).is_some());
177 }
178
179 #[test]
180 fn batch_equals_streaming() {
181 let prices: Vec<f64> = (1..=80)
182 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
183 .collect();
184 let mut a = TsfOscillator::new(14).unwrap();
185 let mut b = TsfOscillator::new(14).unwrap();
186 assert_eq!(
187 a.batch(&prices),
188 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
189 );
190 }
191
192 #[test]
193 fn reset_clears_state() {
194 let mut osc = TsfOscillator::new(5).unwrap();
195 osc.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
196 assert!(osc.is_ready());
197 osc.reset();
198 assert!(!osc.is_ready());
199 assert_eq!(osc.update(1.0), None);
200 }
201
202 #[test]
203 fn zero_close_holds_value() {
204 let mut osc = TsfOscillator::new(3).unwrap();
205 osc.batch(&[1.0_f64, 2.0, 3.0]);
206 let before = osc.current;
207 assert_eq!(osc.update(0.0), before);
208 }
209}