egui_charts/studies/builtin/
accumulation_swing.rs1use super::clamp_param;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8#[derive(Clone)]
12pub struct AccumulationSwingIndex {
13 limit_move: f64,
15 values: IndicatorValues,
16 stepper: Stepper<State>,
17 color: Color32,
18 visible: bool,
19}
20
21#[derive(Clone, Default)]
22struct State {
23 bars: usize,
24 count: usize,
25 first: Option<usize>,
26 previous: Option<Bar>,
27 total: f64,
28}
29
30impl AccumulationSwingIndex {
31 pub fn new(limit_move: f64) -> Self {
34 Self {
35 limit_move: clamp_param(limit_move, f64::EPSILON, f64::MAX),
36 values: IndicatorValues::new(),
37 stepper: Stepper::default(),
38 color: palette::LIGHT_BLUE, visible: true,
40 }
41 }
42
43 pub fn with_color(mut self, color: Color32) -> Self {
45 self.color = color;
46 self
47 }
48
49 fn calculate_swing_idx(current: &Bar, prev: &Bar, limit: f64) -> f64 {
50 let c = current.close;
51 let cy = prev.close;
52 let h = current.high;
53 let l = current.low;
54 let o = current.open;
55 let oy = prev.open;
56
57 let k = (h - cy).abs().max((l - cy).abs());
58
59 let h_cy = (h - cy).abs();
60 let l_cy = (l - cy).abs();
61 let h_l = h - l;
62 let cy_oy = (cy - oy).abs();
63
64 let r = if h_cy >= l_cy && h_cy >= h_l {
65 h_cy - 0.5 * l_cy + 0.25 * cy_oy
66 } else if l_cy >= h_cy && l_cy >= h_l {
67 l_cy - 0.5 * h_cy + 0.25 * cy_oy
68 } else {
69 h_l + 0.25 * cy_oy
70 };
71
72 if r == 0.0 {
73 return 0.0;
74 }
75
76 50.0 * ((c - cy) + 0.5 * (c - o) + 0.25 * (cy - oy)) / r * (k / limit)
77 }
78}
79
80impl Default for AccumulationSwingIndex {
81 fn default() -> Self {
82 Self::new(1000.0)
83 }
84}
85
86impl Indicator for AccumulationSwingIndex {
87 crate::studies::inputs::study_inputs!(self;
88 limit_move: number("Limit move", f64::EPSILON, f64::MAX)
89 ; {});
90
91 fn name(&self) -> &str {
92 "ASI"
93 }
94
95 fn desc(&self) -> &str {
96 "Accumulation Swing Index - Cumulative swing index"
97 }
98
99 fn calculate(&mut self, data: &[Bar]) {
100 self.update(data, 0);
101 }
102
103 fn update(&mut self, data: &[Bar], from: usize) {
104 self.stepper
105 .run(data, from, &mut self.values, |state, bar| {
106 let index = state.bars;
107 state.bars += 1;
108 if !([bar.open, bar.high, bar.low, bar.close]
109 .iter()
110 .all(|x| x.is_finite()))
111 {
112 return IndicatorValue::None;
113 }
114 state.count += 1;
115 if let Some(previous) = &state.previous {
116 state.total += Self::calculate_swing_idx(bar, previous, self.limit_move);
117 }
118 state.first.get_or_insert(index);
119 state.previous = Some(bar.clone());
120 IndicatorValue::Single(state.total)
121 });
122 if let Some(first) = self.stepper.state().first {
124 self.values.set(
125 first,
126 if self.stepper.state().count < 2 {
127 IndicatorValue::None
128 } else {
129 IndicatorValue::Single(0.0)
130 },
131 );
132 }
133 }
134
135 fn is_incremental(&self) -> bool {
136 true
137 }
138
139 fn values(&self) -> &IndicatorValues {
140 &self.values
141 }
142
143 fn colors(&self) -> Vec<Color32> {
144 vec![self.color]
145 }
146
147 fn set_colors(&mut self, colors: Vec<Color32>) {
148 if !colors.is_empty() {
149 self.color = colors[0];
150 }
151 }
152
153 fn is_overlay(&self) -> bool {
154 false
155 }
156
157 fn is_visible(&self) -> bool {
158 self.visible
159 }
160
161 fn set_visible(&mut self, visible: bool) {
162 self.visible = visible;
163 }
164
165 fn clone_box(&self) -> Box<dyn Indicator> {
166 Box::new(self.clone())
167 }
168
169 fn line_names(&self) -> Vec<String> {
170 vec!["ASI".to_string()]
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use chrono::Utc;
178
179 fn make_bar(open: f64, high: f64, low: f64, close: f64) -> Bar {
180 Bar {
181 time: Utc::now(),
182 open,
183 high,
184 low,
185 close,
186 volume: 1000.0,
187 colors: None,
188 payload: None,
189 }
190 }
191
192 #[test]
193 fn test_asi_calculation() {
194 let mut asi = AccumulationSwingIndex::new(100.0);
195
196 let data = vec![
197 make_bar(100.0, 102.0, 98.0, 101.0),
198 make_bar(101.0, 104.0, 100.0, 103.0),
199 make_bar(103.0, 106.0, 102.0, 105.0),
200 ];
201
202 asi.calculate(&data);
203
204 assert_eq!(asi.values.len(), 3);
205
206 let (IndicatorValue::Single(v1), IndicatorValue::Single(v2)) =
208 (&asi.values.to_rows()[1], &asi.values.to_rows()[2])
209 else {
210 panic!("no values: {:?}", &asi.values.to_rows()[1..]);
211 };
212 assert!(v2 > v1, "ASI should increase in uptrend");
213 }
214}