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 #[inline]
63 fn update(&mut self, section: CrossSection) -> Option<f64> {
64 let new_highs = section.new_highs();
65 let new_lows = section.new_lows();
66 let extremes = (new_highs + new_lows).max(1) as f64;
67 let record_high_percent = 100.0 * new_highs as f64 / extremes;
68 self.sma.update(record_high_percent)
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 "HighLowIndex"
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 flags(highs: usize, lows: usize) -> CrossSection {
99 let mut members = Vec::new();
100 for _ in 0..highs {
101 members.push(Member::new(1.0, 10.0, true, false));
102 }
103 for _ in 0..lows {
104 members.push(Member::new(-1.0, 10.0, false, true));
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 hli = HighLowIndex::new(10).unwrap();
113 assert_eq!(hli.name(), "HighLowIndex");
114 assert_eq!(hli.warmup_period(), 10);
115 assert_eq!(hli.period(), 10);
116 assert!(!hli.is_ready());
117 }
118
119 #[test]
120 fn rejects_zero_period() {
121 assert!(matches!(HighLowIndex::new(0), Err(Error::PeriodZero)));
122 }
123
124 #[test]
125 fn averages_the_record_high_percent() {
126 let mut hli = HighLowIndex::new(2).unwrap();
127 assert_eq!(hli.update(flags(8, 2)), None);
129 let value = hli.update(flags(6, 4)).unwrap();
131 assert!((value - 70.0).abs() < 1e-9);
132 assert!(hli.is_ready());
133 }
134
135 #[test]
136 fn no_extremes_floors_to_zero_percent() {
137 let mut hli = HighLowIndex::new(1).unwrap();
138 assert_eq!(hli.update(flags(0, 0)), Some(0.0));
140 }
141
142 #[test]
143 fn reset_clears_state() {
144 let mut hli = HighLowIndex::new(2).unwrap();
145 hli.update(flags(8, 2));
146 hli.update(flags(6, 4));
147 assert!(hli.is_ready());
148 hli.reset();
149 assert!(!hli.is_ready());
150 assert_eq!(hli.update(flags(8, 2)), None);
151 }
152
153 #[test]
154 fn batch_equals_streaming() {
155 let sections = vec![flags(8, 2), flags(6, 4), flags(3, 7), flags(0, 0)];
156 let mut a = HighLowIndex::new(2).unwrap();
157 let mut b = HighLowIndex::new(2).unwrap();
158 assert_eq!(
159 a.batch(§ions),
160 sections
161 .iter()
162 .map(|s| b.update(s.clone()))
163 .collect::<Vec<_>>()
164 );
165 }
166}