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