egui_charts/studies/builtin/
chandelier_exit.rs1use super::clamp_param;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{ATR, Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8#[derive(Clone)]
17pub struct ChandelierExit {
18 period: usize,
20 atr_period: usize,
22 multiplier: f64,
24 values: IndicatorValues,
25 atr: ATR,
26 stepper: Stepper<(usize, std::collections::VecDeque<[f64; 2]>)>,
27 colors: Vec<Color32>,
29 visible: bool,
30}
31
32impl ChandelierExit {
33 pub fn new(period: usize, multiplier: f64) -> Self {
35 Self {
36 period: period.max(1),
37 atr_period: period.max(1),
38 multiplier: clamp_param(multiplier, 0.0, f64::MAX),
39 values: IndicatorValues::new(),
40 atr: ATR::new(period),
41 stepper: Stepper::default(),
42 colors: vec![
43 palette::BLUE, palette::ORANGE, ],
46 visible: true,
47 }
48 }
49
50 pub fn with_atr_period(mut self, period: usize) -> Self {
52 self.atr_period = period.max(1);
53 self.atr = ATR::new(self.atr_period);
54 self
55 }
56
57 pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
59 self.colors = colors;
60 self
61 }
62}
63
64impl Default for ChandelierExit {
65 fn default() -> Self {
66 Self::new(22, 3.0)
67 }
68}
69
70impl Indicator for ChandelierExit {
71 crate::studies::inputs::study_inputs!(self;
72 period: length("Lookback period", 1, usize::MAX),
73 atr_period: length("ATR period", 1, usize::MAX),
74 multiplier: number("Multiplier", 0.0, f64::MAX)
75 ; changed => {
76 if changed == "period" {
78 self.atr_period = self.period;
79 }
80 });
81
82 fn name(&self) -> &str {
83 "Chandelier"
84 }
85
86 fn desc(&self) -> &str {
87 "Chandelier Exit - ATR-based trailing stop"
88 }
89
90 fn calculate(&mut self, data: &[Bar]) {
91 self.update(data, 0);
92 }
93
94 fn update(&mut self, data: &[Bar], from: usize) {
95 if from == 0 {
96 self.atr = ATR::new(self.atr_period);
97 }
98 self.atr.update(data, from);
99 self.stepper
100 .run(data, from, &mut self.values, |state, bar| {
101 let index = state.0;
102 state.0 += 1;
103 if ![bar.high, bar.low, bar.close].iter().all(|x| x.is_finite()) {
104 return IndicatorValue::None;
105 }
106 state.1.push_back([bar.high, bar.low]);
107 if state.1.len() > self.period {
108 state.1.pop_front();
109 }
110 if state.1.len() < self.period {
111 return IndicatorValue::None;
112 }
113 let Some(atr) = self.atr.values().value(0, index) else {
114 return IndicatorValue::None;
115 };
116 let high = state
117 .1
118 .iter()
119 .map(|v| v[0])
120 .fold(f64::NEG_INFINITY, f64::max);
121 let low = state.1.iter().map(|v| v[1]).fold(f64::INFINITY, f64::min);
122 IndicatorValue::Multiple(vec![
123 high - atr * self.multiplier,
124 low + atr * self.multiplier,
125 ])
126 });
127 }
128
129 fn is_incremental(&self) -> bool {
130 true
131 }
132
133 fn values(&self) -> &IndicatorValues {
134 &self.values
135 }
136
137 fn colors(&self) -> Vec<Color32> {
138 self.colors.clone()
139 }
140
141 fn set_colors(&mut self, colors: Vec<Color32>) {
142 if colors.len() >= 2 {
143 self.colors = colors;
144 }
145 }
146
147 fn is_overlay(&self) -> bool {
148 true }
150
151 fn is_visible(&self) -> bool {
152 self.visible
153 }
154
155 fn set_visible(&mut self, visible: bool) {
156 self.visible = visible;
157 }
158
159 fn clone_box(&self) -> Box<dyn Indicator> {
160 Box::new(self.clone())
161 }
162
163 fn line_count(&self) -> usize {
164 2
165 }
166
167 fn line_names(&self) -> Vec<String> {
168 let lengths = if self.atr_period == self.period {
169 self.period.to_string()
170 } else {
171 format!("{},{}", self.period, self.atr_period)
172 };
173 vec![
174 format!("Long Exit({lengths},{})", self.multiplier),
175 format!("Short Exit({lengths},{})", self.multiplier),
176 ]
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::studies::test_support::make_range_bar as make_bar;
184
185 #[test]
190 fn matches_reference_values() {
191 let trend = [
192 (102.0, 98.0, 100.0),
193 (104.0, 99.0, 103.0),
194 (106.0, 101.0, 105.0),
195 (108.0, 103.0, 107.0),
196 (118.0, 112.0, 116.0),
197 (117.0, 113.0, 114.0),
198 (115.0, 109.0, 110.0),
199 (112.0, 108.0, 111.0),
200 ];
201 let trend_expected = [
202 (96.66666666666667, 107.33333333333333),
203 (98.44444444444444, 108.55555555555556),
204 (104.29629629629629, 114.70370370370371),
205 (106.19753086419753, 114.80246913580247),
206 (106.13168724279835, 120.86831275720165),
207 (106.42112482853224, 118.57887517146776),
208 ];
209 let flat = [(101.0, 99.0, 100.0); 5];
210 let flat_expected = [(97.0, 103.0); 3];
211 let cases = [
212 ("trend with gap", &trend[..], &trend_expected[..]),
213 ("flat", &flat[..], &flat_expected[..]),
214 ];
215
216 let mut failures = Vec::new();
217 for (label, bars, expected) in cases {
218 let data: Vec<Bar> = bars.iter().map(|&(h, l, c)| make_bar(h, l, c)).collect();
219 let mut ce = ChandelierExit::new(3, 2.0);
220 ce.calculate(&data);
221
222 let got = ce.values().to_rows();
223 let warm_up = got
224 .iter()
225 .take(2)
226 .all(|v| matches!(v, IndicatorValue::None));
227 if got.len() != data.len() || !warm_up {
228 failures.push(format!("{label}: warm-up or length wrong: {got:?}"));
229 continue;
230 }
231 for (i, &(long, short)) in expected.iter().enumerate() {
232 let ok = matches!(&got[i + 2], IndicatorValue::Multiple(v)
233 if (v[0] - long).abs() < 1e-9 && (v[1] - short).abs() < 1e-9);
234 if !ok {
235 failures.push(format!(
236 "{label}: bar {} is {:?}, expected [{long}, {short}]",
237 i + 2,
238 got[i + 2]
239 ));
240 }
241 }
242 }
243 assert!(failures.is_empty(), "{}", failures.join("\n"));
244 }
245}