egui_charts/studies/builtin/
squeeze_momentum.rs1use super::{clamp_param, over_finite_bars};
2use crate::model::Bar;
3use crate::studies::helpers;
4use crate::studies::palette;
5use crate::studies::stepper::{update_window, usable_behind};
6use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
7use egui::Color32;
8
9#[derive(Clone)]
13pub struct SqueezeMomentum {
14 bb_period: usize,
16 bb_mult: f64,
18 kc_period: usize,
20 kc_mult: f64,
22 momentum_period: usize,
24 values: IndicatorValues,
25 colors: Vec<Color32>,
27 visible: bool,
28}
29
30impl SqueezeMomentum {
31 pub fn new() -> Self {
33 Self {
34 bb_period: 20,
35 bb_mult: 2.0,
36 kc_period: 20,
37 kc_mult: 1.5,
38 momentum_period: 12,
39 values: IndicatorValues::new(),
40 colors: vec![
41 palette::GREEN, palette::DISABLED, palette::RED, palette::BLUE, ],
46 visible: true,
47 }
48 }
49
50 pub fn with_bb_params(mut self, period: usize, mult: f64) -> Self {
53 self.bb_period = period.max(1);
54 self.bb_mult = clamp_param(mult, 0.0, f64::MAX);
55 self
56 }
57
58 pub fn with_kc_params(mut self, period: usize, mult: f64) -> Self {
61 self.kc_period = period.max(1);
62 self.kc_mult = clamp_param(mult, 0.0, f64::MAX);
63 self
64 }
65
66 pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
68 self.colors = colors;
69 self
70 }
71
72 fn calculate_atr(data: &[Bar], period: usize) -> Vec<f64> {
73 let mut tr = Vec::with_capacity(data.len());
74 tr.push(data[0].high - data[0].low);
75
76 for i in 1..data.len() {
77 let high_low = data[i].high - data[i].low;
78 let high_close = (data[i].high - data[i - 1].close).abs();
79 let low_close = (data[i].low - data[i - 1].close).abs();
80 tr.push(high_low.max(high_close).max(low_close));
81 }
82
83 helpers::sma(&tr, period)
84 }
85
86 fn linear_regression(data: &[f64], period: usize) -> Vec<f64> {
87 let mut result = Vec::with_capacity(data.len());
88
89 for i in 0..data.len() {
90 if i < period - 1 {
91 result.push(f64::NAN);
92 } else {
93 let window = &data[i + 1 - period..=i];
94 let n = period as f64;
95
96 let sum_x: f64 = (0..period).map(|x| x as f64).sum();
97 let sum_y: f64 = window.iter().sum();
98 let sum_xy: f64 = window.iter().enumerate().map(|(x, &y)| x as f64 * y).sum();
99 let sum_xx: f64 = (0..period).map(|x| (x * x) as f64).sum();
100
101 let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
102 let intercept = (sum_y - slope * sum_x) / n;
103
104 let value = intercept + slope * (period - 1) as f64;
106 result.push(value);
107 }
108 }
109
110 result
111 }
112}
113
114impl Default for SqueezeMomentum {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120impl SqueezeMomentum {
121 fn compute(&self, data: &[Bar]) -> IndicatorValues {
123 let mut values = IndicatorValues::new();
124 let required = self.bb_period.max(self.kc_period).max(self.momentum_period);
125 if data.len() < required {
126 for _ in 0..data.len() {
127 values.push(IndicatorValue::None);
128 }
129 return values;
130 }
131
132 let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
134
135 let bb_sma = helpers::sma(&closes, self.bb_period);
137 let bb_std = helpers::stdev(&closes, self.bb_period);
138
139 let kc_sma = helpers::sma(&closes, self.kc_period);
141 let atr = Self::calculate_atr(data, self.kc_period);
142
143 let hl2: Vec<f64> = data.iter().map(|b| (b.high + b.low) / 2.0).collect();
145 let hl2_sma = helpers::sma(&hl2, self.momentum_period);
146
147 let mut deviation = Vec::with_capacity(data.len());
149 for i in 0..data.len() {
150 if hl2_sma[i].is_nan() {
151 deviation.push(f64::NAN);
152 } else {
153 deviation.push(closes[i] - hl2_sma[i]);
154 }
155 }
156 let momentum = Self::linear_regression(&deviation, self.momentum_period);
157
158 for i in 0..data.len() {
160 if bb_sma[i].is_nan()
161 || bb_std[i].is_nan()
162 || kc_sma[i].is_nan()
163 || atr[i].is_nan()
164 || momentum[i].is_nan()
165 {
166 values.push(IndicatorValue::None);
167 } else {
168 let bb_upper = bb_sma[i] + self.bb_mult * bb_std[i];
169 let bb_lower = bb_sma[i] - self.bb_mult * bb_std[i];
170 let kc_upper = kc_sma[i] + self.kc_mult * atr[i];
171 let kc_lower = kc_sma[i] - self.kc_mult * atr[i];
172
173 let squeeze_on = bb_lower > kc_lower && bb_upper < kc_upper;
175
176 values.push_lines(&[momentum[i], if squeeze_on { 1.0 } else { 0.0 }]);
177 }
178 }
179
180 values
181 }
182}
183
184impl Indicator for SqueezeMomentum {
185 crate::studies::inputs::study_inputs!(self;
186 bb_period: length("Bb period", 1, usize::MAX),
187 bb_mult: number("Bb mult", 0.0, f64::MAX),
188 kc_period: length("Kc period", 1, usize::MAX),
189 kc_mult: number("Kc mult", 0.0, f64::MAX),
190 momentum_period: length("Momentum period", 1, usize::MAX)
191 ; {});
192
193 fn value_format(&self) -> crate::scales::StudyFormat {
194 crate::scales::StudyFormat::Price
195 }
196
197 fn name(&self) -> &str {
198 "Squeeze"
199 }
200
201 fn desc(&self) -> &str {
202 "Squeeze Momentum - Identifies volatility breakouts"
203 }
204
205 fn calculate(&mut self, data: &[Bar]) {
206 self.update(data, 0);
207 }
208
209 fn update(&mut self, data: &[Bar], from: usize) {
210 let mut values = std::mem::take(&mut self.values);
211 update_window(
212 &mut values,
213 data,
214 from,
215 usable_behind(
216 (self.bb_period - 1)
217 .max(self.kc_period)
218 .max((self.momentum_period - 1).saturating_mul(2)),
219 |b| b.high.is_finite() && b.low.is_finite() && b.close.is_finite(),
220 ),
221 |data| {
222 over_finite_bars(
223 data,
224 |b| b.high.is_finite() && b.low.is_finite() && b.close.is_finite(),
225 |data| self.compute(data),
226 )
227 },
228 );
229 self.values = values;
230 }
231
232 fn is_incremental(&self) -> bool {
233 true
234 }
235
236 fn values(&self) -> &IndicatorValues {
237 &self.values
238 }
239
240 fn colors(&self) -> Vec<Color32> {
241 self.colors.clone()
242 }
243
244 fn set_colors(&mut self, colors: Vec<Color32>) {
245 if colors.len() >= 4 {
246 self.colors = colors;
247 }
248 }
249
250 fn is_overlay(&self) -> bool {
251 false
252 }
253
254 fn is_visible(&self) -> bool {
255 self.visible
256 }
257
258 fn set_visible(&mut self, visible: bool) {
259 self.visible = visible;
260 }
261
262 fn clone_box(&self) -> Box<dyn Indicator> {
263 Box::new(self.clone())
264 }
265
266 fn line_names(&self) -> Vec<String> {
267 vec!["Momentum".to_string(), "Squeeze".to_string()]
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::studies::test_support::make_range_bar as make_bar;
275
276 #[test]
277 fn test_squeeze_calculation() {
278 let mut squeeze = SqueezeMomentum::new();
279
280 let data: Vec<Bar> = (0..50)
281 .map(|i| make_bar(102.0 + i as f64, 98.0 + i as f64, 100.0 + i as f64))
282 .collect();
283
284 squeeze.calculate(&data);
285
286 assert_eq!(squeeze.values.len(), 50);
287
288 let rows = squeeze.values.to_rows();
290 let IndicatorValue::Multiple(v) = rows.last().unwrap() else {
291 panic!("no value: {:?}", squeeze.values.to_rows().last().unwrap());
292 };
293 assert_eq!(v.len(), 2);
294 }
295}