Skip to main content

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}