1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct FractalChaosBandsOutput {
12 pub upper: f64,
14 pub lower: f64,
16}
17
18#[derive(Debug, Clone)]
56pub struct FractalChaosBands {
57 k: usize,
58 window: VecDeque<Candle>,
59 last_upper: Option<f64>,
60 last_lower: Option<f64>,
61}
62
63impl FractalChaosBands {
64 pub fn new(k: usize) -> Result<Self> {
72 if k == 0 {
73 return Err(Error::PeriodZero);
74 }
75 if k > crate::error::MAX_PERIOD {
76 return Err(Error::InvalidPeriod {
77 message: crate::error::PERIOD_ABOVE_MAX,
78 });
79 }
80 Ok(Self {
81 k,
82 window: VecDeque::with_capacity(2 * k + 1),
83 last_upper: None,
84 last_lower: None,
85 })
86 }
87
88 pub fn classic() -> Self {
90 Self::new(2).expect("classic Fractal Chaos Bands parameters are valid")
91 }
92
93 pub const fn k(&self) -> usize {
95 self.k
96 }
97}
98
99impl Indicator for FractalChaosBands {
100 type Input = Candle;
101 type Output = FractalChaosBandsOutput;
102
103 #[inline]
104 fn update(&mut self, candle: Candle) -> Option<FractalChaosBandsOutput> {
105 let window_len = 2 * self.k + 1;
106 if self.window.len() == window_len {
107 self.window.pop_front();
108 }
109 self.window.push_back(candle);
110 if self.window.len() < window_len {
111 return None;
112 }
113 let center = &self.window[self.k];
118 let mut is_high = true;
119 let mut is_low = true;
120 for (i, c) in self.window.iter().enumerate() {
121 if i == self.k {
122 continue;
123 }
124 if c.high >= center.high {
125 is_high = false;
126 }
127 if c.low <= center.low {
128 is_low = false;
129 }
130 }
131 if is_high {
132 self.last_upper = Some(center.high);
133 }
134 if is_low {
135 self.last_lower = Some(center.low);
136 }
137 match (self.last_upper, self.last_lower) {
139 (Some(u), Some(l)) => Some(FractalChaosBandsOutput { upper: u, lower: l }),
140 _ => None,
141 }
142 }
143
144 fn reset(&mut self) {
145 self.window.clear();
146 self.last_upper = None;
147 self.last_lower = None;
148 }
149
150 #[inline]
151 fn warmup_period(&self) -> usize {
152 2 * self.k + 1
153 }
154
155 #[inline]
156 fn is_ready(&self) -> bool {
157 self.last_upper.is_some() && self.last_lower.is_some()
158 }
159
160 #[inline]
161 fn name(&self) -> &'static str {
162 "FractalChaosBands"
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::traits::BatchExt;
170 use approx::assert_relative_eq;
171
172 fn c(h: f64, l: f64, cl: f64) -> Candle {
173 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
174 }
175
176 #[test]
177 fn rejects_zero_k() {
178 assert!(matches!(FractalChaosBands::new(0), Err(Error::PeriodZero)));
179 }
180
181 #[test]
182 fn accessors_and_metadata() {
183 let f = FractalChaosBands::classic();
184 assert_eq!(f.k(), 2);
185 assert_eq!(f.warmup_period(), 5);
186 assert_eq!(f.name(), "FractalChaosBands");
187 }
188
189 #[test]
196 fn detects_simple_peak_and_trough() {
197 let candles = vec![
198 c(1.0, 1.0, 1.0),
199 c(2.0, 2.0, 2.0),
200 c(5.0, 3.0, 4.0), c(3.0, 0.5, 1.0), c(2.0, 2.0, 2.0),
203 c(1.0, 1.0, 1.0),
204 c(2.0, 2.0, 2.0),
205 ];
206 let mut f = FractalChaosBands::new(2).unwrap();
207 let out = f.batch(&candles);
208 for v in out.iter().take(5) {
211 assert!(v.is_none());
212 }
213 let v = out[5].unwrap();
217 assert_relative_eq!(v.upper, 5.0, epsilon = 1e-12);
218 assert_relative_eq!(v.lower, 0.5, epsilon = 1e-12);
219 }
220
221 #[test]
224 fn flat_market_never_emits() {
225 let candles: Vec<Candle> = (0..30).map(|_| c(10.0, 10.0, 10.0)).collect();
226 let mut f = FractalChaosBands::new(2).unwrap();
227 for v in f.batch(&candles) {
228 assert!(v.is_none());
229 }
230 }
231
232 #[test]
233 fn batch_equals_streaming() {
234 let candles: Vec<Candle> = (0..40)
235 .map(|i| {
236 let m = 100.0 + (f64::from(i) * 0.5).sin() * 3.0;
237 c(m + 1.0, m - 1.0, m)
238 })
239 .collect();
240 let mut a = FractalChaosBands::new(2).unwrap();
241 let mut b = FractalChaosBands::new(2).unwrap();
242 assert_eq!(
243 a.batch(&candles),
244 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
245 );
246 }
247
248 #[test]
249 fn reset_clears_state() {
250 let candles = vec![
251 c(1.0, 1.0, 1.0),
252 c(2.0, 2.0, 2.0),
253 c(5.0, 3.0, 4.0),
254 c(3.0, 0.5, 1.0),
255 c(2.0, 2.0, 2.0),
256 c(1.0, 1.0, 1.0),
257 c(2.0, 2.0, 2.0),
258 ];
259 let mut f = FractalChaosBands::new(2).unwrap();
260 f.batch(&candles);
261 assert!(f.is_ready());
262 f.reset();
263 assert!(!f.is_ready());
264 assert_eq!(f.update(candles[0]), None);
265 }
266
267 #[test]
268 fn upper_above_lower_when_both_set() {
269 let candles: Vec<Candle> = (0..60)
270 .map(|i| {
271 let m = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
272 c(m + 1.0, m - 1.0, m)
273 })
274 .collect();
275 let mut f = FractalChaosBands::new(2).unwrap();
276 for o in f.batch(&candles).into_iter().flatten() {
277 assert!(o.upper >= o.lower);
278 }
279 }
280
281 #[test]
282 fn rejects_k_above_maximum() {
283 assert!(matches!(
284 FractalChaosBands::new(crate::error::MAX_PERIOD + 1),
285 Err(Error::InvalidPeriod { .. })
286 ));
287 }
288}