wickra_core/indicators/
breadth_thrust.rs1use crate::cross_section::CrossSection;
4use crate::error::Result;
5use crate::traits::Indicator;
6use crate::Sma;
7
8#[derive(Debug, Clone)]
35pub struct BreadthThrust {
36 sma: Sma,
37}
38
39impl BreadthThrust {
40 pub fn new(period: usize) -> Result<Self> {
46 Ok(Self {
47 sma: Sma::new(period)?,
48 })
49 }
50
51 #[must_use]
53 pub const fn period(&self) -> usize {
54 self.sma.period()
55 }
56}
57
58impl Indicator for BreadthThrust {
59 type Input = CrossSection;
60 type Output = f64;
61
62 #[inline]
63 fn update(&mut self, section: CrossSection) -> Option<f64> {
64 let advancers = section.advancers();
65 let decliners = section.decliners();
66 let participating = (advancers + decliners).max(1) as f64;
67 let share = advancers as f64 / participating;
68 self.sma.update(share)
69 }
70
71 fn reset(&mut self) {
72 self.sma.reset();
73 }
74
75 #[inline]
76 fn warmup_period(&self) -> usize {
77 self.sma.period()
78 }
79
80 #[inline]
81 fn is_ready(&self) -> bool {
82 self.sma.value().is_some()
83 }
84
85 #[inline]
86 fn name(&self) -> &'static str {
87 "BreadthThrust"
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::cross_section::Member;
95 use crate::error::Error;
96 use crate::traits::BatchExt;
97
98 fn section(up: usize, down: usize) -> CrossSection {
99 let mut members = Vec::new();
100 for _ in 0..up {
101 members.push(Member::new(1.0, 10.0, false, false));
102 }
103 for _ in 0..down {
104 members.push(Member::new(-1.0, 10.0, false, false));
105 }
106 members.push(Member::new(0.0, 10.0, false, false));
107 CrossSection::new(members, 0).unwrap()
108 }
109
110 #[test]
111 fn accessors_and_metadata() {
112 let bt = BreadthThrust::new(10).unwrap();
113 assert_eq!(bt.name(), "BreadthThrust");
114 assert_eq!(bt.warmup_period(), 10);
115 assert_eq!(bt.period(), 10);
116 assert!(!bt.is_ready());
117 }
118
119 #[test]
120 fn rejects_zero_period() {
121 assert!(matches!(BreadthThrust::new(0), Err(Error::PeriodZero)));
122 }
123
124 #[test]
125 fn averages_the_advancing_share() {
126 let mut bt = BreadthThrust::new(2).unwrap();
127 assert_eq!(bt.update(section(8, 2)), None);
129 let value = bt.update(section(6, 4)).unwrap();
131 assert!((value - 0.7).abs() < 1e-9);
132 assert!(bt.is_ready());
133 let value = bt.update(section(5, 5)).unwrap();
135 assert!((value - 0.55).abs() < 1e-9);
136 }
137
138 #[test]
139 fn empty_participation_floors_to_zero_share() {
140 let mut bt = BreadthThrust::new(1).unwrap();
141 assert_eq!(bt.update(section(0, 0)), Some(0.0));
143 }
144
145 #[test]
146 fn reset_clears_state() {
147 let mut bt = BreadthThrust::new(2).unwrap();
148 bt.update(section(8, 2));
149 bt.update(section(6, 4));
150 assert!(bt.is_ready());
151 bt.reset();
152 assert!(!bt.is_ready());
153 assert_eq!(bt.update(section(8, 2)), None);
154 }
155
156 #[test]
157 fn batch_equals_streaming() {
158 let sections = vec![section(8, 2), section(6, 4), section(5, 5), section(0, 0)];
159 let mut a = BreadthThrust::new(2).unwrap();
160 let mut b = BreadthThrust::new(2).unwrap();
161 assert_eq!(
162 a.batch(§ions),
163 sections
164 .iter()
165 .map(|s| b.update(s.clone()))
166 .collect::<Vec<_>>()
167 );
168 }
169}