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