Skip to main content

wickra_core/indicators/
absolute_breadth_index.rs

1//! Absolute Breadth Index — the magnitude of net advancing-minus-declining issues.
2
3use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6/// Absolute Breadth Index (ABI) — the absolute value of net advancing issues,
7/// `|advancers - decliners|`.
8///
9/// The ABI ignores the *direction* of breadth and measures only its *magnitude*:
10/// a high reading means the universe moved decisively one way or the other (high
11/// internal activity / volatility), while a low reading means advances and
12/// declines were nearly balanced (a quiet, directionless market). It is sometimes
13/// called a "market thermometer" because elevated readings often cluster around
14/// turning points.
15///
16/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
17///
18/// # Example
19///
20/// ```
21/// use wickra_core::{AbsoluteBreadthIndex, CrossSection, Indicator, Member};
22///
23/// let mut abi = AbsoluteBreadthIndex::new();
24/// // 2 advancers, 5 decliners -> |2 - 5| = 3.
25/// let tick = CrossSection::new(
26///     vec![
27///         Member::new(1.0, 10.0, false, false),
28///         Member::new(1.0, 10.0, false, false),
29///         Member::new(-1.0, 10.0, false, false),
30///         Member::new(-1.0, 10.0, false, false),
31///         Member::new(-1.0, 10.0, false, false),
32///         Member::new(-1.0, 10.0, false, false),
33///         Member::new(-1.0, 10.0, false, false),
34///     ],
35///     0,
36/// )
37/// .unwrap();
38/// assert_eq!(abi.update(tick), Some(3.0));
39/// ```
40#[derive(Debug, Clone, Default)]
41pub struct AbsoluteBreadthIndex {
42    has_emitted: bool,
43}
44
45impl AbsoluteBreadthIndex {
46    /// Construct a new Absolute Breadth Index indicator.
47    #[must_use]
48    pub const fn new() -> Self {
49        Self { has_emitted: false }
50    }
51}
52
53impl Indicator for AbsoluteBreadthIndex {
54    type Input = CrossSection;
55    type Output = f64;
56
57    fn update(&mut self, section: CrossSection) -> Option<f64> {
58        let net = section.advancers() as f64 - section.decliners() as f64;
59        self.has_emitted = true;
60        Some(net.abs())
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        "AbsoluteBreadthIndex"
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        members.push(Member::new(0.0, 10.0, false, false));
95        CrossSection::new(members, 0).unwrap()
96    }
97
98    #[test]
99    fn accessors_and_metadata() {
100        let abi = AbsoluteBreadthIndex::new();
101        assert_eq!(abi.name(), "AbsoluteBreadthIndex");
102        assert_eq!(abi.warmup_period(), 1);
103        assert!(!abi.is_ready());
104    }
105
106    #[test]
107    fn magnitude_ignores_direction() {
108        let mut abi = AbsoluteBreadthIndex::new();
109        assert_eq!(abi.update(section(2, 5)), Some(3.0));
110        // Same magnitude with the direction reversed.
111        let mut abi2 = AbsoluteBreadthIndex::new();
112        assert_eq!(abi2.update(section(5, 2)), Some(3.0));
113    }
114
115    #[test]
116    fn balanced_universe_yields_zero() {
117        let mut abi = AbsoluteBreadthIndex::new();
118        assert_eq!(abi.update(section(3, 3)), Some(0.0));
119        assert!(abi.is_ready());
120    }
121
122    #[test]
123    fn reset_clears_state() {
124        let mut abi = AbsoluteBreadthIndex::new();
125        abi.update(section(2, 5));
126        assert!(abi.is_ready());
127        abi.reset();
128        assert!(!abi.is_ready());
129    }
130
131    #[test]
132    fn batch_equals_streaming() {
133        let sections = vec![section(2, 5), section(5, 2), section(3, 3)];
134        let mut a = AbsoluteBreadthIndex::new();
135        let mut b = AbsoluteBreadthIndex::new();
136        assert_eq!(
137            a.batch(&sections),
138            sections
139                .iter()
140                .map(|s| b.update(s.clone()))
141                .collect::<Vec<_>>()
142        );
143    }
144}