Skip to main content

finance_query/backtesting/refs/
ichimoku.rs

1use crate::backtesting::strategy::StrategyContext;
2use crate::indicators::Indicator;
3
4use super::IndicatorRef;
5
6/// Ichimoku Cloud configuration.
7#[derive(Debug, Clone, Copy)]
8pub struct IchimokuConfig {
9    pub conversion: usize,
10    pub base: usize,
11    pub lagging: usize,
12    pub displacement: usize,
13}
14
15impl IchimokuConfig {
16    /// Get the Tenkan-sen (Conversion Line) reference.
17    pub fn conversion_line(&self) -> IchimokuConversionRef {
18        IchimokuConversionRef::new(self.conversion, self.base, self.lagging, self.displacement)
19    }
20
21    /// Get the Kijun-sen (Base Line) reference.
22    pub fn base_line(&self) -> IchimokuBaseRef {
23        IchimokuBaseRef::new(self.conversion, self.base, self.lagging, self.displacement)
24    }
25
26    /// Get the Senkou Span A (Leading Span A) reference.
27    pub fn leading_span_a(&self) -> IchimokuLeadingARef {
28        IchimokuLeadingARef::new(self.conversion, self.base, self.lagging, self.displacement)
29    }
30
31    /// Get the Senkou Span B (Leading Span B) reference.
32    pub fn leading_span_b(&self) -> IchimokuLeadingBRef {
33        IchimokuLeadingBRef::new(self.conversion, self.base, self.lagging, self.displacement)
34    }
35
36    /// Get the Chikou Span (Lagging Span) reference.
37    pub fn lagging_span(&self) -> IchimokuLaggingRef {
38        IchimokuLaggingRef::new(self.conversion, self.base, self.lagging, self.displacement)
39    }
40}
41
42/// Create an Ichimoku Cloud configuration with default periods (9, 26, 26, 26).
43///
44/// Senkou Span B is not independently configurable here; it is always `2 * base`.
45#[inline]
46pub fn ichimoku() -> IchimokuConfig {
47    IchimokuConfig {
48        conversion: 9,
49        base: 26,
50        lagging: 26,
51        displacement: 26,
52    }
53}
54
55/// Create an Ichimoku Cloud configuration with custom periods.
56///
57/// `lagging` is the Chikou Span back-displacement, not a Senkou Span B
58/// period — Senkou Span B is always `2 * base` and is not independently
59/// configurable here.
60#[inline]
61pub fn ichimoku_custom(
62    conversion: usize,
63    base: usize,
64    lagging: usize,
65    displacement: usize,
66) -> IchimokuConfig {
67    IchimokuConfig {
68        conversion,
69        base,
70        lagging,
71        displacement,
72    }
73}
74
75/// Ichimoku Conversion Line (Tenkan-sen) reference.
76#[derive(Debug, Clone)]
77pub struct IchimokuConversionRef {
78    pub conversion: usize,
79    pub base: usize,
80    pub lagging: usize,
81    pub displacement: usize,
82    key: String,
83}
84
85impl IchimokuConversionRef {
86    fn new(conversion: usize, base: usize, lagging: usize, displacement: usize) -> Self {
87        Self {
88            conversion,
89            base,
90            lagging,
91            displacement,
92            key: format!("ichimoku_conversion_{conversion}_{base}_{lagging}_{displacement}"),
93        }
94    }
95}
96
97impl IndicatorRef for IchimokuConversionRef {
98    fn key(&self) -> &str {
99        &self.key
100    }
101
102    fn required_indicators(&self) -> Vec<(String, Indicator)> {
103        vec![(
104            self.key.clone(),
105            Indicator::Ichimoku {
106                conversion: self.conversion,
107                base: self.base,
108                lagging: self.lagging,
109                displacement: self.displacement,
110            },
111        )]
112    }
113
114    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
115        ctx.indicator(self.key())
116    }
117
118    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
119        ctx.indicator_prev(self.key())
120    }
121}
122
123/// Ichimoku Base Line (Kijun-sen) reference.
124#[derive(Debug, Clone)]
125pub struct IchimokuBaseRef {
126    pub conversion: usize,
127    pub base: usize,
128    pub lagging: usize,
129    pub displacement: usize,
130    key: String,
131}
132
133impl IchimokuBaseRef {
134    fn new(conversion: usize, base: usize, lagging: usize, displacement: usize) -> Self {
135        Self {
136            conversion,
137            base,
138            lagging,
139            displacement,
140            key: format!("ichimoku_base_{conversion}_{base}_{lagging}_{displacement}"),
141        }
142    }
143}
144
145impl IndicatorRef for IchimokuBaseRef {
146    fn key(&self) -> &str {
147        &self.key
148    }
149
150    fn required_indicators(&self) -> Vec<(String, Indicator)> {
151        vec![(
152            self.key.clone(),
153            Indicator::Ichimoku {
154                conversion: self.conversion,
155                base: self.base,
156                lagging: self.lagging,
157                displacement: self.displacement,
158            },
159        )]
160    }
161
162    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
163        ctx.indicator(self.key())
164    }
165
166    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
167        ctx.indicator_prev(self.key())
168    }
169}
170
171/// Ichimoku Leading Span A (Senkou Span A) reference.
172#[derive(Debug, Clone)]
173pub struct IchimokuLeadingARef {
174    pub conversion: usize,
175    pub base: usize,
176    pub lagging: usize,
177    pub displacement: usize,
178    key: String,
179}
180
181impl IchimokuLeadingARef {
182    fn new(conversion: usize, base: usize, lagging: usize, displacement: usize) -> Self {
183        Self {
184            conversion,
185            base,
186            lagging,
187            displacement,
188            key: format!("ichimoku_leading_a_{conversion}_{base}_{lagging}_{displacement}"),
189        }
190    }
191}
192
193impl IndicatorRef for IchimokuLeadingARef {
194    fn key(&self) -> &str {
195        &self.key
196    }
197
198    fn required_indicators(&self) -> Vec<(String, Indicator)> {
199        vec![(
200            self.key.clone(),
201            Indicator::Ichimoku {
202                conversion: self.conversion,
203                base: self.base,
204                lagging: self.lagging,
205                displacement: self.displacement,
206            },
207        )]
208    }
209
210    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
211        ctx.indicator(self.key())
212    }
213
214    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
215        ctx.indicator_prev(self.key())
216    }
217}
218
219/// Ichimoku Leading Span B (Senkou Span B) reference.
220#[derive(Debug, Clone)]
221pub struct IchimokuLeadingBRef {
222    pub conversion: usize,
223    pub base: usize,
224    pub lagging: usize,
225    pub displacement: usize,
226    key: String,
227}
228
229impl IchimokuLeadingBRef {
230    fn new(conversion: usize, base: usize, lagging: usize, displacement: usize) -> Self {
231        Self {
232            conversion,
233            base,
234            lagging,
235            displacement,
236            key: format!("ichimoku_leading_b_{conversion}_{base}_{lagging}_{displacement}"),
237        }
238    }
239}
240
241impl IndicatorRef for IchimokuLeadingBRef {
242    fn key(&self) -> &str {
243        &self.key
244    }
245
246    fn required_indicators(&self) -> Vec<(String, Indicator)> {
247        vec![(
248            self.key.clone(),
249            Indicator::Ichimoku {
250                conversion: self.conversion,
251                base: self.base,
252                lagging: self.lagging,
253                displacement: self.displacement,
254            },
255        )]
256    }
257
258    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
259        ctx.indicator(self.key())
260    }
261
262    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
263        ctx.indicator_prev(self.key())
264    }
265}
266
267/// Chikou Span reference: the close from `lagging` bars ago.
268///
269/// This is the price level the chikou span is compared against at decision
270/// time, not the plot-aligned span itself — that array is stored shifted
271/// forward for charting (slot `j` holds `close[j + lagging]`) and would leak
272/// future closes if read directly at the current bar.
273///
274/// Reads base-timeframe candles directly, so inside an `htf()` wrapper it
275/// still lags by base bars, not HTF bars.
276#[derive(Debug, Clone)]
277pub struct IchimokuLaggingRef {
278    pub conversion: usize,
279    pub base: usize,
280    pub lagging: usize,
281    pub displacement: usize,
282    key: String,
283}
284
285impl IchimokuLaggingRef {
286    fn new(conversion: usize, base: usize, lagging: usize, displacement: usize) -> Self {
287        Self {
288            conversion,
289            base,
290            lagging,
291            displacement,
292            key: format!("ichimoku_lagging_{conversion}_{base}_{lagging}_{displacement}"),
293        }
294    }
295}
296
297impl IndicatorRef for IchimokuLaggingRef {
298    fn key(&self) -> &str {
299        &self.key
300    }
301
302    fn required_indicators(&self) -> Vec<(String, Indicator)> {
303        vec![] // computed directly from candle history, not the plot-aligned array
304    }
305
306    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
307        let idx = ctx.index.checked_sub(self.lagging)?;
308        Some(ctx.candles[idx].close)
309    }
310
311    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
312        let idx = ctx.index.checked_sub(self.lagging + 1)?;
313        Some(ctx.candles[idx].close)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::backtesting::config::BacktestConfig;
321    use crate::backtesting::engine::BacktestEngine;
322    use crate::backtesting::refs::{IndicatorRefExt, price};
323    use crate::backtesting::strategy::StrategyBuilder;
324    use crate::models::chart::Candle;
325    use std::collections::HashMap;
326
327    #[test]
328    fn test_ichimoku_keys() {
329        let ich = ichimoku();
330        assert_eq!(
331            ich.conversion_line().key(),
332            "ichimoku_conversion_9_26_26_26"
333        );
334        assert_eq!(ich.base_line().key(), "ichimoku_base_9_26_26_26");
335        assert_eq!(ich.leading_span_a().key(), "ichimoku_leading_a_9_26_26_26");
336        assert_eq!(ich.leading_span_b().key(), "ichimoku_leading_b_9_26_26_26");
337        assert_eq!(ich.lagging_span().key(), "ichimoku_lagging_9_26_26_26");
338    }
339
340    fn flat_candles(closes: &[f64]) -> Vec<Candle> {
341        closes
342            .iter()
343            .enumerate()
344            .map(|(i, &c)| Candle {
345                timestamp: i as i64 * 86_400,
346                open: c,
347                high: c,
348                low: c,
349                close: c,
350                volume: 1_000,
351                adj_close: Some(c),
352                provider_id: None,
353            })
354            .collect()
355    }
356
357    #[test]
358    fn test_lagging_ref_reads_close_from_lagging_bars_ago() {
359        let closes: Vec<f64> = (0..30).map(|i| i as f64).collect();
360        let candles = flat_candles(&closes);
361        let indicators = HashMap::new();
362        let lagging_ref = ichimoku_custom(9, 26, 5, 26).lagging_span();
363
364        let ctx_at = |index: usize| StrategyContext {
365            candles: &candles,
366            index,
367            position: None,
368            equity: 0.0,
369            indicators: &indicators,
370            extremes: None,
371            indicator_index: None,
372        };
373
374        assert_eq!(lagging_ref.value(&ctx_at(4)), None);
375        assert_eq!(lagging_ref.value(&ctx_at(5)), Some(closes[0]));
376        assert_eq!(lagging_ref.value(&ctx_at(10)), Some(closes[5]));
377        assert_eq!(lagging_ref.prev_value(&ctx_at(5)), None);
378        assert_eq!(lagging_ref.prev_value(&ctx_at(10)), Some(closes[4]));
379    }
380
381    #[test]
382    fn test_lagging_span_condition_signals_in_final_lagging_bars() {
383        // Crossing lands in the last `lagging` bars, which the old
384        // plot-aligned array always forced to None.
385        let lagging = 5;
386        let n = 40;
387        let mut closes = vec![100.0; n];
388        for (offset, close) in closes.iter_mut().enumerate().skip(36) {
389            *close = 100.0 + (offset - 35) as f64 * 5.0;
390        }
391        let candles = flat_candles(&closes);
392
393        let ich = ichimoku_custom(9, 26, lagging, 26);
394        let strategy = StrategyBuilder::new("Chikou Cross")
395            .entry(price().above_ref(ich.lagging_span()))
396            .exit(price().below_ref(ich.lagging_span()))
397            .warmup(lagging + 1)
398            .build();
399
400        let result = BacktestEngine::new(BacktestConfig::zero_cost())
401            .run("TEST", &candles, strategy)
402            .unwrap();
403
404        let last_lagging_bars_start = (n - lagging) as i64 * 86_400;
405        assert!(
406            result
407                .trades
408                .iter()
409                .any(|t| t.entry_timestamp >= last_lagging_bars_start)
410        );
411    }
412}