wickra_core/indicators/
range_bars.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::BarBuilder;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct RangeBar {
10 pub open: f64,
12 pub close: f64,
14 pub direction: i8,
16}
17
18#[derive(Debug, Clone)]
50pub struct RangeBars {
51 range: f64,
52 anchor: Option<f64>,
53}
54
55impl RangeBars {
56 pub fn new(range: f64) -> Result<Self> {
62 if !range.is_finite() || range <= 0.0 {
63 return Err(Error::InvalidPeriod {
64 message: "range must be finite and positive",
65 });
66 }
67 Ok(Self {
68 range,
69 anchor: None,
70 })
71 }
72
73 pub const fn range(&self) -> f64 {
75 self.range
76 }
77
78 pub const fn anchor(&self) -> Option<f64> {
81 self.anchor
82 }
83}
84
85impl BarBuilder for RangeBars {
86 type Bar = RangeBar;
87
88 #[inline]
89 fn update(&mut self, candle: Candle) -> Vec<RangeBar> {
90 let close = candle.close;
91 let Some(mut anchor) = self.anchor else {
92 self.anchor = Some(close);
93 return Vec::new();
94 };
95 let range = self.range;
96 let mut bars = Vec::new();
97 while close >= anchor + range {
98 bars.push(RangeBar {
99 open: anchor,
100 close: anchor + range,
101 direction: 1,
102 });
103 anchor += range;
104 }
105 while close <= anchor - range {
106 bars.push(RangeBar {
107 open: anchor,
108 close: anchor - range,
109 direction: -1,
110 });
111 anchor -= range;
112 }
113 self.anchor = Some(anchor);
114 bars
115 }
116
117 fn reset(&mut self) {
118 self.anchor = None;
119 }
120
121 #[inline]
122 fn name(&self) -> &'static str {
123 "RangeBars"
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use approx::assert_relative_eq;
131
132 fn flat(price: f64) -> Candle {
133 Candle::new(price, price, price, price, 1.0, 0).unwrap()
134 }
135
136 #[test]
137 fn rejects_invalid_range() {
138 assert!(matches!(
139 RangeBars::new(0.0),
140 Err(Error::InvalidPeriod { .. })
141 ));
142 assert!(matches!(
143 RangeBars::new(-1.0),
144 Err(Error::InvalidPeriod { .. })
145 ));
146 assert!(matches!(
147 RangeBars::new(f64::NAN),
148 Err(Error::InvalidPeriod { .. })
149 ));
150 }
151
152 #[test]
153 fn accessors_and_metadata() {
154 let bars = RangeBars::new(2.5).unwrap();
155 assert_eq!(bars.name(), "RangeBars");
156 assert_relative_eq!(bars.range(), 2.5, epsilon = 1e-12);
157 assert_eq!(bars.anchor(), None);
158 }
159
160 #[test]
161 fn first_candle_seeds_without_bar() {
162 let mut bars = RangeBars::new(1.0).unwrap();
163 assert!(bars.update(flat(10.0)).is_empty());
164 assert_eq!(bars.anchor(), Some(10.0));
165 }
166
167 #[test]
168 fn up_move_prints_aligned_bars() {
169 let mut bars = RangeBars::new(1.0).unwrap();
170 bars.update(flat(10.0));
171 let up = bars.update(flat(13.0));
172 assert_eq!(up.len(), 3);
173 assert_relative_eq!(up[0].open, 10.0, epsilon = 1e-12);
174 assert_relative_eq!(up[2].close, 13.0, epsilon = 1e-12);
175 assert!(up.iter().all(|b| b.direction == 1));
176 assert_eq!(bars.anchor(), Some(13.0));
177 }
178
179 #[test]
180 fn down_move_prints_aligned_bars() {
181 let mut bars = RangeBars::new(1.0).unwrap();
182 bars.update(flat(10.0));
183 let down = bars.update(flat(7.0));
184 assert_eq!(down.len(), 3);
185 assert!(down.iter().all(|b| b.direction == -1));
186 assert_relative_eq!(down[2].close, 7.0, epsilon = 1e-12);
187 }
188
189 #[test]
190 fn reversal_needs_only_one_range() {
191 let mut bars = RangeBars::new(1.0).unwrap();
193 bars.update(flat(10.0));
194 bars.update(flat(12.0)); let down = bars.update(flat(11.0)); assert_eq!(down.len(), 1);
197 assert_eq!(down[0].direction, -1);
198 assert_relative_eq!(down[0].close, 11.0, epsilon = 1e-12);
199 assert_eq!(bars.anchor(), Some(11.0));
200 }
201
202 #[test]
203 fn small_move_prints_nothing() {
204 let mut bars = RangeBars::new(1.0).unwrap();
205 bars.update(flat(10.0));
206 assert!(bars.update(flat(10.5)).is_empty());
207 assert_eq!(bars.anchor(), Some(10.0));
208 }
209
210 #[test]
211 fn reset_clears_state() {
212 let mut bars = RangeBars::new(1.0).unwrap();
213 bars.update(flat(10.0));
214 bars.update(flat(13.0));
215 bars.reset();
216 assert_eq!(bars.anchor(), None);
217 assert!(bars.update(flat(50.0)).is_empty());
218 assert_eq!(bars.anchor(), Some(50.0));
219 }
220
221 #[test]
222 fn batch_concatenates_completed_bars() {
223 let mut bars = RangeBars::new(1.0).unwrap();
224 let candles = [flat(10.0), flat(12.0), flat(13.0)];
225 let out = bars.batch(&candles);
226 assert_eq!(out.len(), 3);
227 assert!(out.iter().all(|b| b.direction == 1));
228 }
229}