wickra_core/indicators/
high_low_range.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
34pub struct HighLowRange {
35 has_emitted: bool,
36}
37
38impl HighLowRange {
39 pub const fn new() -> Self {
41 Self { has_emitted: false }
42 }
43}
44
45impl Indicator for HighLowRange {
46 type Input = Candle;
47 type Output = f64;
48
49 fn update(&mut self, candle: Candle) -> Option<f64> {
50 self.has_emitted = true;
51 let out = if candle.close == 0.0 {
52 0.0
54 } else {
55 (candle.high - candle.low) / candle.close
56 };
57 Some(out)
58 }
59
60 fn reset(&mut self) {
61 self.has_emitted = false;
62 }
63
64 fn warmup_period(&self) -> usize {
65 1
66 }
67
68 fn is_ready(&self) -> bool {
69 self.has_emitted
70 }
71
72 fn name(&self) -> &'static str {
73 "HighLowRange"
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::traits::BatchExt;
81 use approx::assert_relative_eq;
82
83 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
84 Candle::new(open, high, low, close, 1.0, ts).unwrap()
85 }
86
87 #[test]
88 fn reference_value() {
89 let mut hlr = HighLowRange::new();
91 assert_relative_eq!(
92 hlr.update(candle(99.0, 104.0, 98.0, 100.0, 0)).unwrap(),
93 0.06,
94 epsilon = 1e-12
95 );
96 }
97
98 #[test]
99 fn zero_range_bar_yields_zero() {
100 let mut hlr = HighLowRange::new();
102 assert_relative_eq!(
103 hlr.update(candle(10.0, 10.0, 10.0, 10.0, 0)).unwrap(),
104 0.0,
105 epsilon = 1e-12
106 );
107 }
108
109 #[test]
110 fn zero_close_yields_zero() {
111 let mut hlr = HighLowRange::new();
114 assert_relative_eq!(
115 hlr.update(candle(0.0, 1.0, 0.0, 0.0, 0)).unwrap(),
116 0.0,
117 epsilon = 1e-12
118 );
119 }
120
121 #[test]
122 fn output_is_non_negative() {
123 let candles: Vec<Candle> = (0..100)
124 .map(|i| {
125 let mid = 100.0 + (f64::from(i) * 0.2).sin() * 8.0;
126 candle(mid, mid + 3.0, mid - 3.0, mid, i64::from(i))
127 })
128 .collect();
129 let mut hlr = HighLowRange::new();
130 for v in hlr.batch(&candles).into_iter().flatten() {
131 assert!(v >= 0.0, "HighLowRange {v} must be non-negative");
132 }
133 }
134
135 #[test]
136 fn name_metadata() {
137 let hlr = HighLowRange::new();
138 assert_eq!(hlr.name(), "HighLowRange");
139 }
140
141 #[test]
142 fn emits_from_first_candle() {
143 let mut hlr = HighLowRange::new();
144 assert_eq!(hlr.warmup_period(), 1);
145 assert!(!hlr.is_ready());
146 assert!(hlr.update(candle(10.0, 11.0, 9.0, 10.0, 0)).is_some());
147 assert!(hlr.is_ready());
148 }
149
150 #[test]
151 fn reset_clears_state() {
152 let mut hlr = HighLowRange::new();
153 hlr.update(candle(10.0, 11.0, 9.0, 10.0, 0));
154 assert!(hlr.is_ready());
155 hlr.reset();
156 assert!(!hlr.is_ready());
157 }
158
159 #[test]
160 fn batch_equals_streaming() {
161 let candles: Vec<Candle> = (0..40)
162 .map(|i| {
163 let base = 100.0 + f64::from(i);
164 candle(base, base + 2.0, base - 2.0, base + 1.0, i64::from(i))
165 })
166 .collect();
167 let mut a = HighLowRange::new();
168 let mut b = HighLowRange::new();
169 assert_eq!(
170 a.batch(&candles),
171 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
172 );
173 }
174}