kestrel_chartkit/indicator/
candle_story.rs1use std::collections::HashMap;
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6pub struct CandleStoryEngine {
9 window: Vec<Bar>,
10 alerts: Vec<IndicatorAlert>,
11}
12
13impl CandleStoryEngine {
14 pub fn new() -> Self {
15 Self {
16 window: Vec::with_capacity(5),
17 alerts: Vec::new(),
18 }
19 }
20}
21
22impl Default for CandleStoryEngine {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl Indicator for CandleStoryEngine {
29 fn name(&self) -> &str {
30 "candle_story"
31 }
32
33 fn warmup_period(&self) -> usize {
34 2
35 }
36
37 fn reset(&mut self) {
38 self.window.clear();
39 self.alerts.clear();
40 }
41
42 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
43 self.window.push(bar.clone());
44 if self.window.len() > 5 {
45 self.window.remove(0);
46 }
47
48 self.alerts.clear();
49
50 let range = bar.high - bar.low;
51 if range <= 0.0 {
52 return Some(IndicatorOutput::new(0.0));
53 }
54
55 let body = (bar.close - bar.open).abs();
56 let upper_wick = bar.high - bar.close.max(bar.open);
57 let lower_wick = bar.close.min(bar.open) - bar.low;
58 let is_bullish = bar.close >= bar.open;
59
60 let close_pos = (bar.close - bar.low) / range; let pressure = (close_pos - 0.5) * 200.0;
63
64 let mut pattern_type = 0.0f64;
65
66 if lower_wick >= 0.55 * range && close_pos >= 0.65 {
68 pattern_type = 1.0; self.alerts.push(IndicatorAlert::new(
70 "bullish_kangaroo_tail",
71 format!(
72 "Bullish Kangaroo Tail / Pinbar Reversal (Lower Wick {:.0}%)",
73 (lower_wick / range) * 100.0
74 ),
75 0.9,
76 ));
77 } else if upper_wick >= 0.55 * range && close_pos <= 0.35 {
78 pattern_type = 2.0; self.alerts.push(IndicatorAlert::new(
80 "bearish_kangaroo_tail",
81 format!(
82 "Bearish Kangaroo Tail / Pinbar Reversal (Upper Wick {:.0}%)",
83 (upper_wick / range) * 100.0
84 ),
85 0.9,
86 ));
87 }
88
89 if self.window.len() >= 2 {
91 let prev = &self.window[self.window.len() - 2];
92 let prev_body = (prev.close - prev.open).abs();
93 let prev_bearish = prev.close < prev.open;
94 let prev_bullish = prev.close > prev.open;
95
96 if is_bullish && prev_bearish && body > prev_body && bar.close > prev.open {
97 pattern_type = 3.0; self.alerts.push(IndicatorAlert::new(
99 "bullish_engulfing",
100 "Bullish Engulfing Pattern (Strong Momentum Shift)",
101 0.85,
102 ));
103 } else if !is_bullish && prev_bullish && body > prev_body && bar.close < prev.open {
104 pattern_type = 4.0; self.alerts.push(IndicatorAlert::new(
106 "bearish_engulfing",
107 "Bearish Engulfing Pattern (Strong Seller Dominance)",
108 0.85,
109 ));
110 }
111
112 let high_diff = (bar.high - prev.high).abs() / bar.high;
114 let low_diff = (bar.low - prev.low).abs() / bar.low;
115
116 if low_diff < 0.0015 && is_bullish && prev_bearish {
117 pattern_type = 5.0; self.alerts.push(IndicatorAlert::new(
119 "bullish_tweezer",
120 "Bullish Tweezer Bottom (Double Support Reversal)",
121 0.8,
122 ));
123 } else if high_diff < 0.0015 && !is_bullish && prev_bullish {
124 pattern_type = 6.0; self.alerts.push(IndicatorAlert::new(
126 "bearish_tweezer",
127 "Bearish Tweezer Top (Double Resistance Reversal)",
128 0.8,
129 ));
130 }
131 }
132
133 if body / range >= 0.82 {
135 if is_bullish {
136 pattern_type = 7.0;
137 self.alerts.push(IndicatorAlert::new(
138 "bullish_marubozu",
139 format!(
140 "Bullish Marubozu ({:.0}% Body Dominance)",
141 (body / range) * 100.0
142 ),
143 0.8,
144 ));
145 } else {
146 pattern_type = 8.0;
147 self.alerts.push(IndicatorAlert::new(
148 "bearish_marubozu",
149 format!(
150 "Bearish Marubozu ({:.0}% Body Dominance)",
151 (body / range) * 100.0
152 ),
153 0.8,
154 ));
155 }
156 }
157
158 let mut extra = HashMap::new();
159 extra.insert("pattern_type".to_string(), pattern_type);
160 extra.insert("pressure".to_string(), pressure);
161 extra.insert("body_ratio".to_string(), body / range);
162
163 Some(IndicatorOutput::with_extra(pressure, extra))
164 }
165
166 fn alerts(&self) -> Vec<IndicatorAlert> {
167 self.alerts.clone()
168 }
169}
170
171pub fn build_candle_story(_params: &HashMap<String, f64>) -> CandleStoryEngine {
172 CandleStoryEngine::new()
173}