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 fn update(&mut self, section: CrossSection) -> Option<f64> {
63 let advancers = section.advancers();
64 let decliners = section.decliners();
65 let participating = (advancers + decliners).max(1) as f64;
66 let share = advancers as f64 / participating;
67 self.sma.update(share)
68 }
69
70 fn reset(&mut self) {
71 self.sma.reset();
72 }
73
74 fn warmup_period(&self) -> usize {
75 self.sma.period()
76 }
77
78 fn is_ready(&self) -> bool {
79 self.sma.value().is_some()
80 }
81
82 fn name(&self) -> &'static str {
83 "BreadthThrust"
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::cross_section::Member;
91 use crate::error::Error;
92 use crate::traits::BatchExt;
93
94 fn section(up: usize, down: usize) -> CrossSection {
95 let mut members = Vec::new();
96 for _ in 0..up {
97 members.push(Member::new(1.0, 10.0, false, false));
98 }
99 for _ in 0..down {
100 members.push(Member::new(-1.0, 10.0, false, false));
101 }
102 members.push(Member::new(0.0, 10.0, false, false));
103 CrossSection::new(members, 0).unwrap()
104 }
105
106 #[test]
107 fn accessors_and_metadata() {
108 let bt = BreadthThrust::new(10).unwrap();
109 assert_eq!(bt.name(), "BreadthThrust");
110 assert_eq!(bt.warmup_period(), 10);
111 assert_eq!(bt.period(), 10);
112 assert!(!bt.is_ready());
113 }
114
115 #[test]
116 fn rejects_zero_period() {
117 assert!(matches!(BreadthThrust::new(0), Err(Error::PeriodZero)));
118 }
119
120 #[test]
121 fn averages_the_advancing_share() {
122 let mut bt = BreadthThrust::new(2).unwrap();
123 assert_eq!(bt.update(section(8, 2)), None);
125 let value = bt.update(section(6, 4)).unwrap();
127 assert!((value - 0.7).abs() < 1e-9);
128 assert!(bt.is_ready());
129 let value = bt.update(section(5, 5)).unwrap();
131 assert!((value - 0.55).abs() < 1e-9);
132 }
133
134 #[test]
135 fn empty_participation_floors_to_zero_share() {
136 let mut bt = BreadthThrust::new(1).unwrap();
137 assert_eq!(bt.update(section(0, 0)), Some(0.0));
139 }
140
141 #[test]
142 fn reset_clears_state() {
143 let mut bt = BreadthThrust::new(2).unwrap();
144 bt.update(section(8, 2));
145 bt.update(section(6, 4));
146 assert!(bt.is_ready());
147 bt.reset();
148 assert!(!bt.is_ready());
149 assert_eq!(bt.update(section(8, 2)), None);
150 }
151
152 #[test]
153 fn batch_equals_streaming() {
154 let sections = vec![section(8, 2), section(6, 4), section(5, 5), section(0, 0)];
155 let mut a = BreadthThrust::new(2).unwrap();
156 let mut b = BreadthThrust::new(2).unwrap();
157 assert_eq!(
158 a.batch(§ions),
159 sections
160 .iter()
161 .map(|s| b.update(s.clone()))
162 .collect::<Vec<_>>()
163 );
164 }
165}