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