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 fn update(&mut self, candle: Candle) -> Option<f64> {
100 self.has_emitted = true;
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 Some(0.0);
107 };
108 let range1 = bar1.high - bar1.low;
109 let range2 = bar2.high - bar2.low;
110 if range1 <= 0.0 || range2 <= 0.0 {
111 return Some(0.0);
112 }
113 let body1 = bar1.close - bar1.open;
114 if body1 < 0.5 * range1 {
115 return Some(0.0); }
117 if (bar2.close - bar2.open).abs() > 0.1 * range2 {
118 return Some(0.0); }
120 let star_bottom = bar2.open.min(bar2.close);
121 let bar3_top = candle.open.max(candle.close);
122 if star_bottom > bar1.close
123 && candle.close < candle.open
124 && bar3_top < star_bottom
125 && candle.close < bar1.close - self.penetration * body1
126 {
127 return Some(-1.0);
128 }
129 Some(0.0)
130 }
131
132 fn reset(&mut self) {
133 self.prev = None;
134 self.prev_prev = None;
135 self.has_emitted = false;
136 }
137
138 fn warmup_period(&self) -> usize {
139 3
140 }
141
142 fn is_ready(&self) -> bool {
143 self.has_emitted
144 }
145
146 fn name(&self) -> &'static str {
147 "EveningDojiStar"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::BatchExt;
155
156 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
157 Candle::new(open, high, low, close, 1.0, ts).unwrap()
158 }
159
160 #[test]
161 fn rejects_invalid_penetration() {
162 assert!(EveningDojiStar::with_penetration(-0.01).is_err());
163 assert!(EveningDojiStar::with_penetration(1.0).is_err());
164 }
165
166 #[test]
167 fn accepts_valid_penetration() {
168 let t = EveningDojiStar::with_penetration(0.5).unwrap();
169 assert!((t.penetration() - 0.5).abs() < 1e-12);
170 }
171
172 #[test]
173 fn accessors_and_metadata() {
174 let t = EveningDojiStar::default();
175 assert_eq!(t.name(), "EveningDojiStar");
176 assert_eq!(t.warmup_period(), 3);
177 assert!(!t.is_ready());
178 assert!((t.penetration() - 0.3).abs() < 1e-12);
179 }
180
181 #[test]
182 fn evening_doji_star_is_minus_one() {
183 let mut t = EveningDojiStar::new();
184 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
185 assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), Some(0.0));
186 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(-1.0));
187 }
188
189 #[test]
190 fn middle_not_doji_yields_zero() {
191 let mut t = EveningDojiStar::new();
192 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
193 t.update(c(16.0, 18.1, 15.9, 18.0, 1));
195 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
196 }
197
198 #[test]
199 fn shallow_close_yields_zero() {
200 let mut t = EveningDojiStar::new();
201 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
202 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
203 assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
205 }
206
207 #[test]
208 fn first_two_bars_return_zero() {
209 let mut t = EveningDojiStar::new();
210 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
211 assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), Some(0.0));
212 }
213
214 #[test]
215 fn batch_equals_streaming() {
216 let candles: Vec<Candle> = (0..40)
217 .map(|i| {
218 let base = 100.0 + i as f64;
219 c(base, base + 5.2, base - 0.1, base + 5.0, i)
220 })
221 .collect();
222 let mut a = EveningDojiStar::new();
223 let mut b = EveningDojiStar::new();
224 assert_eq!(
225 a.batch(&candles),
226 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
227 );
228 }
229
230 #[test]
231 fn reset_clears_state() {
232 let mut t = EveningDojiStar::new();
233 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
234 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
235 t.update(c(16.0, 16.1, 11.9, 12.0, 2));
236 assert!(t.is_ready());
237 t.reset();
238 assert!(!t.is_ready());
239 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
240 }
241
242 #[test]
243 fn zero_range_yields_zero() {
244 let mut t = EveningDojiStar::new();
245 t.update(c(10.0, 10.0, 10.0, 10.0, 0));
247 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
248 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
249 }
250
251 #[test]
252 fn short_first_body_yields_zero() {
253 let mut t = EveningDojiStar::new();
254 t.update(c(10.0, 16.0, 9.0, 10.5, 0));
256 t.update(c(17.0, 17.1, 16.9, 17.0, 1));
257 assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
258 }
259}