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    #[inline]
58    fn update(&mut self, section: CrossSection) -> Option<f64> {
59        let net = section.advancers() as f64 - section.decliners() as f64;
60        self.has_emitted = true;
61        Some(net.abs())
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        "AbsoluteBreadthIndex"
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        members.push(Member::new(0.0, 10.0, false, false));
99        CrossSection::new(members, 0).unwrap()
100    }
101
102    #[test]
103    fn accessors_and_metadata() {
104        let abi = AbsoluteBreadthIndex::new();
105        assert_eq!(abi.name(), "AbsoluteBreadthIndex");
106        assert_eq!(abi.warmup_period(), 1);
107        assert!(!abi.is_ready());
108    }
109
110    #[test]
111    fn magnitude_ignores_direction() {
112        let mut abi = AbsoluteBreadthIndex::new();
113        assert_eq!(abi.update(section(2, 5)), Some(3.0));
114        // Same magnitude with the direction reversed.
115        let mut abi2 = AbsoluteBreadthIndex::new();
116        assert_eq!(abi2.update(section(5, 2)), Some(3.0));
117    }
118
119    #[test]
120    fn balanced_universe_yields_zero() {
121        let mut abi = AbsoluteBreadthIndex::new();
122        assert_eq!(abi.update(section(3, 3)), Some(0.0));
123        assert!(abi.is_ready());
124    }
125
126    #[test]
127    fn reset_clears_state() {
128        let mut abi = AbsoluteBreadthIndex::new();
129        abi.update(section(2, 5));
130        assert!(abi.is_ready());
131        abi.reset();
132        assert!(!abi.is_ready());
133    }
134
135    #[test]
136    fn batch_equals_streaming() {
137        let sections = vec![section(2, 5), section(5, 2), section(3, 3)];
138        let mut a = AbsoluteBreadthIndex::new();
139        let mut b = AbsoluteBreadthIndex::new();
140        assert_eq!(
141            a.batch(&sections),
142            sections
143                .iter()
144                .map(|s| b.update(s.clone()))
145                .collect::<Vec<_>>()
146        );
147    }
148}