use crate::error::FinError;
use crate::signals::{BarInput, Signal, SignalValue};
use rust_decimal::Decimal;
use std::collections::VecDeque;
pub struct Ichimoku {
name: String,
tenkan: usize,
kijun: usize,
senkou_b: usize,
highs: VecDeque<Decimal>,
lows: VecDeque<Decimal>,
sb_highs: VecDeque<Decimal>,
sb_lows: VecDeque<Decimal>,
last_tenkan: Option<Decimal>,
last_kijun: Option<Decimal>,
last_span_a: Option<Decimal>,
last_span_b: Option<Decimal>,
last_close: Option<Decimal>,
}
#[derive(Debug, Clone)]
pub struct IchimokuLines {
pub tenkan: Option<Decimal>,
pub kijun: Option<Decimal>,
pub span_a: Option<Decimal>,
pub span_b: Option<Decimal>,
pub close: Option<Decimal>,
}
impl Ichimoku {
pub fn new(
name: impl Into<String>,
tenkan: usize,
kijun: usize,
senkou_b: usize,
) -> Result<Self, FinError> {
if tenkan == 0 { return Err(FinError::InvalidPeriod(tenkan)); }
if kijun == 0 { return Err(FinError::InvalidPeriod(kijun)); }
if senkou_b == 0 { return Err(FinError::InvalidPeriod(senkou_b)); }
let max_window = tenkan.max(kijun).max(senkou_b);
Ok(Self {
name: name.into(),
tenkan,
kijun,
senkou_b,
highs: VecDeque::with_capacity(max_window),
lows: VecDeque::with_capacity(max_window),
sb_highs: VecDeque::with_capacity(senkou_b),
sb_lows: VecDeque::with_capacity(senkou_b),
last_tenkan: None,
last_kijun: None,
last_span_a: None,
last_span_b: None,
last_close: None,
})
}
pub fn lines(&self) -> IchimokuLines {
IchimokuLines {
tenkan: self.last_tenkan,
kijun: self.last_kijun,
span_a: self.last_span_a,
span_b: self.last_span_b,
close: self.last_close,
}
}
fn midpoint(highs: &VecDeque<Decimal>, lows: &VecDeque<Decimal>, n: usize) -> Option<Decimal> {
if highs.len() < n || lows.len() < n {
return None;
}
let h = highs.iter().rev().take(n).copied().fold(Decimal::MIN, Decimal::max);
let l = lows.iter().rev().take(n).copied().fold(Decimal::MAX, Decimal::min);
Some((h + l) / Decimal::TWO)
}
}
impl Signal for Ichimoku {
fn name(&self) -> &str {
&self.name
}
fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
self.highs.push_back(bar.high);
self.lows.push_back(bar.low);
let max_window = self.tenkan.max(self.kijun).max(self.senkou_b);
if self.highs.len() > max_window {
self.highs.pop_front();
self.lows.pop_front();
}
self.sb_highs.push_back(bar.high);
self.sb_lows.push_back(bar.low);
if self.sb_highs.len() > self.senkou_b {
self.sb_highs.pop_front();
self.sb_lows.pop_front();
}
self.last_close = Some(bar.close);
self.last_tenkan = Self::midpoint(&self.highs, &self.lows, self.tenkan);
self.last_kijun = Self::midpoint(&self.highs, &self.lows, self.kijun);
self.last_span_a = match (self.last_tenkan, self.last_kijun) {
(Some(t), Some(k)) => Some((t + k) / Decimal::TWO),
_ => None,
};
self.last_span_b = Self::midpoint(&self.sb_highs, &self.sb_lows, self.senkou_b);
match self.last_tenkan {
Some(t) => Ok(SignalValue::Scalar(t)),
None => Ok(SignalValue::Unavailable),
}
}
fn is_ready(&self) -> bool {
self.last_tenkan.is_some()
}
fn period(&self) -> usize {
self.tenkan
}
fn reset(&mut self) {
self.highs.clear();
self.lows.clear();
self.sb_highs.clear();
self.sb_lows.clear();
self.last_tenkan = None;
self.last_kijun = None;
self.last_span_a = None;
self.last_span_b = None;
self.last_close = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ohlcv::OhlcvBar;
use crate::signals::Signal;
use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
use rust_decimal_macros::dec;
fn bar(h: &str, l: &str, c: &str) -> OhlcvBar {
let hi = Price::new(h.parse().unwrap()).unwrap();
let lo = Price::new(l.parse().unwrap()).unwrap();
let cl = Price::new(c.parse().unwrap()).unwrap();
OhlcvBar {
symbol: Symbol::new("X").unwrap(),
open: cl, high: hi, low: lo, close: cl,
volume: Quantity::zero(),
ts_open: NanoTimestamp::new(0),
ts_close: NanoTimestamp::new(1),
tick_count: 1,
}
}
#[test]
fn test_ichimoku_period_0_fails() {
assert!(Ichimoku::new("i", 0, 26, 52).is_err());
assert!(Ichimoku::new("i", 9, 0, 52).is_err());
assert!(Ichimoku::new("i", 9, 26, 0).is_err());
}
#[test]
fn test_ichimoku_tenkan_flat_price_equals_price() {
let mut ichi = Ichimoku::new("ichi", 3, 5, 7).unwrap();
let mut last = SignalValue::Unavailable;
for _ in 0..10 {
last = ichi.update_bar(&bar("100", "100", "100")).unwrap();
}
assert_eq!(last, SignalValue::Scalar(dec!(100)));
}
#[test]
fn test_ichimoku_unavailable_before_tenkan_bars() {
let mut ichi = Ichimoku::new("ichi", 3, 5, 7).unwrap();
assert_eq!(ichi.update_bar(&bar("100", "90", "95")).unwrap(), SignalValue::Unavailable);
assert_eq!(ichi.update_bar(&bar("100", "90", "95")).unwrap(), SignalValue::Unavailable);
assert!(!ichi.is_ready());
}
#[test]
fn test_ichimoku_lines_accessor() {
let mut ichi = Ichimoku::new("ichi", 3, 5, 7).unwrap();
for _ in 0..10 {
ichi.update_bar(&bar("100", "100", "100")).unwrap();
}
let lines = ichi.lines();
assert!(lines.tenkan.is_some());
assert!(lines.kijun.is_some());
assert!(lines.span_a.is_some());
assert!(lines.span_b.is_some());
}
#[test]
fn test_ichimoku_reset() {
let mut ichi = Ichimoku::new("ichi", 3, 5, 7).unwrap();
for _ in 0..10 {
ichi.update_bar(&bar("100", "100", "100")).unwrap();
}
assert!(ichi.is_ready());
ichi.reset();
assert!(!ichi.is_ready());
}
}