Skip to main content

kestrel_chartkit/indicator/
t3.rs

1use crate::model::Bar;
2
3use super::smoothing::Ema;
4use super::{Indicator, IndicatorOutput};
5
6/// Tillson T3: a weighted combination of six chained exponential averages.
7///
8/// With `e1 .. e6` the successive `Ema(period)` stages over the close and the volume factor `v`:
9///
10/// ```text
11/// c1 = -v^3
12/// c2 =  3*v^2 + 3*v^3
13/// c3 = -6*v^2 - 3*v - 3*v^3
14/// c4 =  1 + 3*v + v^3 + 3*v^2
15/// T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3
16/// ```
17///
18/// `v` is a shape parameter in `0..=1`, not market volume — the name is historical. At `v = 0`
19/// every coefficient but `c4` vanishes and T3 is exactly the third EMA; larger `v` adds the
20/// higher stages with alternating signs, which reduces lag at the price of overshoot. That
21/// overshoot is left in the output: clipping it away would hide what the parameter does.
22///
23/// This is neither TEMA nor a plain triple EMA — both combine fewer stages with different
24/// weights.
25///
26/// Output: `value` in the price units of the series.
27///
28/// First output: with the `period`-th bar, so every stage has consumed at least `period` inputs.
29/// All stages use the shared [`Ema`] with its first-sample seed, so no stage is ever fed a
30/// substituted value. [`Indicator::reset`] clears all six averages and the counter, so the next
31/// series starts deterministically.
32#[derive(Debug, Clone)]
33pub struct T3 {
34    period: usize,
35    stages: [Ema; 6],
36    coefficients: [f64; 4],
37    bars_seen: usize,
38}
39
40impl T3 {
41    pub fn new(period: usize, v: f64) -> Self {
42        let period = period.max(1);
43        let v2 = v * v;
44        let v3 = v2 * v;
45        Self {
46            period,
47            stages: [Ema::new(period); 6],
48            coefficients: [
49                -v3,
50                3.0 * v2 + 3.0 * v3,
51                -6.0 * v2 - 3.0 * v - 3.0 * v3,
52                1.0 + 3.0 * v + v3 + 3.0 * v2,
53            ],
54            bars_seen: 0,
55        }
56    }
57
58    pub fn with_defaults() -> Self {
59        Self::new(5, 0.7)
60    }
61}
62
63impl Indicator for T3 {
64    fn name(&self) -> &str {
65        "t3"
66    }
67
68    fn warmup_period(&self) -> usize {
69        self.period
70    }
71
72    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
73        self.bars_seen += 1;
74
75        let mut value = bar.close;
76        let mut stage_values = [0.0; 6];
77        for (stage, slot) in self.stages.iter_mut().zip(stage_values.iter_mut()) {
78            value = stage.update(value)?;
79            *slot = value;
80        }
81
82        if self.bars_seen < self.period {
83            return None;
84        }
85
86        let [c1, c2, c3, c4] = self.coefficients;
87        let t3 = c1 * stage_values[5]
88            + c2 * stage_values[4]
89            + c3 * stage_values[3]
90            + c4 * stage_values[2];
91
92        Some(IndicatorOutput::new(t3))
93    }
94
95    fn reset(&mut self) {
96        for stage in &mut self.stages {
97            stage.reset();
98        }
99        self.bars_seen = 0;
100    }
101}