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    fn update(&mut self, section: CrossSection) -> Option<f64> {
57        let advancers = section.advancers() as f64;
58        let decliners = section.decliners().max(1) as f64;
59        self.has_emitted = true;
60        Some(advancers / decliners)
61    }
62
63    fn reset(&mut self) {
64        self.has_emitted = false;
65    }
66
67    fn warmup_period(&self) -> usize {
68        1
69    }
70
71    fn is_ready(&self) -> bool {
72        self.has_emitted
73    }
74
75    fn name(&self) -> &'static str {
76        "AdvanceDeclineRatio"
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::cross_section::Member;
84    use crate::traits::BatchExt;
85
86    fn section(up: usize, down: usize) -> CrossSection {
87        let mut members = Vec::new();
88        for _ in 0..up {
89            members.push(Member::new(1.0, 10.0, false, false));
90        }
91        for _ in 0..down {
92            members.push(Member::new(-1.0, 10.0, false, false));
93        }
94        // A non-empty unchanged member guarantees a valid universe when both
95        // counts are zero.
96        members.push(Member::new(0.0, 10.0, false, false));
97        CrossSection::new(members, 0).unwrap()
98    }
99
100    #[test]
101    fn accessors_and_metadata() {
102        let adr = AdvanceDeclineRatio::new();
103        assert_eq!(adr.name(), "AdvanceDeclineRatio");
104        assert_eq!(adr.warmup_period(), 1);
105        assert!(!adr.is_ready());
106    }
107
108    #[test]
109    fn first_tick_emits_ratio() {
110        let mut adr = AdvanceDeclineRatio::new();
111        assert_eq!(adr.update(section(3, 1)), Some(3.0));
112        assert!(adr.is_ready());
113    }
114
115    #[test]
116    fn zero_decliners_floors_denominator() {
117        let mut adr = AdvanceDeclineRatio::new();
118        // 4 advancers, 0 decliners -> 4 / max(0, 1) = 4.0.
119        assert_eq!(adr.update(section(4, 0)), Some(4.0));
120    }
121
122    #[test]
123    fn no_advancers_yields_zero() {
124        let mut adr = AdvanceDeclineRatio::new();
125        assert_eq!(adr.update(section(0, 5)), Some(0.0));
126    }
127
128    #[test]
129    fn reset_clears_state() {
130        let mut adr = AdvanceDeclineRatio::new();
131        adr.update(section(3, 1));
132        assert!(adr.is_ready());
133        adr.reset();
134        assert!(!adr.is_ready());
135        assert_eq!(adr.update(section(2, 1)), Some(2.0));
136    }
137
138    #[test]
139    fn batch_equals_streaming() {
140        let sections = vec![section(3, 1), section(4, 0), section(0, 5), section(2, 2)];
141        let mut a = AdvanceDeclineRatio::new();
142        let mut b = AdvanceDeclineRatio::new();
143        assert_eq!(
144            a.batch(&sections),
145            sections
146                .iter()
147                .map(|s| b.update(s.clone()))
148                .collect::<Vec<_>>()
149        );
150    }
151}