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    fn update(&mut self, section: CrossSection) -> Option<f64> {
62        let net = section.advancers() as f64 - section.decliners() as f64;
63        self.line += net;
64        self.has_emitted = true;
65        Some(self.line)
66    }
67
68    fn reset(&mut self) {
69        self.line = 0.0;
70        self.has_emitted = false;
71    }
72
73    fn warmup_period(&self) -> usize {
74        1
75    }
76
77    fn is_ready(&self) -> bool {
78        self.has_emitted
79    }
80
81    fn name(&self) -> &'static str {
82        "AdvanceDecline"
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::cross_section::Member;
90    use crate::traits::BatchExt;
91
92    /// Build a cross-section with `up` advancers, `down` decliners and `flat`
93    /// unchanged symbols.
94    fn section(up: usize, down: usize, flat: usize) -> CrossSection {
95        let mut members = Vec::new();
96        for _ in 0..up {
97            members.push(Member::new(1.0, 10.0, false, false));
98        }
99        for _ in 0..down {
100            members.push(Member::new(-1.0, 10.0, false, false));
101        }
102        for _ in 0..flat {
103            members.push(Member::new(0.0, 10.0, false, false));
104        }
105        CrossSection::new(members, 0).unwrap()
106    }
107
108    #[test]
109    fn accessors_and_metadata() {
110        let ad = AdvanceDecline::new();
111        assert_eq!(ad.name(), "AdvanceDecline");
112        assert_eq!(ad.warmup_period(), 1);
113        assert!(!ad.is_ready());
114    }
115
116    #[test]
117    fn first_tick_emits_net_breadth() {
118        let mut ad = AdvanceDecline::new();
119        assert_eq!(ad.update(section(3, 1, 0)), Some(2.0));
120        assert!(ad.is_ready());
121    }
122
123    #[test]
124    fn line_accumulates_across_ticks() {
125        let mut ad = AdvanceDecline::new();
126        assert_eq!(ad.update(section(3, 1, 0)), Some(2.0)); // +2 -> 2
127        assert_eq!(ad.update(section(1, 4, 0)), Some(-1.0)); // -3 -> -1
128        assert_eq!(ad.update(section(2, 0, 0)), Some(1.0)); // +2 -> 1
129    }
130
131    #[test]
132    fn unchanged_symbols_are_ignored() {
133        let mut ad = AdvanceDecline::new();
134        // 2 up, 2 down, 5 unchanged -> net 0, line stays flat.
135        assert_eq!(ad.update(section(2, 2, 5)), Some(0.0));
136        assert_eq!(ad.update(section(2, 2, 5)), Some(0.0));
137    }
138
139    #[test]
140    fn reset_clears_state() {
141        let mut ad = AdvanceDecline::new();
142        ad.update(section(5, 0, 0));
143        assert!(ad.is_ready());
144        ad.reset();
145        assert!(!ad.is_ready());
146        // Line restarts from zero, not from the pre-reset value.
147        assert_eq!(ad.update(section(1, 0, 0)), Some(1.0));
148    }
149
150    #[test]
151    fn batch_equals_streaming() {
152        let sections = vec![
153            section(3, 1, 2),
154            section(1, 4, 0),
155            section(2, 2, 1),
156            section(5, 0, 3),
157        ];
158        let mut a = AdvanceDecline::new();
159        let mut b = AdvanceDecline::new();
160        assert_eq!(
161            a.batch(&sections),
162            sections
163                .iter()
164                .map(|s| b.update(s.clone()))
165                .collect::<Vec<_>>()
166        );
167    }
168}