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 fn update(&mut self, candle: Candle) -> Option<f64> {
120 self.has_emitted = true;
121 let range = candle.high - candle.low;
122 if range <= 0.0 {
123 return Some(0.0);
124 }
125 let body = (candle.close - candle.open).abs();
126 if body > self.body_threshold * range {
127 return Some(0.0);
128 }
129 if !self.signed {
130 return Some(1.0);
131 }
132 let body_mid = 0.5 * (candle.open + candle.close);
135 let pos = (body_mid - candle.low) / range;
136 if pos > 2.0 / 3.0 {
137 Some(1.0)
138 } else if pos < 1.0 / 3.0 {
139 Some(-1.0)
140 } else {
141 Some(0.0)
142 }
143 }
144
145 fn reset(&mut self) {
146 self.has_emitted = false;
147 }
148
149 fn warmup_period(&self) -> usize {
150 1
151 }
152
153 fn is_ready(&self) -> bool {
154 self.has_emitted
155 }
156
157 fn name(&self) -> &'static str {
158 "Doji"
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::traits::BatchExt;
166
167 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
168 Candle::new(open, high, low, close, 1.0, ts).unwrap()
169 }
170
171 #[test]
172 fn rejects_invalid_threshold() {
173 assert!(Doji::with_threshold(0.0).is_err());
174 assert!(Doji::with_threshold(-0.1).is_err());
175 assert!(Doji::with_threshold(1.5).is_err());
176 }
177
178 #[test]
179 fn accepts_valid_threshold() {
180 let d = Doji::with_threshold(0.05).unwrap();
181 assert!((d.body_threshold() - 0.05).abs() < 1e-12);
182 }
183
184 #[test]
185 fn accessors_and_metadata() {
186 let d = Doji::default();
187 assert_eq!(d.name(), "Doji");
188 assert_eq!(d.warmup_period(), 1);
189 assert!(!d.is_ready());
190 assert!(!d.is_signed());
191 assert!((d.body_threshold() - 0.1).abs() < 1e-12);
192 }
193
194 #[test]
195 fn obvious_doji_is_one() {
196 let mut d = Doji::new();
197 assert_eq!(d.update(c(10.0, 11.0, 9.0, 10.0, 0)), Some(1.0));
199 assert!(d.is_ready());
200 }
201
202 #[test]
203 fn marubozu_is_not_doji() {
204 let mut d = Doji::new();
206 assert_eq!(d.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
207 }
208
209 #[test]
210 fn zero_range_yields_zero() {
211 let mut d = Doji::new();
212 assert_eq!(d.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
213 }
214
215 #[test]
216 fn batch_equals_streaming() {
217 let candles: Vec<Candle> = (0..40)
218 .map(|i| {
219 let base = 100.0 + i as f64;
220 c(base, base + 2.0, base - 2.0, base + 1.0, i)
221 })
222 .collect();
223 let mut a = Doji::new();
224 let mut b = Doji::new();
225 assert_eq!(
226 a.batch(&candles),
227 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
228 );
229 }
230
231 #[test]
232 fn reset_clears_state() {
233 let mut d = Doji::new();
234 d.update(c(10.0, 11.0, 9.0, 10.0, 0));
235 assert!(d.is_ready());
236 d.reset();
237 assert!(!d.is_ready());
238 }
239
240 #[test]
241 fn signed_accessor_and_builder() {
242 let d = Doji::new().signed();
243 assert!(d.is_signed());
244 let t = Doji::with_threshold(0.05).unwrap().signed();
246 assert!(t.is_signed());
247 assert!((t.body_threshold() - 0.05).abs() < 1e-12);
248 }
249
250 #[test]
251 fn signed_dragonfly_is_plus_one() {
252 let mut d = Doji::new().signed();
254 assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(1.0));
255 }
256
257 #[test]
258 fn signed_gravestone_is_minus_one() {
259 let mut d = Doji::new().signed();
261 assert_eq!(d.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(-1.0));
262 }
263
264 #[test]
265 fn signed_long_legged_is_zero() {
266 let mut d = Doji::new().signed();
268 assert_eq!(d.update(c(10.0, 12.0, 8.0, 10.0, 0)), Some(0.0));
269 }
270
271 #[test]
272 fn signed_non_doji_is_zero() {
273 let mut d = Doji::new().signed();
275 assert_eq!(d.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
276 }
277
278 #[test]
279 fn signed_zero_range_is_zero() {
280 let mut d = Doji::new().signed();
281 assert_eq!(d.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
282 }
283
284 #[test]
285 fn signed_batch_equals_streaming() {
286 let candles: Vec<Candle> = (0..40)
287 .map(|i| {
288 let base = 100.0 + i as f64;
289 match i % 3 {
291 0 => c(base, base + 0.05, base - 4.0, base, i),
292 1 => c(base, base + 4.0, base - 0.05, base, i),
293 _ => c(base, base + 2.0, base - 2.0, base, i),
294 }
295 })
296 .collect();
297 let mut a = Doji::new().signed();
298 let mut b = Doji::new().signed();
299 assert_eq!(
300 a.batch(&candles),
301 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
302 );
303 }
304
305 #[test]
306 fn signed_survives_reset() {
307 let mut d = Doji::new().signed();
308 d.update(c(10.0, 10.05, 6.0, 10.0, 0));
309 assert!(d.is_ready());
310 d.reset();
311 assert!(!d.is_ready());
312 assert!(d.is_signed());
314 assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 1)), Some(1.0));
315 }
316}