1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
45pub struct DojiStar {
46 prev: Option<Candle>,
47 has_emitted: bool,
48}
49
50impl DojiStar {
51 pub const fn new() -> Self {
53 Self {
54 prev: None,
55 has_emitted: false,
56 }
57 }
58}
59
60impl Indicator for DojiStar {
61 type Input = Candle;
62 type Output = f64;
63
64 #[inline]
65 fn update(&mut self, candle: Candle) -> Option<f64> {
66 let prev = self.prev;
67 self.prev = Some(candle);
68 let bar1 = prev?;
69 self.has_emitted = true;
70 let range1 = bar1.high - bar1.low;
71 let range2 = candle.high - candle.low;
72 if range1 <= 0.0 || range2 <= 0.0 {
73 return Some(0.0);
74 }
75 let body1 = bar1.close - bar1.open;
76 if body1.abs() < 0.5 * range1 {
77 return Some(0.0);
78 }
79 if (candle.close - candle.open).abs() > 0.1 * range2 {
80 return Some(0.0);
81 }
82 let doji_top = candle.open.max(candle.close);
83 let doji_bottom = candle.open.min(candle.close);
84 if body1 < 0.0 && doji_top < bar1.close {
86 return Some(1.0);
87 }
88 if body1 > 0.0 && doji_bottom > bar1.close {
90 return Some(-1.0);
91 }
92 Some(0.0)
93 }
94
95 fn reset(&mut self) {
96 self.prev = None;
97 self.has_emitted = false;
98 }
99
100 #[inline]
101 fn warmup_period(&self) -> usize {
102 2
103 }
104
105 #[inline]
106 fn is_ready(&self) -> bool {
107 self.has_emitted
108 }
109
110 #[inline]
111 fn name(&self) -> &'static str {
112 "DojiStar"
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::traits::BatchExt;
120
121 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
122 Candle::new(open, high, low, close, 1.0, ts).unwrap()
123 }
124
125 #[test]
126 fn accessors_and_metadata() {
127 let t = DojiStar::new();
128 assert_eq!(t.name(), "DojiStar");
129 assert_eq!(t.warmup_period(), 2);
130 assert!(!t.is_ready());
131 }
132
133 #[test]
134 fn bullish_doji_star_is_plus_one() {
135 let mut t = DojiStar::new();
136 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), None);
137 assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(1.0));
138 }
139
140 #[test]
141 fn bearish_doji_star_is_minus_one() {
142 let mut t = DojiStar::new();
143 assert_eq!(t.update(c(15.0, 20.2, 14.8, 20.0, 0)), None);
144 assert_eq!(t.update(c(22.0, 22.1, 21.9, 22.0, 1)), Some(-1.0));
145 }
146
147 #[test]
148 fn second_bar_not_doji_yields_zero() {
149 let mut t = DojiStar::new();
150 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
151 assert_eq!(t.update(c(13.0, 13.2, 11.0, 11.5, 1)), Some(0.0));
153 }
154
155 #[test]
156 fn no_gap_yields_zero() {
157 let mut t = DojiStar::new();
158 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
159 assert_eq!(t.update(c(16.0, 16.1, 15.9, 16.0, 1)), Some(0.0));
161 }
162
163 #[test]
164 fn short_first_body_yields_zero() {
165 let mut t = DojiStar::new();
166 t.update(c(20.0, 24.0, 16.0, 19.5, 0));
168 assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(0.0));
169 }
170
171 #[test]
172 fn first_bar_returns_zero() {
173 let mut t = DojiStar::new();
174 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), None);
175 }
176
177 #[test]
178 fn batch_equals_streaming() {
179 let candles: Vec<Candle> = (0..40)
180 .map(|i| {
181 let base = 100.0 + i as f64;
182 if i % 2 == 0 {
183 c(base + 5.0, base + 5.2, base - 0.2, base, i)
184 } else {
185 c(base - 3.0, base - 2.9, base - 3.1, base - 3.0, i)
186 }
187 })
188 .collect();
189 let mut a = DojiStar::new();
190 let mut b = DojiStar::new();
191 assert_eq!(
192 a.batch(&candles),
193 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
194 );
195 }
196
197 #[test]
198 fn reset_clears_state() {
199 let mut t = DojiStar::new();
200 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
201 t.update(c(13.0, 13.1, 12.9, 13.0, 1));
202 assert!(t.is_ready());
203 t.reset();
204 assert!(!t.is_ready());
205 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), None);
206 }
207
208 #[test]
209 fn zero_range_yields_zero() {
210 let mut t = DojiStar::new();
211 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
212 assert_eq!(t.update(c(13.0, 13.0, 13.0, 13.0, 1)), Some(0.0));
214 }
215}