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