Skip to main content

kestrel_chartkit/indicator/
trix.rs

1use std::collections::HashMap;
2
3use crate::model::Bar;
4
5use super::smoothing::Ema;
6use super::{Indicator, IndicatorOutput};
7
8/// TRIX: the rate of change of a triple-smoothed price series.
9///
10/// Three exponential averages of `len` are chained over the close — `e1` over the price, `e2`
11/// over `e1`, `e3` over `e2` — and the published line is the relative change of that third
12/// average against its own previous value:
13///
14/// `TRIX_t = 100 * (e3_t / e3_{t-1} - 1)`
15///
16/// The previous value is the denominator, so the result is the percentage change *of the level
17/// it came from*. Other implementations divide by the current value or publish the absolute
18/// difference instead; those are different numbers under the same name, and this type does not
19/// offer them.
20///
21/// This is not TEMA: TEMA combines the three averages linearly to reduce lag and stays in price
22/// units, while TRIX differentiates the third average and is a rate.
23///
24/// Every stage uses the shared [`Ema`] with its first-sample seed, so each stage always receives
25/// a defined input — no stage is ever fed a substituted zero.
26///
27/// Units: percentage points per bar for the line, the signal and the histogram alike.
28///
29/// Per-bar outputs:
30/// - `value`: the TRIX line.
31/// - `extra["signal"]`: `Ema(signal_len)` over the published TRIX values, seeded with the first
32///   of them and published only from the `signal_len`-th on. Before that the key is absent — an
33///   unfinished signal line is left out, not zeroed.
34/// - `extra["hist"]`: `value - signal`, present exactly when the signal is.
35///
36/// First output: with the `len + 1`-th bar, so both `e3` values of the ratio are built from at
37/// least `len` inputs each. A bar whose previous `e3` is zero or non-finite produces no output
38/// rather than a division result that means nothing. [`Indicator::reset`] clears all four
39/// averages and the counters, so the next series starts deterministically.
40#[derive(Debug, Clone)]
41pub struct Trix {
42    len: usize,
43    signal_len: usize,
44    ema1: Ema,
45    ema2: Ema,
46    ema3: Ema,
47    prev_ema3: Option<f64>,
48    signal_ema: Ema,
49    bars_seen: usize,
50    lines_published: usize,
51}
52
53impl Trix {
54    pub fn new(len: usize, signal_len: usize) -> Self {
55        let len = len.max(1);
56        Self {
57            len,
58            signal_len: signal_len.max(1),
59            ema1: Ema::new(len),
60            ema2: Ema::new(len),
61            ema3: Ema::new(len),
62            prev_ema3: None,
63            signal_ema: Ema::new(signal_len.max(1)),
64            bars_seen: 0,
65            lines_published: 0,
66        }
67    }
68
69    pub fn with_defaults() -> Self {
70        Self::new(15, 9)
71    }
72}
73
74impl Indicator for Trix {
75    fn name(&self) -> &str {
76        "trix"
77    }
78
79    fn warmup_period(&self) -> usize {
80        self.len + 1
81    }
82
83    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
84        self.bars_seen += 1;
85
86        let e1 = self.ema1.update(bar.close)?;
87        let e2 = self.ema2.update(e1)?;
88        let e3 = self.ema3.update(e2)?;
89
90        let prev_ema3 = self.prev_ema3.replace(e3);
91        if self.bars_seen < self.len + 1 {
92            return None;
93        }
94        let prev_ema3 = prev_ema3?;
95        if prev_ema3 == 0.0 || !prev_ema3.is_finite() {
96            return None;
97        }
98
99        let line = 100.0 * (e3 / prev_ema3 - 1.0);
100        self.lines_published += 1;
101
102        let mut extra = HashMap::new();
103        let signal = self.signal_ema.update(line)?;
104        if self.lines_published >= self.signal_len {
105            extra.insert("signal".to_string(), signal);
106            extra.insert("hist".to_string(), line - signal);
107        }
108
109        Some(IndicatorOutput::with_extra(line, extra))
110    }
111
112    fn reset(&mut self) {
113        self.ema1.reset();
114        self.ema2.reset();
115        self.ema3.reset();
116        self.signal_ema.reset();
117        self.prev_ema3 = None;
118        self.bars_seen = 0;
119        self.lines_published = 0;
120    }
121}