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