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    #[inline]
57    fn update(&mut self, section: CrossSection) -> Option<f64> {
58        let above = section.above_ma_count() as f64;
59        let total = section.members.len() as f64;
60        self.has_emitted = true;
61        Some(100.0 * above / total)
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        "PercentAboveMa"
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 tick(above: usize, below: usize) -> CrossSection {
91        let mut members = Vec::new();
92        for _ in 0..above {
93            members.push(Member::with_signals(1.0, 10.0, false, false, true, false));
94        }
95        for _ in 0..below {
96            members.push(Member::with_signals(-1.0, 10.0, false, false, false, false));
97        }
98        CrossSection::new(members, 0).unwrap()
99    }
100
101    #[test]
102    fn accessors_and_metadata() {
103        let pct = PercentAboveMa::new();
104        assert_eq!(pct.name(), "PercentAboveMa");
105        assert_eq!(pct.warmup_period(), 1);
106        assert!(!pct.is_ready());
107    }
108
109    #[test]
110    fn first_tick_emits_percentage() {
111        let mut pct = PercentAboveMa::new();
112        assert_eq!(pct.update(tick(3, 1)), Some(75.0));
113        assert!(pct.is_ready());
114    }
115
116    #[test]
117    fn all_above_is_one_hundred() {
118        let mut pct = PercentAboveMa::new();
119        assert_eq!(pct.update(tick(4, 0)), Some(100.0));
120    }
121
122    #[test]
123    fn none_above_is_zero() {
124        let mut pct = PercentAboveMa::new();
125        assert_eq!(pct.update(tick(0, 5)), Some(0.0));
126    }
127
128    #[test]
129    fn reset_clears_state() {
130        let mut pct = PercentAboveMa::new();
131        pct.update(tick(3, 1));
132        assert!(pct.is_ready());
133        pct.reset();
134        assert!(!pct.is_ready());
135    }
136
137    #[test]
138    fn batch_equals_streaming() {
139        let sections = vec![tick(3, 1), tick(4, 0), tick(0, 5)];
140        let mut a = PercentAboveMa::new();
141        let mut b = PercentAboveMa::new();
142        assert_eq!(
143            a.batch(&sections),
144            sections
145                .iter()
146                .map(|s| b.update(s.clone()))
147                .collect::<Vec<_>>()
148        );
149    }
150}