1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
54pub struct Doji {
55 body_threshold: f64,
56 signed: bool,
57 has_emitted: bool,
58}
59
60impl Default for Doji {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl Doji {
67 pub const fn new() -> Self {
69 Self {
70 body_threshold: 0.1,
71 signed: false,
72 has_emitted: false,
73 }
74 }
75
76 pub fn with_threshold(body_threshold: f64) -> Result<Self> {
80 if !(body_threshold > 0.0 && body_threshold <= 1.0) {
81 return Err(Error::InvalidPeriod {
82 message: "doji body threshold must lie in (0, 1]",
83 });
84 }
85 Ok(Self {
86 body_threshold,
87 signed: false,
88 has_emitted: false,
89 })
90 }
91
92 #[must_use]
99 pub fn signed(mut self) -> Self {
100 self.signed = true;
101 self
102 }
103
104 pub fn body_threshold(&self) -> f64 {
106 self.body_threshold
107 }
108
109 pub fn is_signed(&self) -> bool {
111 self.signed
112 }
113}
114
115impl Indicator for Doji {
116 type Input = Candle;
117 type Output = f64;
118
119 #[inline]
120 fn update(&mut self, candle: Candle) -> Option<f64> {
121 self.has_emitted = true;
122 let range = candle.high - candle.low;
123 if range <= 0.0 {
124 return Some(0.0);
125 }
126 let body = (candle.close - candle.open).abs();
127 if body > self.body_threshold * range {
128 return Some(0.0);
129 }
130 if !self.signed {
131 return Some(1.0);
132 }
133 let body_mid = f64::midpoint(candle.open, candle.close);
136 let pos = (body_mid - candle.low) / range;
137 if pos > 2.0 / 3.0 {
138 Some(1.0)
139 } else if pos < 1.0 / 3.0 {
140 Some(-1.0)
141 } else {
142 Some(0.0)
143 }
144 }
145
146 fn reset(&mut self) {
147 self.has_emitted = false;
148 }
149
150 #[inline]
151 fn warmup_period(&self) -> usize {
152 1
153 }
154
155 #[inline]
156 fn is_ready(&self) -> bool {
157 self.has_emitted
158 }
159
160 #[inline]
161 fn name(&self) -> &'static str {
162 "Doji"
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::traits::BatchExt;
170
171 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
172 Candle::new(open, high, low, close, 1.0, ts).unwrap()
173 }
174
175 #[test]
176 fn rejects_invalid_threshold() {
177 assert!(Doji::with_threshold(0.0).is_err());
178 assert!(Doji::with_threshold(-0.1).is_err());
179 assert!(Doji::with_threshold(1.5).is_err());
180 }
181
182 #[test]
183 fn accepts_valid_threshold() {
184 let d = Doji::with_threshold(0.05).unwrap();
185 assert!((d.body_threshold() - 0.05).abs() < 1e-12);
186 }
187
188 #[test]
189 fn accessors_and_metadata() {
190 let d = Doji::default();
191 assert_eq!(d.name(), "Doji");
192 assert_eq!(d.warmup_period(), 1);
193 assert!(!d.is_ready());
194 assert!(!d.is_signed());
195 assert!((d.body_threshold() - 0.1).abs() < 1e-12);
196 }
197
198 #[test]
199 fn obvious_doji_is_one() {
200 let mut d = Doji::new();
201 assert_eq!(d.update(c(10.0, 11.0, 9.0, 10.0, 0)), Some(1.0));
203 assert!(d.is_ready());
204 }
205
206 #[test]
207 fn marubozu_is_not_doji() {
208 let mut d = Doji::new();
210 assert_eq!(d.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
211 }
212
213 #[test]
214 fn zero_range_yields_zero() {
215 let mut d = Doji::new();
216 assert_eq!(d.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
217 }
218
219 #[test]
220 fn batch_equals_streaming() {
221 let candles: Vec<Candle> = (0..40)
222 .map(|i| {
223 let base = 100.0 + i as f64;
224 c(base, base + 2.0, base - 2.0, base + 1.0, i)
225 })
226 .collect();
227 let mut a = Doji::new();
228 let mut b = Doji::new();
229 assert_eq!(
230 a.batch(&candles),
231 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
232 );
233 }
234
235 #[test]
236 fn reset_clears_state() {
237 let mut d = Doji::new();
238 d.update(c(10.0, 11.0, 9.0, 10.0, 0));
239 assert!(d.is_ready());
240 d.reset();
241 assert!(!d.is_ready());
242 }
243
244 #[test]
245 fn signed_accessor_and_builder() {
246 let d = Doji::new().signed();
247 assert!(d.is_signed());
248 let t = Doji::with_threshold(0.05).unwrap().signed();
250 assert!(t.is_signed());
251 assert!((t.body_threshold() - 0.05).abs() < 1e-12);
252 }
253
254 #[test]
255 fn signed_dragonfly_is_plus_one() {
256 let mut d = Doji::new().signed();
258 assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(1.0));
259 }
260
261 #[test]
262 fn signed_gravestone_is_minus_one() {
263 let mut d = Doji::new().signed();
265 assert_eq!(d.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(-1.0));
266 }
267
268 #[test]
269 fn signed_long_legged_is_zero() {
270 let mut d = Doji::new().signed();
272 assert_eq!(d.update(c(10.0, 12.0, 8.0, 10.0, 0)), Some(0.0));
273 }
274
275 #[test]
276 fn signed_non_doji_is_zero() {
277 let mut d = Doji::new().signed();
279 assert_eq!(d.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
280 }
281
282 #[test]
283 fn signed_zero_range_is_zero() {
284 let mut d = Doji::new().signed();
285 assert_eq!(d.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
286 }
287
288 #[test]
289 fn signed_batch_equals_streaming() {
290 let candles: Vec<Candle> = (0..40)
291 .map(|i| {
292 let base = 100.0 + i as f64;
293 match i % 3 {
295 0 => c(base, base + 0.05, base - 4.0, base, i),
296 1 => c(base, base + 4.0, base - 0.05, base, i),
297 _ => c(base, base + 2.0, base - 2.0, base, i),
298 }
299 })
300 .collect();
301 let mut a = Doji::new().signed();
302 let mut b = Doji::new().signed();
303 assert_eq!(
304 a.batch(&candles),
305 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
306 );
307 }
308
309 #[test]
310 fn signed_survives_reset() {
311 let mut d = Doji::new().signed();
312 d.update(c(10.0, 10.05, 6.0, 10.0, 0));
313 assert!(d.is_ready());
314 d.reset();
315 assert!(!d.is_ready());
316 assert!(d.is_signed());
318 assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 1)), Some(1.0));
319 }
320}