egui_charts/model/bar/patterns.rs
1//! Candlestick Pattern Detection
2//!
3//! Japanese candlestick patterns for technical analysis.
4//! These methods extend the Bar type with pattern recognition capabilities.
5
6use super::bar::Bar;
7
8/// Extension trait for candlestick pattern detection
9pub trait CandlePatterns {
10 // === Single Bar Patterns ===
11
12 /// Returns true if this is a hammer pattern
13 ///
14 /// Hammer characteristics:
15 /// - Small body (< 30% of range)
16 /// - Long lower wick (> 2x body)
17 /// - Little to no upper wick
18 /// - Typically appears in downtrends (bullish reversal)
19 fn is_hammer(&self) -> bool;
20
21 /// Returns true if this is an inverted hammer pattern
22 ///
23 /// Inverted Hammer characteristics:
24 /// - Small body (< 30% of range)
25 /// - Long upper wick (> 2x body)
26 /// - Little to no lower wick
27 /// - Typically appears in downtrends (bullish reversal)
28 fn is_inverted_hammer(&self) -> bool;
29
30 /// Returns true if this is a shooting star pattern
31 ///
32 /// Shooting Star characteristics:
33 /// - Small body (< 30% of range)
34 /// - Long upper wick (> 2x body)
35 /// - Little to no lower wick
36 /// - Body is near the low
37 /// - Typically appears in uptrends (bearish reversal)
38 fn is_shooting_star(&self) -> bool;
39
40 /// Returns true if this is a spinning top pattern
41 ///
42 /// Spinning Top characteristics:
43 /// - Small body (< 30% of range)
44 /// - Upper and lower wicks of similar length
45 /// - Indicates indecision in the market
46 fn is_spinning_top(&self) -> bool;
47
48 /// Returns true if this is a marubozu (very long body, little to no wicks)
49 ///
50 /// Marubozu characteristics:
51 /// - Large body (> 85% of range)
52 /// - Very small or no wicks
53 /// - Strong directional move
54 fn is_marubozu(&self) -> bool;
55
56 // === Two Bar Patterns ===
57
58 /// Returns true if current bar engulfs the previous bar (bullish engulfing)
59 ///
60 /// Bullish Engulfing:
61 /// - Previous bar is bearish
62 /// - Current bar is bullish
63 /// - Current body completely engulfs previous body
64 fn is_bullish_engulfing(&self, prev: &Bar) -> bool;
65
66 /// Returns true if current bar engulfs the previous bar (bearish engulfing)
67 ///
68 /// Bearish Engulfing:
69 /// - Previous bar is bullish
70 /// - Current bar is bearish
71 /// - Current body completely engulfs previous body
72 fn is_bearish_engulfing(&self, prev: &Bar) -> bool;
73
74 // === Three Bar Patterns ===
75
76 /// Returns true if this forms a morning star pattern with two previous bars
77 ///
78 /// Morning Star (bullish reversal):
79 /// 1. Large bearish candle
80 /// 2. Small-bodied candle (gap down)
81 /// 3. Large bullish candle
82 fn is_morning_star(&self, prev1: &Bar, prev2: &Bar) -> bool;
83
84 /// Returns true if this forms an evening star pattern with two previous bars
85 ///
86 /// Evening Star (bearish reversal):
87 /// 1. Large bullish candle
88 /// 2. Small-bodied candle (gap up)
89 /// 3. Large bearish candle
90 fn is_evening_star(&self, prev1: &Bar, prev2: &Bar) -> bool;
91}
92
93impl CandlePatterns for Bar {
94 fn is_hammer(&self) -> bool {
95 let body_pct = self.body_percentage();
96 let lower = self.lower_wick();
97 let upper = self.upper_wick();
98 let body = self.body_height();
99
100 body_pct < 0.3 && lower > 2.0 * body && upper < body * 0.5
101 }
102
103 fn is_inverted_hammer(&self) -> bool {
104 let body_pct = self.body_percentage();
105 let lower = self.lower_wick();
106 let upper = self.upper_wick();
107 let body = self.body_height();
108
109 body_pct < 0.3 && upper > 2.0 * body && lower < body * 0.5
110 }
111
112 fn is_shooting_star(&self) -> bool {
113 let body_pct = self.body_percentage();
114 let lower = self.lower_wick();
115 let upper = self.upper_wick();
116 let body = self.body_height();
117
118 body_pct < 0.3 && upper > 2.0 * body && lower < body * 0.3
119 }
120
121 fn is_spinning_top(&self) -> bool {
122 let body_pct = self.body_percentage();
123 let lower = self.lower_wick();
124 let upper = self.upper_wick();
125
126 if body_pct >= 0.3 {
127 return false;
128 }
129
130 // Wicks should be similar length (within 50% of each other)
131 let wick_ratio = if lower > upper {
132 upper / lower
133 } else if upper > 0.0 {
134 lower / upper
135 } else {
136 return false;
137 };
138
139 wick_ratio > 0.5
140 }
141
142 fn is_marubozu(&self) -> bool {
143 self.body_percentage() > 0.85
144 }
145
146 fn is_bullish_engulfing(&self, prev: &Bar) -> bool {
147 if !prev.is_bearish() || !self.is_bullish() {
148 return false;
149 }
150
151 let prev_body_top = prev.open;
152 let prev_body_bottom = prev.close;
153 let curr_body_top = self.close;
154 let curr_body_bottom = self.open;
155
156 curr_body_top > prev_body_top && curr_body_bottom < prev_body_bottom
157 }
158
159 fn is_bearish_engulfing(&self, prev: &Bar) -> bool {
160 if !prev.is_bullish() || !self.is_bearish() {
161 return false;
162 }
163
164 let prev_body_top = prev.close;
165 let prev_body_bottom = prev.open;
166 let curr_body_top = self.open;
167 let curr_body_bottom = self.close;
168
169 curr_body_top > prev_body_top && curr_body_bottom < prev_body_bottom
170 }
171
172 fn is_morning_star(&self, prev1: &Bar, prev2: &Bar) -> bool {
173 // prev2 (oldest) should be bearish with large body
174 if !prev2.is_bearish() || prev2.body_percentage() < 0.5 {
175 return false;
176 }
177
178 // prev1 (middle) should have small body
179 if prev1.body_percentage() > 0.3 {
180 return false;
181 }
182
183 // Current should be bullish with large body
184 if !self.is_bullish() || self.body_percentage() < 0.5 {
185 return false;
186 }
187
188 // Current close should penetrate into first candle's body
189 self.close > (prev2.open + prev2.close) / 2.0
190 }
191
192 fn is_evening_star(&self, prev1: &Bar, prev2: &Bar) -> bool {
193 // prev2 (oldest) should be bullish with large body
194 if !prev2.is_bullish() || prev2.body_percentage() < 0.5 {
195 return false;
196 }
197
198 // prev1 (middle) should have small body
199 if prev1.body_percentage() > 0.3 {
200 return false;
201 }
202
203 // Current should be bearish with large body
204 if !self.is_bearish() || self.body_percentage() < 0.5 {
205 return false;
206 }
207
208 // Current close should penetrate into first candle's body
209 self.close < (prev2.open + prev2.close) / 2.0
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use chrono::Utc;
217
218 #[test]
219 fn test_hammer_pattern() {
220 // Hammer: small body at top, long lower wick, no upper wick
221 // open=99, high=100, low=90, close=100 => body=1, range=10, lower_wick=9, upper_wick=0
222 // body_pct=0.1 < 0.3 ✓, lower(9) > 2*body(2) ✓, upper(0) < body*0.5(0.5) ✓
223 let hammer = Bar::new(Utc::now(), 99.0, 100.0, 90.0, 100.0, 1000.0);
224 assert!(hammer.is_hammer());
225
226 // Not a hammer: equal wicks (body in middle of range)
227 let not_hammer = Bar::new(Utc::now(), 95.0, 100.0, 90.0, 95.0, 1000.0);
228 assert!(!not_hammer.is_hammer());
229 }
230
231 #[test]
232 fn test_marubozu_pattern() {
233 // Marubozu: body is almost the entire range
234 let marubozu = Bar::new(Utc::now(), 100.0, 110.0, 100.0, 110.0, 1000.0);
235 assert!(marubozu.is_marubozu());
236
237 // Not marubozu: significant wicks
238 let not_marubozu = Bar::new(Utc::now(), 102.0, 110.0, 100.0, 108.0, 1000.0);
239 assert!(!not_marubozu.is_marubozu());
240 }
241
242 #[test]
243 fn test_engulfing_patterns() {
244 let bearish_bar = Bar::new(Utc::now(), 105.0, 107.0, 99.0, 100.0, 1000.0);
245 let bullish_engulfing = Bar::new(Utc::now(), 98.0, 108.0, 97.0, 106.0, 1000.0);
246
247 assert!(bullish_engulfing.is_bullish_engulfing(&bearish_bar));
248 assert!(!bullish_engulfing.is_bearish_engulfing(&bearish_bar));
249 }
250}