use std::cell::RefCell;
use crate::model::Bar;
pub fn transform_to_heikin_ashi(bars: &[Bar]) -> Vec<Bar> {
let mut ha_bars: Vec<Bar> = Vec::with_capacity(bars.len());
let Some(first) = bars.first() else {
return ha_bars;
};
let mut prev_ha_open = (first.open + first.close) / 2.0;
let mut prev_ha_close = (first.open + first.high + first.low + first.close) / 4.0;
ha_bars.push(Bar {
time: first.time,
open: prev_ha_open,
high: first.high,
low: first.low,
close: prev_ha_close,
volume: first.volume,
});
for bar in bars.iter().skip(1) {
let ha_close = (bar.open + bar.high + bar.low + bar.close) / 4.0;
let ha_open = (prev_ha_open + prev_ha_close) / 2.0;
let ha_high = bar.high.max(ha_open).max(ha_close);
let ha_low = bar.low.min(ha_open).min(ha_close);
ha_bars.push(Bar {
time: bar.time,
open: ha_open,
high: ha_high,
low: ha_low,
close: ha_close,
volume: bar.volume,
});
prev_ha_open = ha_open;
prev_ha_close = ha_close;
}
ha_bars
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SeriesFingerprint {
len: usize,
first_time: i64,
last_time: i64,
first_close: u64,
last_close: u64,
}
impl SeriesFingerprint {
fn of(bars: &[Bar]) -> Self {
match (bars.first(), bars.last()) {
(Some(first), Some(last)) => Self {
len: bars.len(),
first_time: first.time.timestamp_millis(),
last_time: last.time.timestamp_millis(),
first_close: first.close.to_bits(),
last_close: last.close.to_bits(),
},
_ => Self {
len: 0,
first_time: 0,
last_time: 0,
first_close: 0,
last_close: 0,
},
}
}
}
struct HeikinAshiCache {
entries: Vec<(SeriesFingerprint, Vec<Bar>)>,
}
const MAX_CACHED_SERIES: usize = 8;
impl HeikinAshiCache {
const fn new() -> Self {
Self {
entries: Vec::new(),
}
}
fn series(&mut self, full_data: &[Bar]) -> Vec<Bar> {
let fingerprint = SeriesFingerprint::of(full_data);
if let Some(pos) = self.entries.iter().position(|(fp, _)| *fp == fingerprint) {
let entry = self.entries.remove(pos);
let series = entry.1.clone();
self.entries.push(entry);
return series;
}
let series = transform_to_heikin_ashi(full_data);
if self.entries.len() >= MAX_CACHED_SERIES {
self.entries.remove(0);
}
self.entries.push((fingerprint, series.clone()));
series
}
}
thread_local! {
static HEIKIN_ASHI_CACHE: RefCell<HeikinAshiCache> = const { RefCell::new(HeikinAshiCache::new()) };
}
pub fn window(full_data: &[Bar], start_idx: usize, len: usize) -> Vec<Bar> {
HEIKIN_ASHI_CACHE.with(|cache| {
let series = cache.borrow_mut().series(full_data);
let start = start_idx.min(series.len());
let end = start.saturating_add(len).min(series.len());
series[start..end].to_vec()
})
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
fn bar(i: i64, open: f64, high: f64, low: f64, close: f64) -> Bar {
Bar {
time: Utc.timestamp_opt(1_700_000_000 + i * 60, 0).unwrap(),
open,
high,
low,
close,
volume: 100.0,
}
}
fn sample_series() -> Vec<Bar> {
vec![
bar(0, 10.0, 12.0, 9.0, 11.0),
bar(1, 11.0, 13.0, 10.5, 12.5),
bar(2, 12.5, 14.0, 12.0, 13.0),
bar(3, 13.0, 13.5, 11.0, 11.5),
bar(4, 11.5, 12.0, 10.0, 10.5),
bar(5, 10.5, 11.0, 9.5, 10.0),
]
}
fn assert_bars_eq(a: &Bar, b: &Bar, ctx: &str) {
assert_eq!(a.open.to_bits(), b.open.to_bits(), "{ctx}: open");
assert_eq!(a.high.to_bits(), b.high.to_bits(), "{ctx}: high");
assert_eq!(a.low.to_bits(), b.low.to_bits(), "{ctx}: low");
assert_eq!(a.close.to_bits(), b.close.to_bits(), "{ctx}: close");
}
#[test]
fn ha_window_is_independent_of_start() {
let full = sample_series();
let ha_full = transform_to_heikin_ashi(&full);
for start in 0..full.len() {
let len = full.len() - start;
let win = window(&full, start, len);
assert_eq!(win.len(), len, "start={start}");
for (i, w) in win.iter().enumerate() {
assert_bars_eq(w, &ha_full[start + i], &format!("start={start}, i={i}"));
}
}
}
#[test]
fn windowed_seed_diverges_from_full_seed() {
let full = sample_series();
let ha_full = transform_to_heikin_ashi(&full);
let ha_windowed = transform_to_heikin_ashi(&full[2..]);
assert_ne!(
ha_windowed[0].open.to_bits(),
ha_full[2].open.to_bits(),
"windowed seed should differ from full-series value"
);
}
#[test]
fn cache_returns_stable_values() {
let full = sample_series();
let first = window(&full, 1, 3);
let second = window(&full, 1, 3);
assert_eq!(first.len(), second.len());
for (a, b) in first.iter().zip(second.iter()) {
assert_bars_eq(a, b, "stable");
}
}
#[test]
fn cache_invalidates_on_data_change() {
let series_a = sample_series();
let win_a = window(&series_a, 0, series_a.len());
assert_eq!(win_a.len(), series_a.len());
let mut series_b = sample_series();
series_b.push(bar(6, 10.0, 10.5, 9.0, 9.5));
let win_b = window(&series_b, 0, series_b.len());
assert_eq!(win_b.len(), series_b.len());
assert_ne!(win_a.len(), win_b.len());
}
#[test]
fn window_clamps_out_of_range() {
let full = sample_series();
assert!(window(&full, 100, 10).is_empty());
let tail = window(&full, full.len() - 1, 50);
assert_eq!(tail.len(), 1);
}
#[test]
fn empty_series_is_empty() {
assert!(window(&[], 0, 10).is_empty());
}
}