wickra_core/indicators/
high_low_index.rs1use crate::cross_section::CrossSection;
4use crate::error::Result;
5use crate::traits::Indicator;
6use crate::Sma;
7
8#[derive(Debug, Clone)]
35pub struct HighLowIndex {
36 sma: Sma,
37}
38
39impl HighLowIndex {
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 HighLowIndex {
59 type Input = CrossSection;
60 type Output = f64;
61
62 fn update(&mut self, section: CrossSection) -> Option<f64> {
63 let new_highs = section.new_highs();
64 let new_lows = section.new_lows();
65 let extremes = (new_highs + new_lows).max(1) as f64;
66 let record_high_percent = 100.0 * new_highs as f64 / extremes;
67 self.sma.update(record_high_percent)
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 "HighLowIndex"
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 flags(highs: usize, lows: usize) -> CrossSection {
95 let mut members = Vec::new();
96 for _ in 0..highs {
97 members.push(Member::new(1.0, 10.0, true, false));
98 }
99 for _ in 0..lows {
100 members.push(Member::new(-1.0, 10.0, false, true));
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 hli = HighLowIndex::new(10).unwrap();
109 assert_eq!(hli.name(), "HighLowIndex");
110 assert_eq!(hli.warmup_period(), 10);
111 assert_eq!(hli.period(), 10);
112 assert!(!hli.is_ready());
113 }
114
115 #[test]
116 fn rejects_zero_period() {
117 assert!(matches!(HighLowIndex::new(0), Err(Error::PeriodZero)));
118 }
119
120 #[test]
121 fn averages_the_record_high_percent() {
122 let mut hli = HighLowIndex::new(2).unwrap();
123 assert_eq!(hli.update(flags(8, 2)), None);
125 let value = hli.update(flags(6, 4)).unwrap();
127 assert!((value - 70.0).abs() < 1e-9);
128 assert!(hli.is_ready());
129 }
130
131 #[test]
132 fn no_extremes_floors_to_zero_percent() {
133 let mut hli = HighLowIndex::new(1).unwrap();
134 assert_eq!(hli.update(flags(0, 0)), Some(0.0));
136 }
137
138 #[test]
139 fn reset_clears_state() {
140 let mut hli = HighLowIndex::new(2).unwrap();
141 hli.update(flags(8, 2));
142 hli.update(flags(6, 4));
143 assert!(hli.is_ready());
144 hli.reset();
145 assert!(!hli.is_ready());
146 assert_eq!(hli.update(flags(8, 2)), None);
147 }
148
149 #[test]
150 fn batch_equals_streaming() {
151 let sections = vec![flags(8, 2), flags(6, 4), flags(3, 7), flags(0, 0)];
152 let mut a = HighLowIndex::new(2).unwrap();
153 let mut b = HighLowIndex::new(2).unwrap();
154 assert_eq!(
155 a.batch(§ions),
156 sections
157 .iter()
158 .map(|s| b.update(s.clone()))
159 .collect::<Vec<_>>()
160 );
161 }
162}