Skip to main content

wickra_core/indicators/
advance_decline_ratio.rs

1//! Advance/Decline Ratio — advancing issues divided by declining issues.
2
3use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6/// Advance/Decline Ratio (ADR) — the number of advancing symbols divided by the
7/// number of declining symbols across a universe.
8///
9/// On each [`CrossSection`] tick the ratio is `advancers / decliners`: a reading
10/// above one means advancing issues outnumber declining ones (broad strength),
11/// while a reading below one signals broad weakness. Because it is a ratio rather
12/// than a difference, the ADR is comparable across universes of different sizes.
13///
14/// When a tick has no declining symbols the denominator is floored to one, so the
15/// ratio degrades gracefully to the advancer count instead of dividing by zero.
16///
17/// `Input = CrossSection`, `Output = f64`. The ratio is defined from the first
18/// tick, so `warmup_period == 1` and the indicator is ready after one update.
19///
20/// # Example
21///
22/// ```
23/// use wickra_core::{AdvanceDeclineRatio, CrossSection, Indicator, Member};
24///
25/// let mut adr = AdvanceDeclineRatio::new();
26/// // 3 advancers, 1 decliner -> ratio 3.0.
27/// let tick = CrossSection::new(
28///     vec![
29///         Member::new(1.0, 10.0, false, false),
30///         Member::new(0.5, 10.0, false, false),
31///         Member::new(2.0, 10.0, false, false),
32///         Member::new(-1.0, 10.0, false, false),
33///     ],
34///     0,
35/// )
36/// .unwrap();
37/// assert_eq!(adr.update(tick), Some(3.0));
38/// ```
39#[derive(Debug, Clone, Default)]
40pub struct AdvanceDeclineRatio {
41    has_emitted: bool,
42}
43
44impl AdvanceDeclineRatio {
45    /// Construct a new Advance/Decline Ratio indicator.
46    #[must_use]
47    pub const fn new() -> Self {
48        Self { has_emitted: false }
49    }
50}
51
52impl Indicator for AdvanceDeclineRatio {
53    type Input = CrossSection;
54    type Output = f64;
55
56    #[inline]
57    fn update(&mut self, section: CrossSection) -> Option<f64> {
58        let advancers = section.advancers() as f64;
59        let decliners = section.decliners().max(1) as f64;
60        self.has_emitted = true;
61        Some(advancers / decliners)
62    }
63
64    fn reset(&mut self) {
65        self.has_emitted = false;
66    }
67
68    #[inline]
69    fn warmup_period(&self) -> usize {
70        1
71    }
72
73    #[inline]
74    fn is_ready(&self) -> bool {
75        self.has_emitted
76    }
77
78    #[inline]
79    fn name(&self) -> &'static str {
80        "AdvanceDeclineRatio"
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::cross_section::Member;
88    use crate::traits::BatchExt;
89
90    fn section(up: usize, down: usize) -> CrossSection {
91        let mut members = Vec::new();
92        for _ in 0..up {
93            members.push(Member::new(1.0, 10.0, false, false));
94        }
95        for _ in 0..down {
96            members.push(Member::new(-1.0, 10.0, false, false));
97        }
98        // A non-empty unchanged member guarantees a valid universe when both
99        // counts are zero.
100        members.push(Member::new(0.0, 10.0, false, false));
101        CrossSection::new(members, 0).unwrap()
102    }
103
104    #[test]
105    fn accessors_and_metadata() {
106        let adr = AdvanceDeclineRatio::new();
107        assert_eq!(adr.name(), "AdvanceDeclineRatio");
108        assert_eq!(adr.warmup_period(), 1);
109        assert!(!adr.is_ready());
110    }
111
112    #[test]
113    fn first_tick_emits_ratio() {
114        let mut adr = AdvanceDeclineRatio::new();
115        assert_eq!(adr.update(section(3, 1)), Some(3.0));
116        assert!(adr.is_ready());
117    }
118
119    #[test]
120    fn zero_decliners_floors_denominator() {
121        let mut adr = AdvanceDeclineRatio::new();
122        // 4 advancers, 0 decliners -> 4 / max(0, 1) = 4.0.
123        assert_eq!(adr.update(section(4, 0)), Some(4.0));
124    }
125
126    #[test]
127    fn no_advancers_yields_zero() {
128        let mut adr = AdvanceDeclineRatio::new();
129        assert_eq!(adr.update(section(0, 5)), Some(0.0));
130    }
131
132    #[test]
133    fn reset_clears_state() {
134        let mut adr = AdvanceDeclineRatio::new();
135        adr.update(section(3, 1));
136        assert!(adr.is_ready());
137        adr.reset();
138        assert!(!adr.is_ready());
139        assert_eq!(adr.update(section(2, 1)), Some(2.0));
140    }
141
142    #[test]
143    fn batch_equals_streaming() {
144        let sections = vec![section(3, 1), section(4, 0), section(0, 5), section(2, 2)];
145        let mut a = AdvanceDeclineRatio::new();
146        let mut b = AdvanceDeclineRatio::new();
147        assert_eq!(
148            a.batch(&sections),
149            sections
150                .iter()
151                .map(|s| b.update(s.clone()))
152                .collect::<Vec<_>>()
153        );
154    }
155}