1use crate::error::{Error, Result};
13use crate::ohlcv::Candle;
14use crate::traits::Indicator;
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct OpeningRangeOutput {
19 pub high: f64,
21 pub low: f64,
23 pub breakout_distance: f64,
26}
27
28#[derive(Debug, Clone)]
56pub struct OpeningRange {
57 period: usize,
58 bars_seen: usize,
59 high: f64,
60 low: f64,
61 last_close: f64,
62 locked: bool,
63 last: Option<OpeningRangeOutput>,
64}
65
66impl OpeningRange {
67 pub fn new(period: usize) -> Result<Self> {
73 if period == 0 {
74 return Err(Error::PeriodZero);
75 }
76 Ok(Self {
77 period,
78 bars_seen: 0,
79 high: f64::NEG_INFINITY,
80 low: f64::INFINITY,
81 last_close: 0.0,
82 locked: false,
83 last: None,
84 })
85 }
86
87 pub fn classic() -> Self {
89 Self::new(6).expect("classic OR period is valid")
90 }
91
92 pub const fn period(&self) -> usize {
94 self.period
95 }
96
97 pub const fn value(&self) -> Option<OpeningRangeOutput> {
99 self.last
100 }
101
102 pub const fn is_locked(&self) -> bool {
104 self.locked
105 }
106
107 fn snapshot(&self) -> OpeningRangeOutput {
108 let mid = f64::midpoint(self.high, self.low);
109 OpeningRangeOutput {
110 high: self.high,
111 low: self.low,
112 breakout_distance: self.last_close - mid,
113 }
114 }
115}
116
117impl Indicator for OpeningRange {
118 type Input = Candle;
119 type Output = OpeningRangeOutput;
120
121 fn update(&mut self, candle: Candle) -> Option<OpeningRangeOutput> {
122 if !self.locked {
123 if candle.high > self.high {
124 self.high = candle.high;
125 }
126 if candle.low < self.low {
127 self.low = candle.low;
128 }
129 self.bars_seen += 1;
130 if self.bars_seen >= self.period {
131 self.locked = true;
132 }
133 }
134 self.last_close = candle.close;
135 let out = self.snapshot();
136 self.last = Some(out);
137 Some(out)
138 }
139
140 fn reset(&mut self) {
141 self.bars_seen = 0;
142 self.high = f64::NEG_INFINITY;
143 self.low = f64::INFINITY;
144 self.last_close = 0.0;
145 self.locked = false;
146 self.last = None;
147 }
148
149 fn warmup_period(&self) -> usize {
150 1
151 }
152
153 fn is_ready(&self) -> bool {
154 self.bars_seen > 0
155 }
156
157 fn name(&self) -> &'static str {
158 "OpeningRange"
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::traits::BatchExt;
166 use approx::assert_relative_eq;
167
168 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
169 let open = f64::midpoint(high, low);
170 Candle::new(open, high, low, close, 10.0, ts).unwrap()
171 }
172
173 #[test]
174 fn rejects_zero_period() {
175 assert!(matches!(OpeningRange::new(0), Err(Error::PeriodZero)));
176 }
177
178 #[test]
179 fn accessors_and_metadata() {
180 let or = OpeningRange::new(6).unwrap();
181 assert_eq!(or.period(), 6);
182 assert_eq!(or.name(), "OpeningRange");
183 assert_eq!(or.warmup_period(), 1);
184 assert!(or.value().is_none());
185 assert!(!or.is_locked());
186 }
187
188 #[test]
189 fn classic_is_constructible() {
190 let or = OpeningRange::classic();
191 assert_eq!(or.period(), 6);
192 }
193
194 #[test]
195 fn tracks_range_during_window() {
196 let mut or = OpeningRange::new(3).unwrap();
197 let o1 = or.update(c(102.0, 100.0, 101.0, 0)).unwrap();
198 assert_relative_eq!(o1.high, 102.0);
199 assert_relative_eq!(o1.low, 100.0);
200 assert_relative_eq!(o1.breakout_distance, 0.0, epsilon = 1e-12);
202 let o2 = or.update(c(105.0, 99.0, 104.0, 1)).unwrap();
203 assert_relative_eq!(o2.high, 105.0);
204 assert_relative_eq!(o2.low, 99.0);
205 assert_relative_eq!(o2.breakout_distance, 2.0, epsilon = 1e-12);
207 }
208
209 #[test]
210 fn locks_after_period_and_breakout_reflects_close_minus_mid() {
211 let mut or = OpeningRange::new(2).unwrap();
212 or.update(c(102.0, 100.0, 101.0, 0));
213 or.update(c(103.0, 101.0, 102.0, 1));
214 assert!(or.is_locked());
215 let after = or.update(c(200.0, 50.0, 105.0, 2)).unwrap();
218 assert_relative_eq!(after.high, 103.0);
219 assert_relative_eq!(after.low, 100.0);
220 assert_relative_eq!(after.breakout_distance, 3.5, epsilon = 1e-12);
221 }
222
223 #[test]
224 fn breakout_distance_is_negative_below_range() {
225 let mut or = OpeningRange::new(2).unwrap();
226 or.update(c(102.0, 100.0, 101.0, 0));
227 or.update(c(103.0, 101.0, 102.0, 1));
228 let out = or.update(c(110.0, 89.0, 90.0, 2)).unwrap();
230 assert_relative_eq!(out.breakout_distance, -11.5, epsilon = 1e-12);
231 }
232
233 #[test]
234 fn reset_unlocks_and_clears_state() {
235 let mut or = OpeningRange::new(2).unwrap();
236 or.update(c(102.0, 100.0, 101.0, 0));
237 or.update(c(103.0, 101.0, 102.0, 1));
238 assert!(or.is_locked());
239 or.reset();
240 assert!(!or.is_locked());
241 assert!(!or.is_ready());
242 let o = or.update(c(50.0, 49.0, 49.5, 2)).unwrap();
243 assert_relative_eq!(o.high, 50.0);
244 assert_relative_eq!(o.low, 49.0);
245 }
246
247 #[test]
248 fn batch_equals_streaming() {
249 let candles: Vec<Candle> = (0..20)
250 .map(|i| {
251 let base = 100.0 + i as f64 * 0.25;
252 c(base + 1.0, base - 1.0, base, i)
253 })
254 .collect();
255 let mut a = OpeningRange::new(5).unwrap();
256 let mut b = OpeningRange::new(5).unwrap();
257 assert_eq!(
258 a.batch(&candles),
259 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
260 );
261 }
262
263 #[test]
264 fn is_ready_after_first_bar() {
265 let mut or = OpeningRange::new(5).unwrap();
266 assert!(!or.is_ready());
267 or.update(c(101.0, 99.0, 100.0, 0));
268 assert!(or.is_ready());
269 }
270}