m4rs/
ichimoku.rs

1//! Ichimoku Kinko Hyo
2//!
3//! # Examples
4//! ```rust
5//! // Prepare candlesticks in some way
6//! let candlesticks = vec![
7//!     m4rs::Candlestick::new(1719400001, 100.0, 130.0, 90.0, 110.0, 1000.0),
8//!     m4rs::Candlestick::new(1719400002, 110.0, 140.0, 100.0, 130.0, 1000.0),
9//!     m4rs::Candlestick::new(1719400003, 130.0, 135.0, 120.0, 120.0, 1000.0),
10//!     m4rs::Candlestick::new(1719400004, 120.0, 130.0, 80.0, 95.0, 1000.0),
11//!     m4rs::Candlestick::new(1719400005, 90.0, 100.0, 70.0, 82.0, 1000.0),
12//! ];
13//!
14//! // Get Ichimoku calculation result with default common parameters
15//! let result = m4rs::ichimoku_default(&candlesticks);
16//! ```
17
18use crate::{Candlestick, Error, IndexEntry, IndexEntryLike};
19
20#[derive(Debug)]
21pub struct IchimokuEntry {
22    pub at: u64,
23    pub conversion_line: Option<f64>,
24    pub base_line: Option<f64>,
25    pub leading_span_a: Option<f64>,
26    pub leading_span_b: Option<f64>,
27    pub lagging_span: Option<f64>,
28}
29
30#[derive(Debug)]
31pub struct IchimokuData {
32    pub conversion_line: Vec<IndexEntry>,
33    pub base_line: Vec<IndexEntry>,
34    pub leading_span_a: Vec<IndexEntry>,
35    pub leading_span_b: Vec<IndexEntry>,
36    pub lagging_span: Vec<IndexEntry>,
37}
38
39impl IchimokuData {
40    pub fn get(&self, at: u64) -> Option<IchimokuEntry> {
41        let conversion_line = self
42            .conversion_line
43            .iter()
44            .find(|x| x.at == at)
45            .map(|x| x.value);
46        let base_line = self.base_line.iter().find(|x| x.at == at).map(|x| x.value);
47        let leading_span_a = self
48            .leading_span_a
49            .iter()
50            .find(|x| x.at == at)
51            .map(|x| x.value);
52        let leading_span_b = self
53            .leading_span_b
54            .iter()
55            .find(|x| x.at == at)
56            .map(|x| x.value);
57        let lagging_span = self
58            .lagging_span
59            .iter()
60            .find(|x| x.at == at)
61            .map(|x| x.value);
62        if [
63            conversion_line,
64            base_line,
65            leading_span_a,
66            leading_span_b,
67            lagging_span,
68        ]
69        .iter()
70        .all(|x| x.is_none())
71        {
72            return None;
73        }
74        Some(IchimokuEntry {
75            at,
76            conversion_line,
77            base_line,
78            leading_span_a,
79            leading_span_b,
80            lagging_span,
81        })
82    }
83}
84
85/// Returns Ichimoku Kinkohyo for given Candlestick list with default parameters
86pub fn ichimoku_default(entries: &[Candlestick]) -> Result<IchimokuData, Error> {
87    ichimoku(entries, 9, 26, 52, 26)
88}
89
90/// Returns Ichimoku Kinkohyo for given Candlestick list with custom parameters
91pub fn ichimoku(
92    entries: &[Candlestick],
93    conversion_line_len: usize,
94    base_line_len: usize,
95    leading_span_b_len: usize,
96    lagging_span: usize,
97) -> Result<IchimokuData, Error> {
98    Candlestick::validate_list(entries)?;
99
100    let base_line = calc_base_and_conversion_line(entries, base_line_len);
101    let conversion_line = calc_base_and_conversion_line(entries, conversion_line_len);
102    Ok(IchimokuData {
103        conversion_line: conversion_line.clone(),
104        base_line: base_line.clone(),
105        leading_span_a: calc_leading_span_a(&base_line, &conversion_line, lagging_span),
106        leading_span_b: calc_leading_span_b(entries, leading_span_b_len, lagging_span),
107        lagging_span: calc_lagging_span(entries, lagging_span),
108    })
109}
110
111fn calc_base_and_conversion_line(entries: &[Candlestick], line_len: usize) -> Vec<IndexEntry> {
112    if line_len == 0 {
113        return vec![];
114    }
115    let mut ret = Vec::<IndexEntry>::new();
116    for i in 0..entries.len() {
117        if entries.len() < i + line_len {
118            break;
119        }
120        let xs = &entries[i..i + line_len];
121        let highest = xs.iter().fold(
122            -1.0,
123            |z, x| if z == -1.0 || z < x.high { x.high } else { z },
124        );
125        let lowest = xs
126            .iter()
127            .fold(-1.0, |z, x| if z == -1.0 || z > x.low { x.low } else { z });
128        ret.push(IndexEntry {
129            at: xs.last().unwrap().at,
130            value: (highest + lowest) / 2.0,
131        });
132    }
133    ret
134}
135
136fn calc_leading_span_a(
137    base_line: &[IndexEntry],
138    conversion_line: &[IndexEntry],
139    span: usize,
140) -> Vec<IndexEntry> {
141    let entries: Vec<IndexEntry> = base_line
142        .iter()
143        .filter_map(|b| {
144            conversion_line
145                .iter()
146                .find(|c| c.at == b.at)
147                .map(|c| (b, c))
148        })
149        .map(|(b, c)| IndexEntry {
150            at: b.at,
151            value: (b.value + c.value) / 2.0,
152        })
153        .collect();
154    apply_lag(&entries, span, false)
155}
156
157fn calc_leading_span_b(entries: &[Candlestick], line_len: usize, span: usize) -> Vec<IndexEntry> {
158    apply_lag(
159        &calc_base_and_conversion_line(entries, line_len),
160        span,
161        false,
162    )
163}
164
165fn calc_lagging_span(entries: &[Candlestick], span: usize) -> Vec<IndexEntry> {
166    apply_lag(entries, span, true)
167}
168
169fn apply_lag(entries: &[impl IndexEntryLike], span: usize, backward: bool) -> Vec<IndexEntry> {
170    if entries.len() < 2 || span == 0 {
171        return entries.iter().map(|x| IndexEntry::from(x)).collect();
172    }
173    let len = entries.len();
174    let last = entries.last().unwrap();
175    let prev = &entries[len - 2];
176    let duration = (last.get_at() - prev.get_at()) * (span - 1) as u64;
177    entries
178        .iter()
179        .enumerate()
180        .map(|(i, x)| IndexEntry {
181            at: {
182                let pos = i as u32 + span as u32;
183                if 0 == pos && pos < len as u32 {
184                    entries[pos as usize].get_at()
185                } else if backward {
186                    x.get_at() - duration
187                } else {
188                    x.get_at() + duration
189                }
190            },
191            value: x.get_value(),
192        })
193        .collect()
194}