wickra_core/indicators/
bullish_percent_index.rs1use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
41pub struct BullishPercentIndex {
42 has_emitted: bool,
43}
44
45impl BullishPercentIndex {
46 #[must_use]
48 pub const fn new() -> Self {
49 Self { has_emitted: false }
50 }
51}
52
53impl Indicator for BullishPercentIndex {
54 type Input = CrossSection;
55 type Output = f64;
56
57 fn update(&mut self, section: CrossSection) -> Option<f64> {
58 let bullish = section.on_buy_signal_count() as f64;
59 let total = section.members.len() as f64;
60 self.has_emitted = true;
61 Some(100.0 * bullish / total)
62 }
63
64 fn reset(&mut self) {
65 self.has_emitted = false;
66 }
67
68 fn warmup_period(&self) -> usize {
69 1
70 }
71
72 fn is_ready(&self) -> bool {
73 self.has_emitted
74 }
75
76 fn name(&self) -> &'static str {
77 "BullishPercentIndex"
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::cross_section::Member;
85 use crate::traits::BatchExt;
86
87 fn tick(bullish: usize, bearish: usize) -> CrossSection {
88 let mut members = Vec::new();
89 for _ in 0..bullish {
90 members.push(Member::with_signals(1.0, 10.0, false, false, false, true));
91 }
92 for _ in 0..bearish {
93 members.push(Member::with_signals(-1.0, 10.0, false, false, false, false));
94 }
95 CrossSection::new(members, 0).unwrap()
96 }
97
98 #[test]
99 fn accessors_and_metadata() {
100 let bpi = BullishPercentIndex::new();
101 assert_eq!(bpi.name(), "BullishPercentIndex");
102 assert_eq!(bpi.warmup_period(), 1);
103 assert!(!bpi.is_ready());
104 }
105
106 #[test]
107 fn first_tick_emits_percentage() {
108 let mut bpi = BullishPercentIndex::new();
109 assert_eq!(bpi.update(tick(2, 2)), Some(50.0));
110 assert!(bpi.is_ready());
111 }
112
113 #[test]
114 fn all_bullish_is_one_hundred() {
115 let mut bpi = BullishPercentIndex::new();
116 assert_eq!(bpi.update(tick(5, 0)), Some(100.0));
117 }
118
119 #[test]
120 fn none_bullish_is_zero() {
121 let mut bpi = BullishPercentIndex::new();
122 assert_eq!(bpi.update(tick(0, 4)), Some(0.0));
123 }
124
125 #[test]
126 fn reset_clears_state() {
127 let mut bpi = BullishPercentIndex::new();
128 bpi.update(tick(2, 2));
129 assert!(bpi.is_ready());
130 bpi.reset();
131 assert!(!bpi.is_ready());
132 }
133
134 #[test]
135 fn batch_equals_streaming() {
136 let sections = vec![tick(2, 2), tick(5, 0), tick(0, 4)];
137 let mut a = BullishPercentIndex::new();
138 let mut b = BullishPercentIndex::new();
139 assert_eq!(
140 a.batch(§ions),
141 sections
142 .iter()
143 .map(|s| b.update(s.clone()))
144 .collect::<Vec<_>>()
145 );
146 }
147}