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 fn update(&mut self, candle: Candle) -> Vec<RangeBar> {
89 let close = candle.close;
90 let Some(mut anchor) = self.anchor else {
91 self.anchor = Some(close);
92 return Vec::new();
93 };
94 let range = self.range;
95 let mut bars = Vec::new();
96 while close >= anchor + range {
97 bars.push(RangeBar {
98 open: anchor,
99 close: anchor + range,
100 direction: 1,
101 });
102 anchor += range;
103 }
104 while close <= anchor - range {
105 bars.push(RangeBar {
106 open: anchor,
107 close: anchor - range,
108 direction: -1,
109 });
110 anchor -= range;
111 }
112 self.anchor = Some(anchor);
113 bars
114 }
115
116 fn reset(&mut self) {
117 self.anchor = None;
118 }
119
120 fn name(&self) -> &'static str {
121 "RangeBars"
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use approx::assert_relative_eq;
129
130 fn flat(price: f64) -> Candle {
131 Candle::new(price, price, price, price, 1.0, 0).unwrap()
132 }
133
134 #[test]
135 fn rejects_invalid_range() {
136 assert!(matches!(
137 RangeBars::new(0.0),
138 Err(Error::InvalidPeriod { .. })
139 ));
140 assert!(matches!(
141 RangeBars::new(-1.0),
142 Err(Error::InvalidPeriod { .. })
143 ));
144 assert!(matches!(
145 RangeBars::new(f64::NAN),
146 Err(Error::InvalidPeriod { .. })
147 ));
148 }
149
150 #[test]
151 fn accessors_and_metadata() {
152 let bars = RangeBars::new(2.5).unwrap();
153 assert_eq!(bars.name(), "RangeBars");
154 assert_relative_eq!(bars.range(), 2.5, epsilon = 1e-12);
155 assert_eq!(bars.anchor(), None);
156 }
157
158 #[test]
159 fn first_candle_seeds_without_bar() {
160 let mut bars = RangeBars::new(1.0).unwrap();
161 assert!(bars.update(flat(10.0)).is_empty());
162 assert_eq!(bars.anchor(), Some(10.0));
163 }
164
165 #[test]
166 fn up_move_prints_aligned_bars() {
167 let mut bars = RangeBars::new(1.0).unwrap();
168 bars.update(flat(10.0));
169 let up = bars.update(flat(13.0));
170 assert_eq!(up.len(), 3);
171 assert_relative_eq!(up[0].open, 10.0, epsilon = 1e-12);
172 assert_relative_eq!(up[2].close, 13.0, epsilon = 1e-12);
173 assert!(up.iter().all(|b| b.direction == 1));
174 assert_eq!(bars.anchor(), Some(13.0));
175 }
176
177 #[test]
178 fn down_move_prints_aligned_bars() {
179 let mut bars = RangeBars::new(1.0).unwrap();
180 bars.update(flat(10.0));
181 let down = bars.update(flat(7.0));
182 assert_eq!(down.len(), 3);
183 assert!(down.iter().all(|b| b.direction == -1));
184 assert_relative_eq!(down[2].close, 7.0, epsilon = 1e-12);
185 }
186
187 #[test]
188 fn reversal_needs_only_one_range() {
189 let mut bars = RangeBars::new(1.0).unwrap();
191 bars.update(flat(10.0));
192 bars.update(flat(12.0)); let down = bars.update(flat(11.0)); assert_eq!(down.len(), 1);
195 assert_eq!(down[0].direction, -1);
196 assert_relative_eq!(down[0].close, 11.0, epsilon = 1e-12);
197 assert_eq!(bars.anchor(), Some(11.0));
198 }
199
200 #[test]
201 fn small_move_prints_nothing() {
202 let mut bars = RangeBars::new(1.0).unwrap();
203 bars.update(flat(10.0));
204 assert!(bars.update(flat(10.5)).is_empty());
205 assert_eq!(bars.anchor(), Some(10.0));
206 }
207
208 #[test]
209 fn reset_clears_state() {
210 let mut bars = RangeBars::new(1.0).unwrap();
211 bars.update(flat(10.0));
212 bars.update(flat(13.0));
213 bars.reset();
214 assert_eq!(bars.anchor(), None);
215 assert!(bars.update(flat(50.0)).is_empty());
216 assert_eq!(bars.anchor(), Some(50.0));
217 }
218
219 #[test]
220 fn batch_concatenates_completed_bars() {
221 let mut bars = RangeBars::new(1.0).unwrap();
222 let candles = [flat(10.0), flat(12.0), flat(13.0)];
223 let out = bars.batch(&candles);
224 assert_eq!(out.len(), 3);
225 assert!(out.iter().all(|b| b.direction == 1));
226 }
227}