Skip to main content

wickra_core/indicators/
percent_above_ma.rs

1//! Percent Above Moving Average — share of a universe trading above its MA.
2
3use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6/// Percent Above Moving Average — the percentage of symbols in a universe that
7/// are trading above their reference moving average.
8///
9/// On each [`CrossSection`] tick the value is `100 * above_ma_count / universe
10/// size`, read from the per-symbol `above_ma` flag (the caller decides which MA —
11/// 50-day, 200-day — when it builds the tick). It is a bounded `0..=100` breadth
12/// gauge: readings near 100 mean almost the whole universe is in an uptrend
13/// (broad participation, but also a potential overbought extreme), readings near
14/// zero mark washouts. Crosses of the 50 line are read as bull/bear regime flips.
15///
16/// `Input = CrossSection`, `Output = f64` (a percentage in `0..=100`),
17/// `warmup_period == 1`. The universe is non-empty by construction, so the share
18/// is always defined.
19///
20/// # Example
21///
22/// ```
23/// use wickra_core::{CrossSection, Indicator, Member, PercentAboveMa};
24///
25/// let mut pct = PercentAboveMa::new();
26/// // 3 of 4 symbols above their MA -> 75%.
27/// let tick = CrossSection::new(
28///     vec![
29///         Member::with_signals(1.0, 10.0, false, false, true, false),
30///         Member::with_signals(1.0, 10.0, false, false, true, false),
31///         Member::with_signals(-1.0, 10.0, false, false, true, false),
32///         Member::with_signals(-1.0, 10.0, false, false, false, false),
33///     ],
34///     0,
35/// )
36/// .unwrap();
37/// assert_eq!(pct.update(tick), Some(75.0));
38/// ```
39#[derive(Debug, Clone, Default)]
40pub struct PercentAboveMa {
41    has_emitted: bool,
42}
43
44impl PercentAboveMa {
45    /// Construct a new Percent Above Moving Average indicator.
46    #[must_use]
47    pub const fn new() -> Self {
48        Self { has_emitted: false }
49    }
50}
51
52impl Indicator for PercentAboveMa {
53    type Input = CrossSection;
54    type Output = f64;
55
56    fn update(&mut self, section: CrossSection) -> Option<f64> {
57        let above = section.above_ma_count() as f64;
58        let total = section.members.len() as f64;
59        self.has_emitted = true;
60        Some(100.0 * above / total)
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        "PercentAboveMa"
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 tick(above: usize, below: usize) -> CrossSection {
87        let mut members = Vec::new();
88        for _ in 0..above {
89            members.push(Member::with_signals(1.0, 10.0, false, false, true, false));
90        }
91        for _ in 0..below {
92            members.push(Member::with_signals(-1.0, 10.0, false, false, false, false));
93        }
94        CrossSection::new(members, 0).unwrap()
95    }
96
97    #[test]
98    fn accessors_and_metadata() {
99        let pct = PercentAboveMa::new();
100        assert_eq!(pct.name(), "PercentAboveMa");
101        assert_eq!(pct.warmup_period(), 1);
102        assert!(!pct.is_ready());
103    }
104
105    #[test]
106    fn first_tick_emits_percentage() {
107        let mut pct = PercentAboveMa::new();
108        assert_eq!(pct.update(tick(3, 1)), Some(75.0));
109        assert!(pct.is_ready());
110    }
111
112    #[test]
113    fn all_above_is_one_hundred() {
114        let mut pct = PercentAboveMa::new();
115        assert_eq!(pct.update(tick(4, 0)), Some(100.0));
116    }
117
118    #[test]
119    fn none_above_is_zero() {
120        let mut pct = PercentAboveMa::new();
121        assert_eq!(pct.update(tick(0, 5)), Some(0.0));
122    }
123
124    #[test]
125    fn reset_clears_state() {
126        let mut pct = PercentAboveMa::new();
127        pct.update(tick(3, 1));
128        assert!(pct.is_ready());
129        pct.reset();
130        assert!(!pct.is_ready());
131    }
132
133    #[test]
134    fn batch_equals_streaming() {
135        let sections = vec![tick(3, 1), tick(4, 0), tick(0, 5)];
136        let mut a = PercentAboveMa::new();
137        let mut b = PercentAboveMa::new();
138        assert_eq!(
139            a.batch(&sections),
140            sections
141                .iter()
142                .map(|s| b.update(s.clone()))
143                .collect::<Vec<_>>()
144        );
145    }
146}