1use crate::aes::Aesthetic;
16use crate::data::{DataFrame, Value};
17use crate::scale::ScaleSet;
18
19use super::Stat;
20
21type Brick = (f64, f64, bool);
23
24pub fn renko_bricks(prices: &[f64], b: f64) -> Vec<(f64, f64, bool)> {
26 let mut out: Vec<(f64, f64, bool)> = Vec::new();
27 let Some(&first) = prices.first() else {
28 return out;
29 };
30 if b.is_nan() || b <= 0.0 {
31 return out;
32 }
33 let (mut lo, mut hi) = (first, first);
34 for &p in &prices[1..] {
35 while p >= hi + b {
36 out.push((hi, hi + b, true));
37 lo = hi;
38 hi += b;
39 }
40 while p <= lo - b {
41 out.push((lo - b, lo, false));
42 hi = lo;
43 lo -= b;
44 }
45 }
46 out
47}
48
49pub fn point_figure_boxes(prices: &[f64], b: f64, reversal: usize) -> Vec<(usize, f64, bool)> {
52 let mut out = Vec::new();
53 let Some(&first) = prices.first() else {
54 return out;
55 };
56 if b.is_nan() || b <= 0.0 {
57 return out;
58 }
59 let k = reversal.max(1) as f64;
60 let base = (first / b).floor() * b;
62 let level = |p: f64| ((p - base) / b).floor();
63 let mut col: Option<(bool, f64, f64)> = None;
65 let mut idx = 0usize;
66 let start = level(first);
67 let push = |out: &mut Vec<(usize, f64, bool)>, idx: usize, from: f64, to: f64, up: bool| {
68 let mut l = from;
69 while l <= to {
70 out.push((idx, base + l * b, up));
71 l += 1.0;
72 }
73 };
74 for &p in &prices[1..] {
75 let lv = level(p);
76 match col {
77 None => {
78 if lv >= start + 1.0 {
79 col = Some((true, start, lv));
80 } else if lv <= start - 1.0 {
81 col = Some((false, lv, start));
82 }
83 }
84 Some((true, low, high)) => {
85 if lv > high {
86 col = Some((true, low, lv));
87 } else if lv <= high - k {
88 push(&mut out, idx, low, high, true);
89 idx += 1;
90 col = Some((false, lv, high - 1.0));
91 }
92 }
93 Some((false, low, high)) => {
94 if lv < low {
95 col = Some((false, lv, high));
96 } else if lv >= low + k {
97 push(&mut out, idx, low, high, false);
98 idx += 1;
99 col = Some((true, low + 1.0, lv));
100 }
101 }
102 }
103 }
104 if let Some((up, low, high)) = col {
105 push(&mut out, idx, low, high, up);
106 }
107 out
108}
109
110fn prices(data: &DataFrame) -> Vec<f64> {
111 data.column("y")
112 .map(|c| {
113 c.iter()
114 .filter_map(|v| v.as_f64())
115 .filter(|v| v.is_finite())
116 .collect()
117 })
118 .unwrap_or_default()
119}
120
121#[derive(Clone, Debug)]
123pub struct StatRenko {
124 pub box_size: f64,
125}
126
127impl Stat for StatRenko {
128 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
129 let bricks = renko_bricks(&prices(data), self.box_size);
130 let f = |g: &dyn Fn(usize, &Brick) -> f64| {
131 bricks
132 .iter()
133 .enumerate()
134 .map(|(i, b)| Value::Float(g(i, b)))
135 .collect::<Vec<_>>()
136 };
137 let mut out = DataFrame::new();
138 out.add_column("x".into(), f(&|i, _| i as f64 + 1.0));
139 out.add_column("ymin".into(), f(&|_, b| b.0));
140 out.add_column("ymax".into(), f(&|_, b| b.1));
141 out.add_column("y".into(), f(&|_, b| b.1));
142 out.add_column(
143 "direction".into(),
144 bricks
145 .iter()
146 .map(|b| Value::Str(if b.2 { "up" } else { "down" }.into()))
147 .collect(),
148 );
149 out
150 }
151 fn required_aes(&self) -> Vec<Aesthetic> {
152 vec![Aesthetic::Y]
153 }
154 fn panelwise(&self) -> bool {
155 true
156 }
157 fn name(&self) -> &str {
158 "renko"
159 }
160}
161
162#[derive(Clone, Debug)]
164pub struct StatPointFigure {
165 pub box_size: f64,
166 pub reversal: usize,
167}
168
169impl Stat for StatPointFigure {
170 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
171 let boxes = point_figure_boxes(&prices(data), self.box_size, self.reversal);
172 let b = self.box_size;
173 let mut out = DataFrame::new();
174 out.add_column(
175 "x".into(),
176 boxes
177 .iter()
178 .map(|x| Value::Float(x.0 as f64 + 1.0))
179 .collect(),
180 );
181 out.add_column(
182 "y".into(),
183 boxes.iter().map(|x| Value::Float(x.1 + b / 2.0)).collect(),
184 );
185 out.add_column(
186 "ymin".into(),
187 boxes.iter().map(|x| Value::Float(x.1)).collect(),
188 );
189 out.add_column(
190 "ymax".into(),
191 boxes.iter().map(|x| Value::Float(x.1 + b)).collect(),
192 );
193 out.add_column(
194 "mark".into(),
195 boxes
196 .iter()
197 .map(|x| Value::Str(if x.2 { "X" } else { "O" }.into()))
198 .collect(),
199 );
200 out
201 }
202 fn required_aes(&self) -> Vec<Aesthetic> {
203 vec![Aesthetic::Y]
204 }
205 fn panelwise(&self) -> bool {
206 true
207 }
208 fn name(&self) -> &str {
209 "point_figure"
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn renko_needs_a_full_box_and_two_to_reverse() {
219 let b = renko_bricks(&[10.0, 10.9, 11.0, 12.5, 11.2, 10.0, 9.0], 1.0);
220 assert_eq!(
222 b,
223 vec![
224 (10.0, 11.0, true),
225 (11.0, 12.0, true),
226 (10.0, 11.0, false),
227 (9.0, 10.0, false)
228 ]
229 );
230 assert!(renko_bricks(&[1.0, 2.0], 0.0).is_empty());
231 }
232
233 #[test]
234 fn point_figure_columns_reverse_after_k_boxes() {
235 let p = point_figure_boxes(&[10.0, 12.0, 15.0, 14.0, 12.0, 11.0, 14.0, 16.0], 1.0, 3);
237 let col = |c: usize| {
238 p.iter()
239 .filter(|b| b.0 == c)
240 .map(|b| (b.1, b.2))
241 .collect::<Vec<_>>()
242 };
243 assert_eq!(col(0).len(), 6, "X from 10 to 15");
244 assert!(col(0).iter().all(|b| b.1));
245 assert_eq!(
246 col(1),
247 vec![(11.0, false), (12.0, false), (13.0, false), (14.0, false)]
248 );
249 assert!(col(2).iter().all(|b| b.1) && col(2).last().unwrap().0 == 16.0);
250 }
251}