Skip to main content

egui_charts/studies/builtin/
accumulation_swing.rs

1use 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/// Accumulation Swing Index (ASI)
9/// Developed by Welles Wilder
10/// Cumulative sum of swing index values
11#[derive(Clone)]
12pub struct AccumulationSwingIndex {
13    /// Limit move (typically high value for stocks, actual limit for futures)
14    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    /// `limit_move` is clamped into `f64::EPSILON` to `f64::MAX`, NaN to `f64::EPSILON`; a
32    /// smaller limit move only scales the index up.
33    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, // Teal
39            visible: true,
40        }
41    }
42
43    /// Set the plot color.
44    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        // The batch definition reveals the initial zero only once a second usable bar exists.
123        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        // In uptrend, ASI should increase
207        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}