use std::{fmt::Display, num::NonZero};
use crate::{
Indicator, IndicatorConfig, IndicatorConfigBuilder, Price,
internals::{BarAction, BarState, RingBuffer, RollingExtremes},
};
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct IchimokuConfig {
tenkan_length: usize,
kijun_length: usize,
senkou_b_length: usize,
displacement: usize,
}
impl IndicatorConfig for IchimokuConfig {
type Builder = IchimokuBuilder;
fn builder() -> Self::Builder {
IchimokuBuilder::new()
}
fn source(&self) -> crate::PriceSource {
crate::PriceSource::Close
}
fn convergence(&self) -> usize {
let span_a = self.kijun_length.max(self.tenkan_length) + self.displacement;
let span_b = self.senkou_b_length + self.displacement + 1;
span_a.max(span_b)
}
fn to_builder(&self) -> Self::Builder {
IchimokuBuilder {
tenkan_length: Some(self.tenkan_length),
kijun_length: Some(self.kijun_length),
senkou_b_length: Some(self.senkou_b_length),
displacement: Some(self.displacement),
}
}
}
impl IchimokuConfig {
#[must_use]
pub fn tenkan_length(&self) -> usize {
self.tenkan_length
}
#[must_use]
pub fn kijun_length(&self) -> usize {
self.kijun_length
}
#[must_use]
pub fn senkou_b_length(&self) -> usize {
self.senkou_b_length
}
#[must_use]
pub fn displacement(&self) -> usize {
self.displacement
}
}
impl Default for IchimokuConfig {
fn default() -> Self {
Self {
tenkan_length: 9,
kijun_length: 26,
senkou_b_length: 52,
displacement: 26,
}
}
}
impl Display for IchimokuConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"IchimokuConfig(tenkan: {}, kijun: {}, senkou_b: {}, displacement: {})",
self.tenkan_length, self.kijun_length, self.senkou_b_length, self.displacement
)
}
}
pub struct IchimokuBuilder {
tenkan_length: Option<usize>,
kijun_length: Option<usize>,
senkou_b_length: Option<usize>,
displacement: Option<usize>,
}
impl IchimokuBuilder {
fn new() -> Self {
Self {
tenkan_length: None,
kijun_length: None,
senkou_b_length: None,
displacement: None,
}
}
#[must_use]
pub fn default(mut self) -> Self {
self.tenkan_length.replace(9);
self.kijun_length.replace(26);
self.senkou_b_length.replace(52);
self.displacement.replace(26);
self
}
#[must_use]
pub fn tenkan_length(mut self, value: NonZero<usize>) -> Self {
self.tenkan_length.replace(value.get());
self
}
#[must_use]
pub fn kijun_length(mut self, value: NonZero<usize>) -> Self {
self.kijun_length.replace(value.get());
self
}
#[must_use]
pub fn senkou_b_length(mut self, value: NonZero<usize>) -> Self {
self.senkou_b_length.replace(value.get());
self
}
#[must_use]
pub fn displacement(mut self, value: NonZero<usize>) -> Self {
self.displacement.replace(value.get());
self
}
}
impl IndicatorConfigBuilder<IchimokuConfig> for IchimokuBuilder {
fn source(self, _source: crate::PriceSource) -> Self {
self
}
fn build(self) -> IchimokuConfig {
IchimokuConfig {
tenkan_length: self.tenkan_length.expect("tenkan_length is required"),
kijun_length: self.kijun_length.expect("kijun_length is required"),
senkou_b_length: self.senkou_b_length.expect("senkou_b_length is required"),
displacement: self.displacement.expect("displacement is required"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IchimokuValue {
tenkan: Price,
kijun: Price,
senkou_a: Price,
senkou_b: Price,
chikou_close: Price,
}
impl IchimokuValue {
#[inline]
#[must_use]
pub fn tenkan(&self) -> Price {
self.tenkan
}
#[inline]
#[must_use]
pub fn kijun(&self) -> Price {
self.kijun
}
#[inline]
#[must_use]
pub fn senkou_a(&self) -> Price {
self.senkou_a
}
#[inline]
#[must_use]
pub fn senkou_b(&self) -> Price {
self.senkou_b
}
#[inline]
#[must_use]
pub fn chikou_close(&self) -> Price {
self.chikou_close
}
}
impl Display for IchimokuValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Ichimoku(tenkan: {}, kijun: {}, senkou_a: {}, senkou_b: {}, chikou_close: {})",
self.tenkan, self.kijun, self.senkou_a, self.senkou_b, self.chikou_close
)
}
}
#[derive(Clone, Debug)]
pub struct Ichimoku {
config: IchimokuConfig,
bar_state: BarState,
tenkan_extremes: RollingExtremes,
kijun_extremes: RollingExtremes,
senkou_b_extremes: RollingExtremes,
senkou_a_buffer: RingBuffer,
senkou_b_buffer: RingBuffer,
current: Option<IchimokuValue>,
}
impl Indicator for Ichimoku {
type Config = IchimokuConfig;
type Output = IchimokuValue;
fn new(config: Self::Config) -> Self {
Self {
config,
bar_state: BarState::new(crate::PriceSource::Close),
tenkan_extremes: RollingExtremes::new(config.tenkan_length),
kijun_extremes: RollingExtremes::new(config.kijun_length),
senkou_b_extremes: RollingExtremes::new(config.senkou_b_length),
senkou_a_buffer: RingBuffer::new(config.displacement),
senkou_b_buffer: RingBuffer::new(config.displacement + 1),
current: None,
}
}
fn compute(&mut self, ohlcv: &impl crate::Ohlcv) -> Option<Self::Output> {
self.current = match self.bar_state.handle(ohlcv) {
BarAction::Advance(price) => {
let tenkan_sen = Self::price_midpoint(self.tenkan_extremes.push(ohlcv));
let kijun_sen = Self::price_midpoint(self.kijun_extremes.push(ohlcv));
let displaced_b = Self::price_midpoint(self.senkou_b_extremes.push(ohlcv))
.and_then(|sb| self.senkou_b_buffer.push(sb));
if let (Some(tenkan), Some(kijun)) = (tenkan_sen, kijun_sen) {
let displaced_a = self.senkou_a_buffer.push(tenkan.midpoint(kijun));
if let (Some(senkou_a), Some(senkou_b)) = (displaced_a, displaced_b) {
Some(IchimokuValue {
tenkan,
kijun,
senkou_a,
senkou_b,
chikou_close: price,
})
} else {
None
}
} else {
None
}
}
BarAction::Repaint(price) => {
let tenkan_sen = Self::price_midpoint(self.tenkan_extremes.replace(ohlcv));
let kijun_sen = Self::price_midpoint(self.kijun_extremes.replace(ohlcv));
if let Some(senkou_b) = Self::price_midpoint(self.senkou_b_extremes.replace(ohlcv))
{
self.senkou_b_buffer.replace(senkou_b);
}
if let (Some(tenkan), Some(kijun)) = (tenkan_sen, kijun_sen) {
self.senkou_a_buffer.replace(tenkan.midpoint(kijun));
self.current.map(|prev| IchimokuValue {
tenkan,
kijun,
senkou_a: prev.senkou_a,
senkou_b: prev.senkou_b,
chikou_close: price,
})
} else {
None
}
}
};
self.current
}
fn value(&self) -> Option<Self::Output> {
self.current
}
}
impl Ichimoku {
fn price_midpoint(extremes: Option<(Price, Price)>) -> Option<Price> {
extremes.map(|(highest_high, lowest_low)| highest_high.midpoint(lowest_low))
}
}
impl Display for Ichimoku {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Ichimoku(tenkan: {}, kijun: {}, senkou_b: {}, displacement: {})",
self.config.tenkan_length,
self.config.kijun_length,
self.config.senkou_b_length,
self.config.displacement
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{nz, ohlc};
fn default_config() -> IchimokuConfig {
IchimokuConfig::builder().default().build()
}
fn small_config() -> IchimokuConfig {
IchimokuConfig::builder()
.tenkan_length(nz(2))
.kijun_length(nz(3))
.senkou_b_length(nz(4))
.displacement(nz(2))
.build()
}
fn ichimoku(config: IchimokuConfig) -> Ichimoku {
Ichimoku::new(config)
}
mod filling {
use super::*;
#[test]
fn none_until_convergence() {
let mut ich = ichimoku(small_config());
for t in 1..=6 {
assert_eq!(
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t)),
None,
"should be None at bar {t}"
);
}
}
#[test]
fn returns_value_at_convergence() {
let mut ich = ichimoku(small_config());
for t in 1..=6 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
assert!(ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 7)).is_some());
}
}
mod computation {
use super::*;
#[test]
fn tenkan_is_midpoint_of_window_extremes() {
let mut ich = ichimoku(small_config());
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 1));
ich.compute(&ohlc(10.0, 18.0, 6.0, 14.0, 2));
ich.compute(&ohlc(10.0, 22.0, 4.0, 16.0, 3));
ich.compute(&ohlc(10.0, 19.0, 7.0, 13.0, 4));
ich.compute(&ohlc(10.0, 21.0, 3.0, 12.0, 5));
ich.compute(&ohlc(10.0, 23.0, 6.0, 16.0, 6));
let val = ich.compute(&ohlc(10.0, 25.0, 8.0, 17.0, 7)).unwrap();
assert!((val.tenkan() - 15.5).abs() < 1e-10);
}
#[test]
fn kijun_is_midpoint_of_window_extremes() {
let mut ich = ichimoku(small_config());
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 1));
ich.compute(&ohlc(10.0, 18.0, 6.0, 14.0, 2));
ich.compute(&ohlc(10.0, 22.0, 4.0, 16.0, 3));
ich.compute(&ohlc(10.0, 19.0, 7.0, 13.0, 4));
ich.compute(&ohlc(10.0, 21.0, 3.0, 12.0, 5));
ich.compute(&ohlc(10.0, 23.0, 6.0, 16.0, 6));
let val = ich.compute(&ohlc(10.0, 25.0, 8.0, 17.0, 7)).unwrap();
assert!((val.kijun() - 14.0).abs() < 1e-10);
}
#[test]
fn chikou_close_equals_current_close() {
let mut ich = ichimoku(small_config());
for t in 1..=6 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
let val = ich.compute(&ohlc(10.0, 20.0, 5.0, 42.0, 7)).unwrap();
assert!((val.chikou_close() - 42.0).abs() < 1e-10);
}
#[test]
fn senkou_a_and_b_are_displaced() {
let config = IchimokuConfig::builder()
.tenkan_length(nz(2))
.kijun_length(nz(2))
.senkou_b_length(nz(2))
.displacement(nz(2))
.build();
let mut ich = ichimoku(config);
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 1));
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 2));
ich.compute(&ohlc(10.0, 30.0, 2.0, 15.0, 3));
ich.compute(&ohlc(10.0, 28.0, 3.0, 15.0, 4));
let v5 = ich.compute(&ohlc(10.0, 26.0, 4.0, 15.0, 5)).unwrap();
assert!((v5.senkou_a() - 16.0).abs() < 1e-10);
assert!((v5.senkou_b() - 12.5).abs() < 1e-10);
}
}
mod sliding {
use super::*;
#[test]
fn values_change_as_window_slides() {
let mut ich = ichimoku(small_config());
for t in 1..=7 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
let v1 = ich.value().unwrap();
let v2 = ich.compute(&ohlc(50.0, 60.0, 40.0, 55.0, 8)).unwrap();
assert!(
(v1.tenkan() - v2.tenkan()).abs() > 1e-10,
"tenkan should change as window slides"
);
}
}
mod repaint {
use super::*;
#[test]
fn updates_current_bar() {
let mut ich = ichimoku(small_config());
for t in 1..=6 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
let v1 = ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 7)).unwrap();
let v2 = ich.compute(&ohlc(10.0, 40.0, 1.0, 15.0, 7)).unwrap();
assert!(
(v1.tenkan() - v2.tenkan()).abs() > 1e-10,
"repaint with different extremes should change tenkan"
);
}
#[test]
fn multiple_repaints_match_single() {
let mut ich = ichimoku(small_config());
for t in 1..=6 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
ich.compute(&ohlc(10.0, 18.0, 7.0, 14.0, 7));
ich.compute(&ohlc(10.0, 19.0, 6.0, 16.0, 7)); let final_val = ich.compute(&ohlc(10.0, 21.0, 4.0, 13.0, 7)).unwrap();
let mut clean = ichimoku(small_config());
for t in 1..=6 {
clean.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
let expected = clean.compute(&ohlc(10.0, 21.0, 4.0, 13.0, 7)).unwrap();
assert!((final_val.tenkan() - expected.tenkan()).abs() < 1e-10);
assert!((final_val.kijun() - expected.kijun()).abs() < 1e-10);
assert!((final_val.chikou_close() - expected.chikou_close()).abs() < 1e-10);
}
#[test]
fn repaint_during_filling() {
let mut ich = ichimoku(small_config());
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 1));
ich.compute(&ohlc(10.0, 25.0, 3.0, 14.0, 1)); assert_eq!(ich.compute(&ohlc(10.0, 18.0, 6.0, 14.0, 2)), None);
}
#[test]
fn advance_after_repaint() {
let mut ich = ichimoku(small_config());
for t in 1..=6 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 7));
ich.compute(&ohlc(10.0, 25.0, 3.0, 18.0, 7)); let after = ich.compute(&ohlc(10.0, 22.0, 7.0, 16.0, 8));
assert!(after.is_some());
}
}
mod live_data {
use super::*;
#[test]
fn mixed_open_and_closed_bars() {
let mut ich = ichimoku(small_config());
for t in 1..=5 {
ich.compute(&ohlc(10.0, 18.0, 6.0, 14.0, t));
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t)); }
assert_eq!(ich.compute(&ohlc(10.0, 19.0, 4.0, 13.0, 6)), None);
ich.compute(&ohlc(10.0, 21.0, 3.0, 12.0, 6));
let v1 = ich.compute(&ohlc(10.0, 22.0, 6.0, 16.0, 7)).unwrap();
let v2 = ich.compute(&ohlc(10.0, 25.0, 2.0, 17.0, 7)).unwrap();
assert!(
(v1.tenkan() - v2.tenkan()).abs() > 1e-10,
"repaint should change tenkan"
);
let v3 = ich.compute(&ohlc(10.0, 24.0, 4.0, 18.0, 8));
assert!(v3.is_some());
}
}
mod clone {
use super::*;
#[test]
fn produces_independent_state() {
let mut ich = ichimoku(small_config());
for t in 1..=7 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
let mut cloned = ich.clone();
let orig = ich.compute(&ohlc(50.0, 80.0, 1.0, 60.0, 8)).unwrap();
let clone_val = cloned.compute(&ohlc(14.0, 16.0, 14.0, 15.0, 8)).unwrap();
assert!(
(orig.tenkan() - clone_val.tenkan()).abs() > 1e-10,
"divergent inputs should give different tenkan"
);
}
}
mod config {
use super::*;
use std::collections::HashSet;
#[test]
fn default_sets_standard_values() {
let config = default_config();
assert_eq!(config.tenkan_length(), 9);
assert_eq!(config.kijun_length(), 26);
assert_eq!(config.senkou_b_length(), 52);
assert_eq!(config.displacement(), 26);
}
#[test]
fn convergence_is_senkou_b_plus_displacement_plus_one() {
let config = default_config();
assert_eq!(config.convergence(), 79);
let config = small_config();
assert_eq!(config.convergence(), 7);
}
#[test]
fn source_is_close() {
let config = default_config();
assert_eq!(config.source(), crate::PriceSource::Close);
}
#[test]
#[should_panic(expected = "tenkan_length is required")]
fn panics_without_tenkan() {
let _ = IchimokuConfig::builder()
.kijun_length(nz(26))
.senkou_b_length(nz(52))
.displacement(nz(26))
.build();
}
#[test]
#[should_panic(expected = "kijun_length is required")]
fn panics_without_kijun() {
let _ = IchimokuConfig::builder()
.tenkan_length(nz(9))
.senkou_b_length(nz(52))
.displacement(nz(26))
.build();
}
#[test]
#[should_panic(expected = "senkou_b_length is required")]
fn panics_without_senkou_b() {
let _ = IchimokuConfig::builder()
.tenkan_length(nz(9))
.kijun_length(nz(26))
.displacement(nz(26))
.build();
}
#[test]
#[should_panic(expected = "displacement is required")]
fn panics_without_displacement() {
let _ = IchimokuConfig::builder()
.tenkan_length(nz(9))
.kijun_length(nz(26))
.senkou_b_length(nz(52))
.build();
}
#[test]
fn eq_and_hash() {
let a = default_config();
let b = default_config();
let c = small_config();
let mut set = HashSet::new();
set.insert(a);
assert!(set.contains(&b));
assert!(!set.contains(&c));
}
#[test]
fn custom_parameters() {
let config = IchimokuConfig::builder()
.tenkan_length(nz(7))
.kijun_length(nz(22))
.senkou_b_length(nz(44))
.displacement(nz(22))
.build();
assert_eq!(config.tenkan_length(), 7);
assert_eq!(config.kijun_length(), 22);
assert_eq!(config.senkou_b_length(), 44);
assert_eq!(config.displacement(), 22);
}
#[test]
fn to_builder_roundtrip() {
let config = IchimokuConfig::builder().default().build();
assert_eq!(config.to_builder().build(), config);
}
}
mod display {
use super::*;
#[test]
fn formats_indicator() {
let ich = ichimoku(default_config());
assert_eq!(
ich.to_string(),
"Ichimoku(tenkan: 9, kijun: 26, senkou_b: 52, displacement: 26)"
);
}
#[test]
fn formats_config() {
let config = default_config();
assert_eq!(
config.to_string(),
"IchimokuConfig(tenkan: 9, kijun: 26, senkou_b: 52, displacement: 26)"
);
}
#[test]
fn formats_value() {
let val = IchimokuValue {
tenkan: 1.0,
kijun: 2.0,
senkou_a: 3.0,
senkou_b: 4.0,
chikou_close: 5.0,
};
assert_eq!(
val.to_string(),
"Ichimoku(tenkan: 1, kijun: 2, senkou_a: 3, senkou_b: 4, chikou_close: 5)"
);
}
}
mod value_accessor {
use super::*;
#[test]
fn none_before_convergence() {
let ich = ichimoku(small_config());
assert_eq!(ich.value(), None);
}
#[test]
fn returns_current_value() {
let mut ich = ichimoku(small_config());
for t in 1..=7 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
assert!(ich.value().is_some());
}
#[test]
fn matches_last_compute() {
let mut ich = ichimoku(small_config());
for t in 1..=6 {
ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, t));
}
let computed = ich.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 7));
assert_eq!(ich.value(), computed);
}
}
}