use crate::indicators::utils::{validate_data_length, validate_period};
use crate::indicators::{Candle, Indicator, IndicatorError};
use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IchimokuResult {
pub tenkan: f64,
pub kijun: f64,
pub senkou_a: f64,
pub senkou_b: f64,
pub chikou: f64,
}
#[derive(Debug)]
pub struct Ichimoku {
tenkan_period: usize,
kijun_period: usize,
senkou_b_period: usize,
buffer: VecDeque<(f64, f64)>,
}
impl Ichimoku {
pub fn new(
tenkan_period: usize,
kijun_period: usize,
senkou_b_period: usize,
) -> Result<Self, IndicatorError> {
validate_period(tenkan_period, 1)?;
validate_period(kijun_period, 1)?;
validate_period(senkou_b_period, 1)?;
if tenkan_period > kijun_period || kijun_period > senkou_b_period {
return Err(IndicatorError::InvalidParameter(
"Ichimoku periods must satisfy tenkan <= kijun <= senkou_b".to_string(),
));
}
Ok(Self {
tenkan_period,
kijun_period,
senkou_b_period,
buffer: VecDeque::with_capacity(senkou_b_period),
})
}
pub fn default_params() -> Self {
Self::new(9, 26, 52).expect("canonical params are valid")
}
pub fn reset_state(&mut self) {
self.buffer.clear();
}
fn midpoint(buffer: &VecDeque<(f64, f64)>, n: usize) -> f64 {
let start = buffer.len().saturating_sub(n);
let slice = buffer.iter().skip(start);
let mut hi = f64::NEG_INFINITY;
let mut lo = f64::INFINITY;
for &(h, l) in slice {
if h > hi {
hi = h;
}
if l < lo {
lo = l;
}
}
(hi + lo) / 2.0
}
fn step(&mut self, candle: Candle) -> Option<IchimokuResult> {
self.buffer.push_back((candle.high, candle.low));
if self.buffer.len() > self.senkou_b_period {
self.buffer.pop_front();
}
if self.buffer.len() < self.senkou_b_period {
return None;
}
let tenkan = Self::midpoint(&self.buffer, self.tenkan_period);
let kijun = Self::midpoint(&self.buffer, self.kijun_period);
let senkou_a = (tenkan + kijun) / 2.0;
let senkou_b = Self::midpoint(&self.buffer, self.senkou_b_period);
Some(IchimokuResult {
tenkan,
kijun,
senkou_a,
senkou_b,
chikou: candle.close,
})
}
}
impl Indicator<Candle, IchimokuResult> for Ichimoku {
fn calculate(&mut self, data: &[Candle]) -> Result<Vec<IchimokuResult>, IndicatorError> {
validate_data_length(data, self.senkou_b_period)?;
self.reset_state();
let mut out = Vec::with_capacity(data.len() - self.senkou_b_period + 1);
for c in data {
if let Some(v) = self.step(*c) {
out.push(v);
}
}
Ok(out)
}
fn next(&mut self, value: Candle) -> Result<Option<IchimokuResult>, IndicatorError> {
Ok(self.step(value))
}
fn reset(&mut self) {
self.reset_state();
}
fn name(&self) -> &'static str {
"Ichimoku"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linear_candles(n: usize) -> Vec<Candle> {
(0..n)
.map(|i| Candle {
timestamp: i as u64,
open: i as f64,
high: i as f64 + 1.0,
low: i as f64 - 1.0,
close: i as f64,
volume: 1.0,
})
.collect()
}
#[test]
fn validates_period_ordering() {
assert!(Ichimoku::new(0, 26, 52).is_err());
assert!(Ichimoku::new(26, 9, 52).is_err()); assert!(Ichimoku::new(9, 52, 26).is_err()); assert!(Ichimoku::new(9, 26, 52).is_ok());
}
#[test]
fn first_emission_at_senkou_b_period() {
let mut ichi = Ichimoku::new(2, 4, 6).unwrap();
let candles = linear_candles(20);
let mut emissions = 0;
for c in &candles {
if ichi.next(*c).unwrap().is_some() {
emissions += 1;
}
}
assert_eq!(emissions, 15);
}
#[test]
fn senkou_a_is_average_of_tenkan_and_kijun() {
let mut ichi = Ichimoku::default_params();
let candles = linear_candles(120);
let out = ichi.calculate(&candles).unwrap();
for v in &out {
assert!((v.senkou_a - (v.tenkan + v.kijun) / 2.0).abs() < 1e-12);
}
}
#[test]
fn chikou_is_current_close() {
let mut ichi = Ichimoku::default_params();
let candles = linear_candles(120);
let out = ichi.calculate(&candles).unwrap();
for (offset, v) in out.iter().enumerate() {
let bar_idx = offset + 51;
assert_eq!(v.chikou, candles[bar_idx].close);
}
}
}