wickra_core/indicators/
fry_pan_bottom.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
44pub struct FryPanBottom {
45 period: usize,
46 closes: VecDeque<f64>,
47 last: Option<f64>,
48}
49
50impl FryPanBottom {
51 pub fn new(period: usize) -> Result<Self> {
58 if period < 5 {
59 return Err(Error::InvalidPeriod {
60 message: "frying pan bottom needs period >= 5",
61 });
62 }
63 Ok(Self {
64 period,
65 closes: VecDeque::with_capacity(period),
66 last: None,
67 })
68 }
69
70 pub const fn period(&self) -> usize {
72 self.period
73 }
74
75 pub const fn value(&self) -> Option<f64> {
77 self.last
78 }
79}
80
81impl Indicator for FryPanBottom {
82 type Input = Candle;
83 type Output = f64;
84
85 fn update(&mut self, candle: Candle) -> Option<f64> {
86 if self.closes.len() == self.period {
87 self.closes.pop_front();
88 }
89 self.closes.push_back(candle.close);
90 if self.closes.len() < self.period {
91 return None;
92 }
93 let first = *self.closes.front().expect("non-empty");
94 let last = *self.closes.back().expect("non-empty");
95 let mut min_idx = 0;
97 let mut min_val = f64::INFINITY;
98 for (i, &v) in self.closes.iter().enumerate() {
99 if v < min_val {
100 min_val = v;
101 min_idx = i;
102 }
103 }
104 let lo = self.period / 4;
105 let hi = self.period - self.period / 4;
106 let bowl = min_idx >= lo && min_idx < hi;
107 let recovered = last > first && last > min_val;
108 let v = if bowl && recovered { 1.0 } else { 0.0 };
109 self.last = Some(v);
110 Some(v)
111 }
112
113 fn reset(&mut self) {
114 self.closes.clear();
115 self.last = None;
116 }
117
118 fn warmup_period(&self) -> usize {
119 self.period
120 }
121
122 fn is_ready(&self) -> bool {
123 self.last.is_some()
124 }
125
126 fn name(&self) -> &'static str {
127 "FryPanBottom"
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::traits::BatchExt;
135
136 fn c(close: f64) -> Candle {
137 Candle::new_unchecked(close, close + 0.5, close - 0.5, close, 1_000.0, 0)
138 }
139
140 #[test]
141 fn rejects_small_period() {
142 assert!(matches!(
143 FryPanBottom::new(4),
144 Err(Error::InvalidPeriod { .. })
145 ));
146 assert!(FryPanBottom::new(5).is_ok());
147 }
148
149 #[test]
150 fn accessors_and_metadata() {
151 let f = FryPanBottom::new(9).unwrap();
152 assert_eq!(f.period(), 9);
153 assert_eq!(f.warmup_period(), 9);
154 assert_eq!(f.name(), "FryPanBottom");
155 assert!(!f.is_ready());
156 assert_eq!(f.value(), None);
157 }
158
159 #[test]
160 fn first_emission_at_warmup_period() {
161 let mut f = FryPanBottom::new(5).unwrap();
162 let out = f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0), c(102.0)]);
163 for v in out.iter().take(4) {
164 assert!(v.is_none());
165 }
166 assert!(out[4].is_some());
167 }
168
169 #[test]
170 fn rounded_bottom_then_recovery_signals() {
171 let mut f = FryPanBottom::new(9).unwrap();
172 let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 98.0, 101.0, 103.0, 105.0];
173 let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
174 let last = f.batch(&candles).into_iter().flatten().last().unwrap();
175 assert_eq!(last, 1.0);
176 }
177
178 #[test]
179 fn one_sided_drop_is_zero() {
180 let mut f = FryPanBottom::new(9).unwrap();
182 let candles: Vec<Candle> = (0..9).map(|i| c(100.0 - f64::from(i))).collect();
183 let last = f.batch(&candles).into_iter().flatten().last().unwrap();
184 assert_eq!(last, 0.0);
185 }
186
187 #[test]
188 fn no_recovery_is_zero() {
189 let mut f = FryPanBottom::new(9).unwrap();
191 let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 97.0, 98.0, 99.0, 99.5];
192 let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
193 let last = f.batch(&candles).into_iter().flatten().last().unwrap();
194 assert_eq!(last, 0.0);
195 }
196
197 #[test]
198 fn reset_clears_state() {
199 let mut f = FryPanBottom::new(5).unwrap();
200 f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0)]);
201 assert!(f.is_ready());
202 f.reset();
203 assert!(!f.is_ready());
204 assert_eq!(f.value(), None);
205 assert_eq!(f.update(c(100.0)), None);
206 }
207
208 #[test]
209 fn batch_equals_streaming() {
210 let candles: Vec<Candle> = (0..60)
211 .map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0))
212 .collect();
213 let batch = FryPanBottom::new(9).unwrap().batch(&candles);
214 let mut b = FryPanBottom::new(9).unwrap();
215 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
216 assert_eq!(batch, streamed);
217 }
218}