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