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 Ok(Self {
81 run_length,
82 count: 0,
83 open: 0.0,
84 high: 0.0,
85 low: 0.0,
86 close: 0.0,
87 prev_close: None,
88 run_sign: 0,
89 run_len: 0,
90 })
91 }
92
93 pub const fn run_length(&self) -> usize {
95 self.run_length
96 }
97
98 pub const fn run(&self) -> usize {
100 self.run_len
101 }
102}
103
104impl BarBuilder for RunBars {
105 type Bar = RunBar;
106
107 fn update(&mut self, candle: Candle) -> Vec<RunBar> {
108 if self.count == 0 {
109 self.open = candle.open;
110 self.high = candle.high;
111 self.low = candle.low;
112 } else {
113 self.high = self.high.max(candle.high);
114 self.low = self.low.min(candle.low);
115 }
116 self.close = candle.close;
117 self.count += 1;
118 if let Some(prev) = self.prev_close {
119 let directional = if candle.close > prev {
120 1
121 } else if candle.close < prev {
122 -1
123 } else {
124 0
125 };
126 if directional == 0 {
127 if self.run_sign != 0 {
129 self.run_len += 1;
130 }
131 } else if directional == self.run_sign {
132 self.run_len += 1;
133 } else {
134 self.run_sign = directional;
135 self.run_len = 1;
136 }
137 }
138 self.prev_close = Some(candle.close);
139 if self.run_sign == 0 || self.run_len < self.run_length {
140 return Vec::new();
141 }
142 let bar = RunBar {
143 open: self.open,
144 high: self.high,
145 low: self.low,
146 close: self.close,
147 length: self.run_len,
148 direction: self.run_sign,
149 };
150 self.count = 0;
151 self.run_sign = 0;
152 self.run_len = 0;
153 vec![bar]
154 }
155
156 fn reset(&mut self) {
157 self.count = 0;
158 self.prev_close = None;
159 self.run_sign = 0;
160 self.run_len = 0;
161 }
162
163 fn name(&self) -> &'static str {
164 "RunBars"
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn flat(price: f64) -> Candle {
173 Candle::new(price, price, price, price, 1.0, 0).unwrap()
174 }
175
176 #[test]
177 fn rejects_zero_run_length() {
178 assert!(matches!(RunBars::new(0), Err(Error::PeriodZero)));
179 }
180
181 #[test]
182 fn accessors_and_metadata() {
183 let bars = RunBars::new(5).unwrap();
184 assert_eq!(bars.run_length(), 5);
185 assert_eq!(bars.run(), 0);
186 assert_eq!(bars.name(), "RunBars");
187 }
188
189 #[test]
190 fn buy_run_closes_up_bar() {
191 let mut bars = RunBars::new(3).unwrap();
192 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);
197 assert_eq!(out[0].direction, 1);
198 assert_eq!(out[0].length, 3);
199 }
200
201 #[test]
202 fn sell_run_closes_down_bar() {
203 let mut bars = RunBars::new(3).unwrap();
204 bars.update(flat(10.0));
205 bars.update(flat(9.0)); bars.update(flat(8.0)); let out = bars.update(flat(7.0)); assert_eq!(out.len(), 1);
209 assert_eq!(out[0].direction, -1);
210 }
211
212 #[test]
213 fn opposite_tick_restarts_run() {
214 let mut bars = RunBars::new(3).unwrap();
215 bars.update(flat(10.0));
216 bars.update(flat(11.0)); bars.update(flat(12.0)); bars.update(flat(11.0)); assert_eq!(bars.run(), 1);
220 }
221
222 #[test]
223 fn flat_tick_extends_run() {
224 let mut bars = RunBars::new(3).unwrap();
225 bars.update(flat(10.0));
226 bars.update(flat(11.0)); bars.update(flat(11.0)); let out = bars.update(flat(12.0)); assert_eq!(out.len(), 1);
230 assert_eq!(out[0].direction, 1);
231 }
232
233 #[test]
234 fn reset_clears_state() {
235 let mut bars = RunBars::new(3).unwrap();
236 bars.update(flat(10.0));
237 bars.update(flat(11.0));
238 bars.reset();
239 assert_eq!(bars.run(), 0);
240 assert!(bars.update(flat(50.0)).is_empty());
241 }
242
243 #[test]
244 fn batch_concatenates_completed_bars() {
245 let mut bars = RunBars::new(2).unwrap();
246 let candles = [
247 flat(10.0),
248 flat(11.0), flat(12.0), flat(13.0), flat(14.0), ];
253 let out = bars.batch(&candles);
254 assert_eq!(out.len(), 2);
255 assert!(out.iter().all(|b| b.direction == 1));
256 }
257}