use crate::aes::Aesthetic;
use crate::data::{DataFrame, Value};
use crate::scale::ScaleSet;
use super::Stat;
type Brick = (f64, f64, bool);
pub fn renko_bricks(prices: &[f64], b: f64) -> Vec<(f64, f64, bool)> {
let mut out: Vec<(f64, f64, bool)> = Vec::new();
let Some(&first) = prices.first() else {
return out;
};
if b.is_nan() || b <= 0.0 {
return out;
}
let (mut lo, mut hi) = (first, first);
for &p in &prices[1..] {
while p >= hi + b {
out.push((hi, hi + b, true));
lo = hi;
hi += b;
}
while p <= lo - b {
out.push((lo - b, lo, false));
hi = lo;
lo -= b;
}
}
out
}
pub fn point_figure_boxes(prices: &[f64], b: f64, reversal: usize) -> Vec<(usize, f64, bool)> {
let mut out = Vec::new();
let Some(&first) = prices.first() else {
return out;
};
if b.is_nan() || b <= 0.0 {
return out;
}
let k = reversal.max(1) as f64;
let base = (first / b).floor() * b;
let level = |p: f64| ((p - base) / b).floor();
let mut col: Option<(bool, f64, f64)> = None;
let mut idx = 0usize;
let start = level(first);
let push = |out: &mut Vec<(usize, f64, bool)>, idx: usize, from: f64, to: f64, up: bool| {
let mut l = from;
while l <= to {
out.push((idx, base + l * b, up));
l += 1.0;
}
};
for &p in &prices[1..] {
let lv = level(p);
match col {
None => {
if lv >= start + 1.0 {
col = Some((true, start, lv));
} else if lv <= start - 1.0 {
col = Some((false, lv, start));
}
}
Some((true, low, high)) => {
if lv > high {
col = Some((true, low, lv));
} else if lv <= high - k {
push(&mut out, idx, low, high, true);
idx += 1;
col = Some((false, lv, high - 1.0));
}
}
Some((false, low, high)) => {
if lv < low {
col = Some((false, lv, high));
} else if lv >= low + k {
push(&mut out, idx, low, high, false);
idx += 1;
col = Some((true, low + 1.0, lv));
}
}
}
}
if let Some((up, low, high)) = col {
push(&mut out, idx, low, high, up);
}
out
}
fn prices(data: &DataFrame) -> Vec<f64> {
data.column("y")
.map(|c| {
c.iter()
.filter_map(|v| v.as_f64())
.filter(|v| v.is_finite())
.collect()
})
.unwrap_or_default()
}
#[derive(Clone, Debug)]
pub struct StatRenko {
pub box_size: f64,
}
impl Stat for StatRenko {
fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
let bricks = renko_bricks(&prices(data), self.box_size);
let f = |g: &dyn Fn(usize, &Brick) -> f64| {
bricks
.iter()
.enumerate()
.map(|(i, b)| Value::Float(g(i, b)))
.collect::<Vec<_>>()
};
let mut out = DataFrame::new();
out.add_column("x".into(), f(&|i, _| i as f64 + 1.0));
out.add_column("ymin".into(), f(&|_, b| b.0));
out.add_column("ymax".into(), f(&|_, b| b.1));
out.add_column("y".into(), f(&|_, b| b.1));
out.add_column(
"direction".into(),
bricks
.iter()
.map(|b| Value::Str(if b.2 { "up" } else { "down" }.into()))
.collect(),
);
out
}
fn required_aes(&self) -> Vec<Aesthetic> {
vec![Aesthetic::Y]
}
fn panelwise(&self) -> bool {
true
}
fn name(&self) -> &str {
"renko"
}
}
#[derive(Clone, Debug)]
pub struct StatPointFigure {
pub box_size: f64,
pub reversal: usize,
}
impl Stat for StatPointFigure {
fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
let boxes = point_figure_boxes(&prices(data), self.box_size, self.reversal);
let b = self.box_size;
let mut out = DataFrame::new();
out.add_column(
"x".into(),
boxes
.iter()
.map(|x| Value::Float(x.0 as f64 + 1.0))
.collect(),
);
out.add_column(
"y".into(),
boxes.iter().map(|x| Value::Float(x.1 + b / 2.0)).collect(),
);
out.add_column(
"ymin".into(),
boxes.iter().map(|x| Value::Float(x.1)).collect(),
);
out.add_column(
"ymax".into(),
boxes.iter().map(|x| Value::Float(x.1 + b)).collect(),
);
out.add_column(
"mark".into(),
boxes
.iter()
.map(|x| Value::Str(if x.2 { "X" } else { "O" }.into()))
.collect(),
);
out
}
fn required_aes(&self) -> Vec<Aesthetic> {
vec![Aesthetic::Y]
}
fn panelwise(&self) -> bool {
true
}
fn name(&self) -> &str {
"point_figure"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renko_needs_a_full_box_and_two_to_reverse() {
let b = renko_bricks(&[10.0, 10.9, 11.0, 12.5, 11.2, 10.0, 9.0], 1.0);
assert_eq!(
b,
vec![
(10.0, 11.0, true),
(11.0, 12.0, true),
(10.0, 11.0, false),
(9.0, 10.0, false)
]
);
assert!(renko_bricks(&[1.0, 2.0], 0.0).is_empty());
}
#[test]
fn point_figure_columns_reverse_after_k_boxes() {
let p = point_figure_boxes(&[10.0, 12.0, 15.0, 14.0, 12.0, 11.0, 14.0, 16.0], 1.0, 3);
let col = |c: usize| {
p.iter()
.filter(|b| b.0 == c)
.map(|b| (b.1, b.2))
.collect::<Vec<_>>()
};
assert_eq!(col(0).len(), 6, "X from 10 to 15");
assert!(col(0).iter().all(|b| b.1));
assert_eq!(
col(1),
vec![(11.0, false), (12.0, false), (13.0, false), (14.0, false)]
);
assert!(col(2).iter().all(|b| b.1) && col(2).last().unwrap().0 == 16.0);
}
}