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}