1use super::PriceBasedSource;
13use crate::model::{Bar, PriceBuilder};
14use chrono::{DateTime, Utc};
15
16#[derive(Debug, Clone)]
18pub struct RenkoBrick {
19 pub ts: DateTime<Utc>,
21 pub open: f64,
23 pub close: f64,
25 pub high: f64,
27 pub low: f64,
29 pub direction: RenkoDirection,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum RenkoDirection {
36 Up,
38 Down,
40}
41
42impl RenkoBrick {
43 pub fn to_bar(&self) -> Bar {
45 Bar {
46 time: self.ts,
47 open: self.open,
48 high: self.high,
49 low: self.low,
50 close: self.close,
51 volume: 0.0, colors: None,
53 payload: None,
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
63pub struct RenkoConfig {
64 pub brick_size: f64,
66 pub source: PriceBasedSource,
68}
69
70impl Default for RenkoConfig {
71 fn default() -> Self {
72 Self {
73 brick_size: 1.0,
74 source: PriceBasedSource::Close,
75 }
76 }
77}
78
79impl RenkoConfig {
80 pub fn new(brick_size: f64) -> Self {
82 Self {
83 brick_size,
84 source: PriceBasedSource::Close,
85 }
86 }
87
88 pub fn from_atr(bars: &[Bar], period: usize, multiplier: f64) -> Self {
90 Self {
91 brick_size: super::BoxSizeMethod::Atr { period, multiplier }
92 .resolve(0.0, bars)
93 .unwrap_or(f64::NAN),
94 source: PriceBasedSource::Close,
95 }
96 }
97}
98
99pub fn to_renko_bricks(bars: &[Bar], config: &RenkoConfig) -> Vec<RenkoBrick> {
114 let brick_size = config.brick_size;
115 if bars.is_empty() || !(brick_size > 0.0 && brick_size.is_finite()) {
116 return Vec::new();
117 }
118 let mut builder = RenkoBuilder::new(brick_size, config.source);
119 for bar in bars[work_start(bars, brick_size, config.source)..]
120 .iter()
121 .filter(|bar| !bar.is_whitespace())
122 {
123 builder.push(bar);
124 }
125 builder.elements()
126}
127
128pub(crate) const MAX_BRICKS: usize = 10_000;
129
130#[derive(Clone)]
132pub(crate) struct RenkoBuilder {
133 size: f64,
134 source: PriceBasedSource,
135 extremes: Option<(f64, f64)>,
136 last: Option<(f64, f64)>,
139 bricks: Vec<RenkoBrick>,
140}
141
142impl RenkoBuilder {
143 pub(crate) fn new(size: f64, source: PriceBasedSource) -> Self {
144 Self {
145 size,
146 source,
147 extremes: None,
148 last: None,
149 bricks: Vec::new(),
150 }
151 }
152}
153
154impl PriceBuilder for RenkoBuilder {
155 type Element = RenkoBrick;
156
157 fn push(&mut self, bar: &Bar) {
158 let size = self.size;
159 let Some(mut last) = self.last else {
160 let start = (bar.close / size).floor() * size;
161 self.last = Some((start, start));
162 self.extremes = Some((bar.close, bar.close));
163 return;
164 };
165 let (low, high) = match self.source {
166 PriceBasedSource::Close => (bar.close, bar.close),
167 PriceBasedSource::HighLow => (bar.low, bar.high),
168 };
169 let (prior_low, prior_high) = self.extremes.unwrap_or((bar.close, bar.close));
170 let (mut wick_low, mut wick_high) = (prior_low.min(low), prior_high.max(high));
171 let (open, close) = last;
172 let price = match self.source {
173 PriceBasedSource::Close => bar.close,
174 PriceBasedSource::HighLow if close >= open => {
175 if high >= close + size {
176 high
177 } else {
178 low
179 }
180 }
181 PriceBasedSource::HighLow => {
182 if low <= close - size {
183 low
184 } else {
185 high
186 }
187 }
188 };
189 loop {
190 let (open, close) = last;
191 let (low, high) = (open.min(close), open.max(close));
192 let (brick_open, brick_close) = if price >= high + size {
194 (high, high + size)
195 } else if price <= low - size {
196 (low, low - size)
197 } else {
198 break;
199 };
200 if brick_close == brick_open {
202 break;
203 }
204 let up = brick_close > brick_open;
205
206 self.bricks.push(RenkoBrick {
207 ts: bar.time,
208 open: brick_open,
209 close: brick_close,
210 high: if up {
211 brick_close
212 } else {
213 wick_high.max(brick_open)
214 },
215 low: if up {
216 wick_low.min(brick_open)
217 } else {
218 brick_close
219 },
220 direction: if up {
221 RenkoDirection::Up
222 } else {
223 RenkoDirection::Down
224 },
225 });
226 last = (brick_open, brick_close);
227 wick_low = brick_close;
228 wick_high = brick_close;
229
230 if self.bricks.len() == 2 * MAX_BRICKS {
232 self.bricks.drain(..MAX_BRICKS);
233 }
234 }
235 self.last = Some(last);
236 self.extremes = Some((wick_low.min(price), wick_high.max(price)));
238 }
239
240 fn elements(&self) -> Vec<RenkoBrick> {
241 self.bricks[self.bricks.len().saturating_sub(MAX_BRICKS)..].to_vec()
242 }
243}
244
245pub(crate) fn work_start(bars: &[Bar], size: f64, source: PriceBasedSource) -> usize {
248 let mut total = 0.0;
249 let mut priced = bars
250 .iter()
251 .enumerate()
252 .rev()
253 .filter(|(_, bar)| !bar.is_whitespace());
254 let Some((mut i, mut bar)) = priced.next() else {
255 return 0;
256 };
257 for (previous_i, previous) in priced {
258 let movement = match source {
259 PriceBasedSource::Close => (bar.close - previous.close).abs(),
260 PriceBasedSource::HighLow => bar.high.max(previous.high) - bar.low.min(previous.low),
261 };
262 total += (movement / size).max(0.0) + 2.0;
263 if total > super::MAX_WORK {
264 return i;
265 }
266 (i, bar) = (previous_i, previous);
267 }
268 0
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use chrono::Duration;
275 #[test]
276 fn bricks_reverse_only_after_a_two_box_move() {
277 let cases = [
278 (
279 vec![100.0, 103.0, 102.0],
280 vec![(100.0, 101.0), (101.0, 102.0), (102.0, 103.0)],
281 ),
282 (
283 vec![100.0, 103.0, 101.0],
284 vec![
285 (100.0, 101.0),
286 (101.0, 102.0),
287 (102.0, 103.0),
288 (102.0, 101.0),
289 ],
290 ),
291 (
292 vec![100.0, 103.0, 101.0, 102.0, 103.0],
293 vec![
294 (100.0, 101.0),
295 (101.0, 102.0),
296 (102.0, 103.0),
297 (102.0, 101.0),
298 (102.0, 103.0),
299 ],
300 ),
301 (vec![100.0, 98.0], vec![(100.0, 99.0), (99.0, 98.0)]),
302 (vec![100.0, 100.9], vec![]),
303 (vec![9_007_199_254_740_992.0; 2], vec![]),
305 ];
306 let start = Utc::now();
307 for (closes, expected) in &cases {
308 let bars: Vec<Bar> = closes
309 .iter()
310 .enumerate()
311 .map(|(i, &c)| Bar {
312 time: start + Duration::minutes(i as i64),
313 open: c,
314 high: c,
315 low: c,
316 close: c,
317 volume: 0.0,
318 colors: None,
319 payload: None,
320 })
321 .collect();
322 let got: Vec<(f64, f64)> = to_renko_bricks(&bars, &RenkoConfig::new(1.0))
323 .iter()
324 .map(|b| (b.open, b.close))
325 .collect();
326 assert_eq!(got, *expected, "closes {closes:?}");
327 }
328 }
329
330 #[test]
331 fn test_renko_brick_to_bar() {
332 let brick = RenkoBrick {
333 ts: Utc::now(),
334 open: 100.0,
335 close: 102.0,
336 high: 102.0,
337 low: 99.5,
338 direction: RenkoDirection::Up,
339 };
340
341 let bar = brick.to_bar();
342 assert_eq!(bar.open, 100.0);
343 assert_eq!(bar.close, 102.0);
344 assert_eq!(bar.high, 102.0);
345 assert_eq!(bar.low, 99.5);
346 }
347}