Skip to main content

egui_charts/studies/builtin/
market_breadth.rs

1//! Market breadth from two host-supplied symbol histories.
2//!
3//! The ratio is advancing issues divided by declining issues. The line is the running
4//! total of advancing minus declining issues. The volume index applies that same running
5//! difference to advancing and declining volume. Each input bar's close carries the count
6//! or volume, not the trading volume of an index instrument. Missing pairs and zero ratio
7//! denominators have no value; a missing pair leaves the running total unchanged.
8
9use crate::model::Bar;
10use crate::studies::palette;
11use crate::studies::{
12    Indicator, IndicatorValues, InputKind, InputValue, StudyData, StudyDataRequest, StudyInput,
13};
14use egui::Color32;
15
16/// Advancing issues divided by declining issues.
17pub type AdvanceDeclineRatio = MarketBreadth<true, false>;
18/// Running total of advancing minus declining issues.
19pub type AdvanceDeclineLine = MarketBreadth<false, false>;
20/// Running total of advancing volume minus declining volume.
21pub type CumulativeVolumeIndex = MarketBreadth<false, true>;
22
23/// A ratio or cumulative difference of two market series. `RATIO` selects the ratio and its name.
24/// `VOLUME` chooses volume-series default symbols and, without `RATIO`, the volume-index name, so
25/// with both `true` the study is the "Advance/Decline Ratio" of the volume series. Hosts map the
26/// default symbol keys to their own source, or replace them using the typed inputs. Calculations
27/// require [`Indicator::update_data`].
28#[derive(Clone)]
29pub struct MarketBreadth<const RATIO: bool, const VOLUME: bool> {
30    advancing_symbol: String,
31    declining_symbol: String,
32    timeframe: Option<crate::model::Timeframe>,
33    clock: Option<crate::model::BarClock>,
34    wait_for_close: bool,
35    values: IndicatorValues,
36    source_values: Vec<Option<f64>>,
37    color: Color32,
38    visible: bool,
39}
40
41impl<const RATIO: bool, const VOLUME: bool> Default for MarketBreadth<RATIO, VOLUME> {
42    fn default() -> Self {
43        Self {
44            advancing_symbol: if VOLUME {
45                "ADVANCING_VOLUME"
46            } else {
47                "ADVANCING_ISSUES"
48            }
49            .into(),
50            declining_symbol: if VOLUME {
51                "DECLINING_VOLUME"
52            } else {
53                "DECLINING_ISSUES"
54            }
55            .into(),
56            timeframe: None,
57            clock: None,
58            wait_for_close: true,
59            values: IndicatorValues::new(),
60            source_values: Vec::new(),
61            color: if !RATIO && !VOLUME {
62                palette::LIGHT_BLUE
63            } else {
64                palette::BLUE
65            },
66            visible: true,
67        }
68    }
69}
70
71impl<const RATIO: bool, const VOLUME: bool> Indicator for MarketBreadth<RATIO, VOLUME> {
72    fn name(&self) -> &str {
73        if RATIO {
74            "Advance/Decline Ratio"
75        } else if VOLUME {
76            "Cumulative Volume Index"
77        } else {
78            "Advance/Decline Line"
79        }
80    }
81    fn desc(&self) -> &str {
82        "Market breadth from host-supplied advancing and declining series"
83    }
84    fn inputs(&self) -> Vec<StudyInput> {
85        let defaults = Self::default();
86        vec![
87            StudyInput::new(
88                "advancing_symbol",
89                "Advancing symbol",
90                InputValue::Symbol(self.advancing_symbol.clone()),
91                InputKind::Symbol,
92            )
93            .with_default(InputValue::Symbol(defaults.advancing_symbol)),
94            StudyInput::new(
95                "declining_symbol",
96                "Declining symbol",
97                InputValue::Symbol(self.declining_symbol.clone()),
98                InputKind::Symbol,
99            )
100            .with_default(InputValue::Symbol(defaults.declining_symbol)),
101            StudyInput::new(
102                "timeframe",
103                "Interval",
104                InputValue::Timeframe(self.timeframe),
105                InputKind::Timeframe,
106            )
107            .with_default(InputValue::Timeframe(None)),
108            StudyInput::new(
109                "wait_for_close",
110                "Wait for interval close",
111                InputValue::Toggle(self.wait_for_close),
112                InputKind::Toggle,
113            )
114            .with_default(InputValue::Toggle(true)),
115        ]
116    }
117    fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
118        let schema = self
119            .inputs()
120            .into_iter()
121            .find(|input| input.id == id)
122            .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
123        if !schema.accepts(&value) {
124            return Err(crate::studies::StudyError::invalid_input(id));
125        }
126        match (id, value) {
127            ("advancing_symbol", InputValue::Symbol(value)) => self.advancing_symbol = value,
128            ("declining_symbol", InputValue::Symbol(value)) => self.declining_symbol = value,
129            ("timeframe", InputValue::Timeframe(value)) => self.timeframe = value,
130            ("wait_for_close", InputValue::Toggle(value)) => self.wait_for_close = value,
131            _ => return Err(crate::studies::StudyError::invalid_input(id)),
132        }
133        self.values.clear();
134        Ok(())
135    }
136    fn data_requests(&self) -> Vec<StudyDataRequest> {
137        [&self.advancing_symbol, &self.declining_symbol]
138            .into_iter()
139            .map(|symbol| StudyDataRequest {
140                metric: None,
141                symbol: symbol.clone(),
142                timeframe: self.timeframe,
143            })
144            .collect()
145    }
146    fn set_bar_clock(&mut self, clock: &crate::model::BarClock) -> bool {
147        let changed = self.clock.as_ref() != Some(clock);
148        self.clock = Some(clock.clone());
149        changed
150    }
151    fn calculate(&mut self, data: &[Bar]) {
152        self.values = IndicatorValues::missing(data.len());
153        self.source_values.clear();
154    }
155    fn update_data(&mut self, data: &[Bar], from: usize, sources: &[StudyData<'_>]) {
156        let mut from = from.min(self.values.len()).min(data.len());
157        let requests = self.data_requests();
158        let Some(advancing) = sources.iter().find(|source| source.request == requests[0]) else {
159            self.calculate(data);
160            return;
161        };
162        let Some(declining) = sources.iter().find(|source| source.request == requests[1]) else {
163            self.calculate(data);
164            return;
165        };
166        if from == 0 || self.source_values.len() != advancing.bars.len() {
167            from = 0;
168            let mut total = 0.0;
169            self.source_values = advancing
170                .bars
171                .iter()
172                .map(|bar| {
173                    let other = declining
174                        .bar_at(bar.time)
175                        .map(|at| declining.bars[at].close)?;
176                    if !bar.close.is_finite() || !other.is_finite() || (RATIO && other == 0.0) {
177                        return None;
178                    }
179                    let value = if RATIO {
180                        bar.close / other
181                    } else {
182                        total + bar.close - other
183                    };
184                    if !value.is_finite() {
185                        return None;
186                    }
187                    if !RATIO {
188                        total = value;
189                    }
190                    Some(value)
191                })
192                .collect();
193        }
194        // The former last chart bar becomes historical when another bar is appended.
195        let from = from.saturating_sub(1);
196        self.values.truncate(from);
197        let clock = self.clock.as_ref().unwrap_or(&advancing.clock);
198        for (at, bar) in data.iter().enumerate().skip(from) {
199            let value = advancing
200                .bar_at(bar.time)
201                .and_then(|_| {
202                    let forming_higher = at + 1 == data.len()
203                        && self.wait_for_close
204                        && clock
205                            .timeframe()
206                            .is_some_and(|tf| advancing.timeframe.duration_ms() > tf.duration_ms());
207                    let close = if forming_higher {
208                        bar.time
209                    } else {
210                        clock
211                            .next_open(clock.bar_open(bar.time))
212                            .unwrap_or(bar.time)
213                    };
214                    advancing.output_at(close, !self.wait_for_close && at + 1 == data.len())
215                })
216                .and_then(|at| self.source_values[at])
217                .unwrap_or(f64::NAN);
218            self.values.push_lines(&[value]);
219        }
220    }
221    fn is_incremental(&self) -> bool {
222        true
223    }
224    fn values(&self) -> &IndicatorValues {
225        &self.values
226    }
227    fn colors(&self) -> Vec<Color32> {
228        vec![self.color]
229    }
230    fn set_colors(&mut self, colors: Vec<Color32>) {
231        if let Some(color) = colors.first() {
232            self.color = *color;
233        }
234    }
235    fn is_overlay(&self) -> bool {
236        false
237    }
238    fn is_visible(&self) -> bool {
239        self.visible
240    }
241    fn set_visible(&mut self, visible: bool) {
242        self.visible = visible;
243    }
244    fn clone_box(&self) -> Box<dyn Indicator> {
245        Box::new(self.clone())
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::model::{BarClock, Timeframe};
253    use chrono::{DateTime, Duration};
254
255    #[test]
256    fn breadth_uses_paired_market_values_and_skips_missing_observations() {
257        let bars = |values: &[Option<f64>]| {
258            values
259                .iter()
260                .enumerate()
261                .filter_map(|(at, value)| {
262                    let value = (*value)?;
263                    Some(Bar::new(
264                        DateTime::UNIX_EPOCH + Duration::days(at as i64),
265                        value,
266                        value,
267                        value,
268                        value,
269                        999.0,
270                    ))
271                })
272                .collect::<Vec<_>>()
273        };
274        let chart = bars(&[Some(1.0); 5]);
275        for (name, advancing, declining, expected) in [
276            (
277                "Advance/Decline Ratio",
278                [Some(10.0), Some(14.0), Some(8.0), None, Some(5.0)],
279                [Some(4.0), Some(7.0), Some(0.0), Some(3.0), Some(10.0)],
280                [Some(2.5), Some(2.0), None, None, Some(0.5)],
281            ),
282            (
283                "Advance/Decline Line",
284                [Some(10.0), Some(14.0), Some(8.0), None, Some(5.0)],
285                [Some(4.0), Some(7.0), Some(0.0), Some(3.0), Some(10.0)],
286                [Some(6.0), Some(13.0), Some(21.0), None, Some(16.0)],
287            ),
288            (
289                "Cumulative Volume Index",
290                [Some(30.0), Some(70.0), Some(50.0), None, Some(20.0)],
291                [Some(10.0), Some(20.0), Some(0.0), Some(5.0), Some(5.0)],
292                [Some(20.0), Some(70.0), Some(120.0), None, Some(135.0)],
293            ),
294        ] {
295            for stamp in [0, 6] {
296                let mut study = crate::studies::IndicatorFactory::new()
297                    .create(name)
298                    .unwrap();
299                let requests = study.data_requests();
300                let mut advancing = bars(&advancing);
301                let mut declining = bars(&declining);
302                for bar in &mut advancing {
303                    bar.time += Duration::hours(stamp);
304                }
305                for bar in &mut declining {
306                    bar.time += Duration::hours(stamp * 2);
307                }
308                let sources: Vec<_> = requests
309                    .into_iter()
310                    .zip([&advancing, &declining])
311                    .map(|(request, bars)| StudyData {
312                        request,
313                        bars,
314                        clock: BarClock::new(Timeframe::Day1),
315                        instrument: None,
316                        timeframe: Timeframe::Day1,
317                    })
318                    .collect();
319                study.calculate(&chart);
320                assert!((0..chart.len()).all(|at| study.values().value(0, at).is_none()));
321                study.set_bar_clock(&BarClock::new(Timeframe::Day1));
322                study.update_data(&chart, 0, &sources);
323                let actual: Vec<_> = (0..chart.len())
324                    .map(|at| study.values().value(0, at))
325                    .collect();
326                assert_eq!(actual, expected, "{name}");
327                let half_days: Vec<_> = (0..11)
328                    .map(|at| {
329                        Bar::new(
330                            DateTime::UNIX_EPOCH + Duration::hours(at * 12),
331                            1.0,
332                            1.0,
333                            1.0,
334                            1.0,
335                            1.0,
336                        )
337                    })
338                    .collect();
339                study.set_bar_clock(&BarClock::new("12h".parse().unwrap()));
340                study.update_data(&half_days, 0, &sources);
341                // Daily values become available on the second half-day; the missing day stays empty.
342                let aligned = [
343                    None,
344                    expected[0],
345                    expected[0],
346                    expected[1],
347                    expected[1],
348                    expected[2],
349                    None,
350                    None,
351                    expected[2],
352                    expected[4],
353                    None,
354                ];
355                let actual: Vec<_> = (0..11).map(|at| study.values().value(0, at)).collect();
356                assert_eq!(actual, aligned, "{name} confirmed intervals");
357                study.update_data(&half_days[..10], 0, &sources);
358                assert_eq!(
359                    study.values().value(0, 9),
360                    expected[2],
361                    "{name} forming chart bar"
362                );
363                study
364                    .set_input("wait_for_close", InputValue::Toggle(false))
365                    .unwrap();
366                study.update_data(&half_days[..9], 0, &sources);
367                assert_eq!(
368                    study.values().value(0, 8),
369                    expected[4],
370                    "{name} developing interval"
371                );
372                assert_eq!(study.values().value(0, 0), None);
373            }
374        }
375    }
376}