1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
48pub struct EveningDojiStar {
49 penetration: f64,
50 prev: Option<Candle>,
51 prev_prev: Option<Candle>,
52 has_emitted: bool,
53}
54
55impl Default for EveningDojiStar {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl EveningDojiStar {
62 pub const fn new() -> Self {
64 Self {
65 penetration: 0.3,
66 prev: None,
67 prev_prev: None,
68 has_emitted: false,
69 }
70 }
71
72 pub fn with_penetration(penetration: f64) -> Result<Self> {
76 if !(0.0..1.0).contains(&penetration) {
77 return Err(Error::InvalidPeriod {
78 message: "evening doji star penetration must lie in [0, 1)",
79 });
80 }
81 Ok(Self {
82 penetration,
83 prev: None,
84 prev_prev: None,
85 has_emitted: false,
86 })
87 }
88
89 pub fn penetration(&self) -> f64 {
91 self.penetration
92 }
93}
94
95impl Indicator for EveningDojiStar {
96 type Input = Candle;
97 type Output = f64;
98
99 #[inline]
100 fn update(&mut self, candle: Candle) -> Option<f64> {
101 let bar1 = self.prev_prev;
102 let bar2 = self.prev;
103 self.prev_prev = self.prev;
104 self.prev = Some(candle);
105 let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
106 return None;
107 };
108 self.has_emitted = true;
109 let range1 = bar1.high - bar1.low;
110 let range2 = bar2.high - bar2.low;
111 if range1 <= 0.0 || range2 <= 0.0 {
112 return Some(0.0);
113 }
114 let body1 = bar1.close - bar1.open;
115 if body1 < 0.5 * range1 {
116 return Some(0.0); }
118 if (bar2.close - bar2.open).abs() > 0.1 * range2 {
119 return Some(0.0); }
121 let star_bottom = bar2.open.min(bar2.close);
122 let bar3_top = candle.open.max(candle.close);
123 if star_bottom > bar1.close
124 && candle.close < candle.open
125 && bar3_top < star_bottom
126 && candle.close < bar1.close - self.penetration * body1
127 {
128 return Some(-1.0);
129 }
130 Some(0.0)
131 }
132
133 fn reset(&mut self) {
134 self.prev = None;
135 self.prev_prev = None;
136 self.has_emitted = false;
137 }
138
139 #[inline]
140 fn warmup_period(&self) -> usize {
141 3
142 }
143
144 #[inline]
145 fn is_ready(&self) -> bool {
146 self.has_emitted
147 }
148
149 #[inline]
150 fn name(&self) -> &'static str {
151 "EveningDojiStar"
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::traits::BatchExt;
159
160 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
161 Candle::new(open, high, low, close, 1.0, ts).unwrap()
162 }
163
164 #[test]
165 fn rejects_invalid_penetration() {
166 assert!(EveningDojiStar::with_penetration(-0.01).is_err());
167 assert!(EveningDojiStar::with_penetration(1.0).is_err());
168 }
169
170 #[test]
171 fn accepts_valid_penetration() {
172 let t = EveningDojiStar::with_penetration(0.5).unwrap();
173 assert!((t.penetration() - 0.5).abs() < 1e-12);
174 }
175
176 #[test]
177 fn accessors_and_metadata() {
178 let t = EveningDojiStar::default();
179 assert_eq!(t.name(), "EveningDojiStar");
180 assert_eq!(t.warmup_period(), 3);
181 assert!(!t.is_ready());
182 assert!((t.penetration() - 0.3).abs() < 1e-12);
183 }
184
185 #[test]
186 fn evening_doji_star_is_minus_one() {
187 let mut t = EveningDojiStar::new();
188 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
189 assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), None);
190 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(-1.0));
191 }
192
193 #[test]
194 fn middle_not_doji_yields_zero() {
195 let mut t = EveningDojiStar::new();
196 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
197 t.update(c(16.0, 18.1, 15.9, 18.0, 1));
199 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
200 }
201
202 #[test]
203 fn shallow_close_yields_zero() {
204 let mut t = EveningDojiStar::new();
205 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
206 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
207 assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
209 }
210
211 #[test]
212 fn first_two_bars_return_zero() {
213 let mut t = EveningDojiStar::new();
214 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
215 assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), None);
216 }
217
218 #[test]
219 fn batch_equals_streaming() {
220 let candles: Vec<Candle> = (0..40)
221 .map(|i| {
222 let base = 100.0 + i as f64;
223 c(base, base + 5.2, base - 0.1, base + 5.0, i)
224 })
225 .collect();
226 let mut a = EveningDojiStar::new();
227 let mut b = EveningDojiStar::new();
228 assert_eq!(
229 a.batch(&candles),
230 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
231 );
232 }
233
234 #[test]
235 fn reset_clears_state() {
236 let mut t = EveningDojiStar::new();
237 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
238 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
239 t.update(c(16.0, 16.1, 11.9, 12.0, 2));
240 assert!(t.is_ready());
241 t.reset();
242 assert!(!t.is_ready());
243 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
244 }
245
246 #[test]
247 fn zero_range_yields_zero() {
248 let mut t = EveningDojiStar::new();
249 t.update(c(10.0, 10.0, 10.0, 10.0, 0));
251 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
252 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
253 }
254
255 #[test]
256 fn short_first_body_yields_zero() {
257 let mut t = EveningDojiStar::new();
258 t.update(c(10.0, 16.0, 9.0, 10.5, 0));
260 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
261 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
262 }
263}