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