wickra_core/indicators/
average_daily_range.rs1use std::collections::VecDeque;
5
6use crate::calendar::civil_from_timestamp;
7use crate::error::{Error, Result};
8use crate::ohlcv::Candle;
9use crate::traits::Indicator;
10
11#[derive(Debug, Clone)]
35pub struct AverageDailyRange {
36 period: usize,
37 utc_offset_minutes: i32,
38 day_key: Option<(i64, u32, u32)>,
39 cur_high: f64,
40 cur_low: f64,
41 completed: VecDeque<f64>,
42 sum: f64,
43}
44
45impl AverageDailyRange {
46 pub fn new(period: usize, utc_offset_minutes: i32) -> Result<Self> {
59 if period == 0 {
60 return Err(Error::PeriodZero);
61 }
62 if period > crate::error::MAX_PERIOD {
63 return Err(Error::InvalidPeriod {
64 message: crate::error::PERIOD_ABOVE_MAX,
65 });
66 }
67 Ok(Self {
68 period,
69 utc_offset_minutes,
70 day_key: None,
71 cur_high: f64::NEG_INFINITY,
72 cur_low: f64::INFINITY,
73 completed: VecDeque::with_capacity(period),
74 sum: 0.0,
75 })
76 }
77
78 pub const fn params(&self) -> (usize, i32) {
80 (self.period, self.utc_offset_minutes)
81 }
82
83 pub fn value(&self) -> Option<f64> {
85 if self.completed.is_empty() {
86 None
87 } else {
88 Some(self.sum / self.completed.len() as f64)
89 }
90 }
91}
92
93impl Indicator for AverageDailyRange {
94 type Input = Candle;
95 type Output = f64;
96
97 #[inline]
98 fn update(&mut self, candle: Candle) -> Option<f64> {
99 let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
100 let key = (civil.year, civil.month, civil.day);
101 match self.day_key {
102 Some(prev) if prev == key => {
103 if candle.high > self.cur_high {
104 self.cur_high = candle.high;
105 }
106 if candle.low < self.cur_low {
107 self.cur_low = candle.low;
108 }
109 }
110 Some(_) => {
111 let range = self.cur_high - self.cur_low;
112 self.completed.push_back(range);
113 self.sum += range;
114 if self.completed.len() > self.period {
115 self.sum -= self
116 .completed
117 .pop_front()
118 .expect("len > period implies a front element");
119 }
120 self.day_key = Some(key);
121 self.cur_high = candle.high;
122 self.cur_low = candle.low;
123 }
124 None => {
125 self.day_key = Some(key);
126 self.cur_high = candle.high;
127 self.cur_low = candle.low;
128 }
129 }
130 self.value()
131 }
132
133 fn reset(&mut self) {
134 self.day_key = None;
135 self.cur_high = f64::NEG_INFINITY;
136 self.cur_low = f64::INFINITY;
137 self.completed.clear();
138 self.sum = 0.0;
139 }
140
141 #[inline]
142 fn warmup_period(&self) -> usize {
143 self.period
144 }
145
146 #[inline]
147 fn is_ready(&self) -> bool {
148 !self.completed.is_empty()
149 }
150
151 #[inline]
152 fn name(&self) -> &'static str {
153 "AverageDailyRange"
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::traits::BatchExt;
161 use approx::assert_relative_eq;
162
163 const HOUR: i64 = 3_600_000;
164 const DAY: i64 = 24 * HOUR;
165
166 fn c(high: f64, low: f64, ts: i64) -> Candle {
167 let mid = f64::midpoint(high, low);
168 Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
169 }
170
171 #[test]
172 fn rejects_zero_period() {
173 assert!(matches!(
174 AverageDailyRange::new(0, 0),
175 Err(Error::PeriodZero)
176 ));
177 }
178
179 #[test]
180 fn metadata_and_accessors() {
181 let adr = AverageDailyRange::new(5, -60).unwrap();
182 assert_eq!(adr.params(), (5, -60));
183 assert_eq!(adr.name(), "AverageDailyRange");
184 assert_eq!(adr.warmup_period(), 5);
185 assert!(!adr.is_ready());
186 assert!(adr.value().is_none());
187 }
188
189 #[test]
190 fn averages_completed_day_ranges() {
191 let mut adr = AverageDailyRange::new(3, 0).unwrap();
192 assert!(adr.update(c(110.0, 100.0, 0)).is_none());
194 assert!(adr.update(c(108.0, 104.0, HOUR)).is_none());
195 let v = adr.update(c(120.0, 110.0, DAY)).unwrap();
197 assert_relative_eq!(v, 10.0);
198 assert!(adr.is_ready());
199 let v = adr.update(c(130.0, 100.0, 2 * DAY)).unwrap();
201 assert_relative_eq!(v, 10.0);
202 }
203
204 #[test]
205 fn rolls_off_oldest_day_beyond_period() {
206 let mut adr = AverageDailyRange::new(2, 0).unwrap();
207 adr.update(c(110.0, 100.0, 0)); let v = adr.update(c(125.0, 110.0, DAY)).unwrap(); assert_relative_eq!(v, 10.0);
210 let v = adr.update(c(130.0, 110.0, 2 * DAY)).unwrap();
212 assert_relative_eq!(v, 12.5);
213 let v = adr.update(c(140.0, 138.0, 3 * DAY)).unwrap();
215 assert_relative_eq!(v, 17.5);
216 }
217
218 #[test]
219 fn reset_clears_state() {
220 let mut adr = AverageDailyRange::new(2, 0).unwrap();
221 adr.update(c(110.0, 100.0, 0));
222 adr.update(c(120.0, 110.0, DAY));
223 adr.reset();
224 assert!(!adr.is_ready());
225 assert!(adr.value().is_none());
226 assert!(adr.update(c(50.0, 40.0, 2 * DAY)).is_none());
227 }
228
229 #[test]
230 fn batch_equals_streaming() {
231 let candles: Vec<Candle> = (0..60)
232 .map(|i| {
233 c(
234 110.0 + f64::from(i % 5),
235 100.0 - f64::from(i % 3),
236 i64::from(i) * 6 * HOUR,
237 )
238 })
239 .collect();
240 let mut a = AverageDailyRange::new(4, 0).unwrap();
241 let mut b = AverageDailyRange::new(4, 0).unwrap();
242 assert_eq!(
243 a.batch(&candles),
244 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
245 );
246 }
247}