Skip to main content

wickra_core/indicators/
advance_decline.rs

1//! Advance/Decline Line — cumulative net advancing-minus-declining issues.
2
3use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6/// Advance/Decline Line (A/D Line) — the running cumulative sum of net advancing
7/// issues across a universe.
8///
9/// On each [`CrossSection`] tick the net breadth is `advancers - decliners`:
10/// the number of symbols with a positive price change minus the number with a
11/// negative change (unchanged symbols are ignored). The line accumulates this
12/// net value over time, so a rising line means advancers have persistently
13/// outnumbered decliners — broad participation — while a falling line warns that
14/// a rally is being carried by fewer and fewer names (a breadth divergence when
15/// the index itself is still rising).
16///
17/// `Input = CrossSection`, `Output = f64`. The line is defined from the very
18/// first tick, so `warmup_period == 1` and the indicator is ready after one
19/// update.
20///
21/// # Example
22///
23/// ```
24/// use wickra_core::{AdvanceDecline, CrossSection, Indicator, Member};
25///
26/// let mut ad = AdvanceDecline::new();
27/// // 3 advancers, 1 decliner -> net +2.
28/// let tick = CrossSection::new(
29///     vec![
30///         Member::new(1.0, 10.0, false, false),
31///         Member::new(0.5, 10.0, false, false),
32///         Member::new(2.0, 10.0, false, false),
33///         Member::new(-1.0, 10.0, false, false),
34///     ],
35///     0,
36/// )
37/// .unwrap();
38/// assert_eq!(ad.update(tick), Some(2.0));
39/// ```
40#[derive(Debug, Clone, Default)]
41pub struct AdvanceDecline {
42    line: f64,
43    has_emitted: bool,
44}
45
46impl AdvanceDecline {
47    /// Construct a new Advance/Decline Line indicator.
48    #[must_use]
49    pub const fn new() -> Self {
50        Self {
51            line: 0.0,
52            has_emitted: false,
53        }
54    }
55}
56
57impl Indicator for AdvanceDecline {
58    type Input = CrossSection;
59    type Output = f64;
60
61    #[inline]
62    fn update(&mut self, section: CrossSection) -> Option<f64> {
63        let net = section.advancers() as f64 - section.decliners() as f64;
64        self.line += net;
65        self.has_emitted = true;
66        Some(self.line)
67    }
68
69    fn reset(&mut self) {
70        self.line = 0.0;
71        self.has_emitted = false;
72    }
73
74    #[inline]
75    fn warmup_period(&self) -> usize {
76        1
77    }
78
79    #[inline]
80    fn is_ready(&self) -> bool {
81        self.has_emitted
82    }
83
84    #[inline]
85    fn name(&self) -> &'static str {
86        "AdvanceDecline"
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::cross_section::Member;
94    use crate::traits::BatchExt;
95
96    /// Build a cross-section with `up` advancers, `down` decliners and `flat`
97    /// unchanged symbols.
98    fn section(up: usize, down: usize, flat: usize) -> CrossSection {
99        let mut members = Vec::new();
100        for _ in 0..up {
101            members.push(Member::new(1.0, 10.0, false, false));
102        }
103        for _ in 0..down {
104            members.push(Member::new(-1.0, 10.0, false, false));
105        }
106        for _ in 0..flat {
107            members.push(Member::new(0.0, 10.0, false, false));
108        }
109        CrossSection::new(members, 0).unwrap()
110    }
111
112    #[test]
113    fn accessors_and_metadata() {
114        let ad = AdvanceDecline::new();
115        assert_eq!(ad.name(), "AdvanceDecline");
116        assert_eq!(ad.warmup_period(), 1);
117        assert!(!ad.is_ready());
118    }
119
120    #[test]
121    fn first_tick_emits_net_breadth() {
122        let mut ad = AdvanceDecline::new();
123        assert_eq!(ad.update(section(3, 1, 0)), Some(2.0));
124        assert!(ad.is_ready());
125    }
126
127    #[test]
128    fn line_accumulates_across_ticks() {
129        let mut ad = AdvanceDecline::new();
130        assert_eq!(ad.update(section(3, 1, 0)), Some(2.0)); // +2 -> 2
131        assert_eq!(ad.update(section(1, 4, 0)), Some(-1.0)); // -3 -> -1
132        assert_eq!(ad.update(section(2, 0, 0)), Some(1.0)); // +2 -> 1
133    }
134
135    #[test]
136    fn unchanged_symbols_are_ignored() {
137        let mut ad = AdvanceDecline::new();
138        // 2 up, 2 down, 5 unchanged -> net 0, line stays flat.
139        assert_eq!(ad.update(section(2, 2, 5)), Some(0.0));
140        assert_eq!(ad.update(section(2, 2, 5)), Some(0.0));
141    }
142
143    #[test]
144    fn reset_clears_state() {
145        let mut ad = AdvanceDecline::new();
146        ad.update(section(5, 0, 0));
147        assert!(ad.is_ready());
148        ad.reset();
149        assert!(!ad.is_ready());
150        // Line restarts from zero, not from the pre-reset value.
151        assert_eq!(ad.update(section(1, 0, 0)), Some(1.0));
152    }
153
154    #[test]
155    fn batch_equals_streaming() {
156        let sections = vec![
157            section(3, 1, 2),
158            section(1, 4, 0),
159            section(2, 2, 1),
160            section(5, 0, 3),
161        ];
162        let mut a = AdvanceDecline::new();
163        let mut b = AdvanceDecline::new();
164        assert_eq!(
165            a.batch(&sections),
166            sections
167                .iter()
168                .map(|s| b.update(s.clone()))
169                .collect::<Vec<_>>()
170        );
171    }
172}