wickra_core/indicators/
run_bars.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::BarBuilder;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct RunBar {
10 pub open: f64,
12 pub high: f64,
14 pub low: f64,
16 pub close: f64,
18 pub length: usize,
20 pub direction: i8,
22}
23
24#[derive(Debug, Clone)]
58pub struct RunBars {
59 run_length: usize,
60 count: usize,
61 open: f64,
62 high: f64,
63 low: f64,
64 close: f64,
65 prev_close: Option<f64>,
66 run_sign: i8,
67 run_len: usize,
68}
69
70impl RunBars {
71 pub fn new(run_length: usize) -> Result<Self> {
77 if run_length == 0 {
78 return Err(Error::PeriodZero);
79 }
80 if run_length > crate::error::MAX_PERIOD {
81 return Err(Error::InvalidPeriod {
82 message: crate::error::PERIOD_ABOVE_MAX,
83 });
84 }
85 Ok(Self {
86 run_length,
87 count: 0,
88 open: 0.0,
89 high: 0.0,
90 low: 0.0,
91 close: 0.0,
92 prev_close: None,
93 run_sign: 0,
94 run_len: 0,
95 })
96 }
97
98 pub const fn run_length(&self) -> usize {
100 self.run_length
101 }
102
103 pub const fn run(&self) -> usize {
105 self.run_len
106 }
107}
108
109impl BarBuilder for RunBars {
110 type Bar = RunBar;
111
112 fn update(&mut self, candle: Candle) -> Vec<RunBar> {
113 if self.count == 0 {
114 self.open = candle.open;
115 self.high = candle.high;
116 self.low = candle.low;
117 } else {
118 self.high = self.high.max(candle.high);
119 self.low = self.low.min(candle.low);
120 }
121 self.close = candle.close;
122 self.count += 1;
123 if let Some(prev) = self.prev_close {
124 let directional = if candle.close > prev {
125 1
126 } else if candle.close < prev {
127 -1
128 } else {
129 0
130 };
131 if directional == 0 {
132 if self.run_sign != 0 {
134 self.run_len += 1;
135 }
136 } else if directional == self.run_sign {
137 self.run_len += 1;
138 } else {
139 self.run_sign = directional;
140 self.run_len = 1;
141 }
142 }
143 self.prev_close = Some(candle.close);
144 if self.run_sign == 0 || self.run_len < self.run_length {
145 return Vec::new();
146 }
147 let bar = RunBar {
148 open: self.open,
149 high: self.high,
150 low: self.low,
151 close: self.close,
152 length: self.run_len,
153 direction: self.run_sign,
154 };
155 self.count = 0;
156 self.run_sign = 0;
157 self.run_len = 0;
158 vec![bar]
159 }
160
161 fn reset(&mut self) {
162 self.count = 0;
163 self.prev_close = None;
164 self.run_sign = 0;
165 self.run_len = 0;
166 }
167
168 #[inline]
169 fn name(&self) -> &'static str {
170 "RunBars"
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn flat(price: f64) -> Candle {
179 Candle::new(price, price, price, price, 1.0, 0).unwrap()
180 }
181
182 #[test]
183 fn rejects_zero_run_length() {
184 assert!(matches!(RunBars::new(0), Err(Error::PeriodZero)));
185 }
186
187 #[test]
188 fn accessors_and_metadata() {
189 let bars = RunBars::new(5).unwrap();
190 assert_eq!(bars.run_length(), 5);
191 assert_eq!(bars.run(), 0);
192 assert_eq!(bars.name(), "RunBars");
193 }
194
195 #[test]
196 fn buy_run_closes_up_bar() {
197 let mut bars = RunBars::new(3).unwrap();
198 bars.update(flat(10.0)); bars.update(flat(11.0)); bars.update(flat(12.0)); let out = bars.update(flat(13.0)); assert_eq!(out.len(), 1);
203 assert_eq!(out[0].direction, 1);
204 assert_eq!(out[0].length, 3);
205 }
206
207 #[test]
208 fn sell_run_closes_down_bar() {
209 let mut bars = RunBars::new(3).unwrap();
210 bars.update(flat(10.0));
211 bars.update(flat(9.0)); bars.update(flat(8.0)); let out = bars.update(flat(7.0)); assert_eq!(out.len(), 1);
215 assert_eq!(out[0].direction, -1);
216 }
217
218 #[test]
219 fn opposite_tick_restarts_run() {
220 let mut bars = RunBars::new(3).unwrap();
221 bars.update(flat(10.0));
222 bars.update(flat(11.0)); bars.update(flat(12.0)); bars.update(flat(11.0)); assert_eq!(bars.run(), 1);
226 }
227
228 #[test]
229 fn flat_tick_extends_run() {
230 let mut bars = RunBars::new(3).unwrap();
231 bars.update(flat(10.0));
232 bars.update(flat(11.0)); bars.update(flat(11.0)); let out = bars.update(flat(12.0)); assert_eq!(out.len(), 1);
236 assert_eq!(out[0].direction, 1);
237 }
238
239 #[test]
240 fn reset_clears_state() {
241 let mut bars = RunBars::new(3).unwrap();
242 bars.update(flat(10.0));
243 bars.update(flat(11.0));
244 bars.reset();
245 assert_eq!(bars.run(), 0);
246 assert!(bars.update(flat(50.0)).is_empty());
247 }
248
249 #[test]
250 fn batch_concatenates_completed_bars() {
251 let mut bars = RunBars::new(2).unwrap();
252 let candles = [
253 flat(10.0),
254 flat(11.0), flat(12.0), flat(13.0), flat(14.0), ];
259 let out = bars.batch(&candles);
260 assert_eq!(out.len(), 2);
261 assert!(out.iter().all(|b| b.direction == 1));
262 }
263}