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