use super::traits::PriceDataAccessor;
#[derive(Debug, Clone, Copy)]
pub struct Candle {
pub timestamp: u64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
impl PriceDataAccessor<Candle> for Candle {
fn get_close(&self, data: &Candle) -> f64 {
data.close
}
fn get_high(&self, data: &Candle) -> f64 {
data.high
}
fn get_low(&self, data: &Candle) -> f64 {
data.low
}
fn get_open(&self, data: &Candle) -> f64 {
data.open
}
fn get_volume(&self, data: &Candle) -> f64 {
data.volume
}
}
pub fn heikin_ashi(candles: &[Candle]) -> Vec<Candle> {
if candles.is_empty() {
return Vec::new();
}
let mut out = Vec::with_capacity(candles.len());
let first = &candles[0];
let ha_close_0 = (first.open + first.high + first.low + first.close) / 4.0;
let ha_open_0 = (first.open + first.close) / 2.0;
out.push(Candle {
timestamp: first.timestamp,
open: ha_open_0,
high: first.high.max(ha_open_0).max(ha_close_0),
low: first.low.min(ha_open_0).min(ha_close_0),
close: ha_close_0,
volume: first.volume,
});
for c in &candles[1..] {
let prev = out.last().unwrap();
let ha_close = (c.open + c.high + c.low + c.close) / 4.0;
let ha_open = (prev.open + prev.close) / 2.0;
out.push(Candle {
timestamp: c.timestamp,
open: ha_open,
high: c.high.max(ha_open).max(ha_close),
low: c.low.min(ha_open).min(ha_close),
close: ha_close,
volume: c.volume,
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_heikin_ashi_empty() {
let out = heikin_ashi(&[]);
assert!(out.is_empty());
}
#[test]
fn test_heikin_ashi_first_bar_seeded() {
let candles = [Candle {
timestamp: 0,
open: 10.0,
high: 11.0,
low: 9.0,
close: 10.5,
volume: 1.0,
}];
let ha = heikin_ashi(&candles);
assert!((ha[0].close - 10.125).abs() < 1e-12);
assert!((ha[0].open - 10.25).abs() < 1e-12);
assert_eq!(ha[0].high, 11.0);
assert_eq!(ha[0].low, 9.0);
}
#[test]
fn test_heikin_ashi_recurrence_uses_prev_ha() {
let candles = [
Candle {
timestamp: 0,
open: 10.0,
high: 11.0,
low: 9.0,
close: 10.5,
volume: 1.0,
},
Candle {
timestamp: 1,
open: 10.5,
high: 12.0,
low: 10.0,
close: 11.5,
volume: 1.0,
},
];
let ha = heikin_ashi(&candles);
assert!((ha[1].open - 10.1875).abs() < 1e-12);
assert!((ha[1].close - 11.0).abs() < 1e-12);
}
#[test]
fn test_candle_creation_and_access() {
let candle = Candle {
timestamp: 1618185600,
open: 100.0,
high: 105.0,
low: 98.0,
close: 103.0,
volume: 1000.0,
};
assert_eq!(candle.timestamp, 1618185600);
assert_eq!(candle.open, 100.0);
assert_eq!(candle.high, 105.0);
assert_eq!(candle.low, 98.0);
assert_eq!(candle.close, 103.0);
assert_eq!(candle.volume, 1000.0);
}
#[test]
fn test_price_data_accessor_impl() {
let candle = Candle {
timestamp: 1618185600,
open: 100.0,
high: 105.0,
low: 98.0,
close: 103.0,
volume: 1000.0,
};
assert_eq!(candle.get_open(&candle), 100.0);
assert_eq!(candle.get_high(&candle), 105.0);
assert_eq!(candle.get_low(&candle), 98.0);
assert_eq!(candle.get_close(&candle), 103.0);
assert_eq!(candle.get_volume(&candle), 1000.0);
}
#[test]
fn test_candle_copy_and_clone() {
let candle1 = Candle {
timestamp: 1618185600,
open: 100.0,
high: 105.0,
low: 98.0,
close: 103.0,
volume: 1000.0,
};
let candle2 = candle1;
assert_eq!(candle1.timestamp, candle2.timestamp);
assert_eq!(candle1.open, candle2.open);
assert_eq!(candle1.high, candle2.high);
assert_eq!(candle1.low, candle2.low);
assert_eq!(candle1.close, candle2.close);
assert_eq!(candle1.volume, candle2.volume);
let candle3 = candle1;
assert_eq!(candle1.timestamp, candle3.timestamp);
assert_eq!(candle1.open, candle3.open);
assert_eq!(candle1.high, candle3.high);
assert_eq!(candle1.low, candle3.low);
assert_eq!(candle1.close, candle3.close);
assert_eq!(candle1.volume, candle3.volume);
}
}