Skip to main content

egui_charts/studies/builtin/
trix.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// TRIX - Triple Exponential Avg
9/// A momentum oscillator that shows the rate of change of a triple-smoothed EMA
10/// Filters out insignificant price movements
11#[derive(Clone)]
12pub struct TRIX {
13    period: usize,
14    signal_period: usize,
15    values: IndicatorValues,
16    stepper: Stepper<State>,
17    /// Colors: [TRIX line, Signal line]
18    colors: Vec<Color32>,
19    visible: bool,
20}
21
22#[derive(Clone, Default)]
23struct State {
24    ema: [Smoothing; 3],
25    signal: Smoothing,
26    previous: Option<f64>,
27    count: usize,
28    bars: usize,
29    pending: Vec<(usize, [f64; 2])>,
30}
31
32impl TRIX {
33    /// Create with a lookback of `period` bars, clamped to at least 1.
34    pub fn new(period: usize) -> Self {
35        Self {
36            period: period.max(1),
37            signal_period: 9,
38            values: IndicatorValues::new(),
39            stepper: Stepper::default(),
40            colors: vec![
41                palette::BRIGHT_RED, // TRIX
42                palette::ORANGE,     // Signal
43            ],
44            visible: true,
45        }
46    }
47
48    /// Set the signal EMA period in bars, clamped to at least one.
49    pub fn with_signal(mut self, signal_period: usize) -> Self {
50        self.signal_period = signal_period.max(1);
51        self
52    }
53
54    /// Set plot colors in the order returned by [`Indicator::line_names`].
55    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
56        self.colors = colors;
57        self
58    }
59}
60
61/// Construct with the conventional default parameters.
62impl Default for TRIX {
63    fn default() -> Self {
64        Self::new(18)
65    }
66}
67
68impl Indicator for TRIX {
69    fn source(&self) -> Option<crate::studies::StudySource> {
70        Some(crate::studies::StudySource::Price(
71            crate::model::PriceSource::Close,
72        ))
73    }
74
75    crate::studies::inputs::study_inputs!(self;
76        period: length("Period", 1, usize::MAX),
77        signal_period: length("Signal period", 1, usize::MAX)
78    ; {});
79
80    fn value_format(&self) -> crate::scales::StudyFormat {
81        crate::scales::StudyFormat::Percent
82    }
83
84    fn name(&self) -> &str {
85        "TRIX"
86    }
87
88    fn desc(&self) -> &str {
89        "TRIX - Rate of change of triple-smoothed EMA"
90    }
91
92    fn calculate(&mut self, data: &[Bar]) {
93        self.update(data, 0);
94    }
95
96    fn update(&mut self, data: &[Bar], from: usize) {
97        let required = self.period.saturating_mul(3).saturating_add(1);
98        self.stepper
99            .run(data, from, &mut self.values, |state, bar| {
100                let index = state.bars;
101                state.bars += 1;
102                if !bar.close.is_finite() {
103                    return IndicatorValue::None;
104                }
105                state.count += 1;
106                let alpha = 2.0 / (self.period as f64 + 1.0);
107                let ema1 = state.ema[0].next(bar.close, self.period, alpha);
108                let ema2 = state.ema[1].next(ema1, self.period, alpha);
109                let ema3 = state.ema[2].next(ema2, self.period, alpha);
110                let previous = state.previous.replace(ema3).unwrap_or(f64::NAN);
111                let trix = if previous == 0.0 {
112                    f64::NAN
113                } else {
114                    (ema3 - previous) / previous * 100.0
115                };
116                let signal = state.signal.next(
117                    trix,
118                    self.signal_period,
119                    2.0 / (self.signal_period as f64 + 1.0),
120                );
121                if state.count < required {
122                    if trix.is_finite() {
123                        state.pending.push((index, [trix, signal]));
124                    }
125                    IndicatorValue::None
126                } else {
127                    IndicatorValue::Multiple(vec![trix, signal])
128                }
129            });
130        // Crossing the batch warm-up threshold reveals the preceding TRIX values too.
131        for &(index, row) in &self.stepper.state().pending {
132            self.values.set(
133                index,
134                if self.stepper.state().count < required {
135                    IndicatorValue::None
136                } else {
137                    IndicatorValue::Multiple(row.to_vec())
138                },
139            );
140        }
141    }
142
143    fn is_incremental(&self) -> bool {
144        true
145    }
146
147    fn values(&self) -> &IndicatorValues {
148        &self.values
149    }
150
151    fn colors(&self) -> Vec<Color32> {
152        self.colors.clone()
153    }
154
155    fn set_colors(&mut self, colors: Vec<Color32>) {
156        if colors.len() >= 2 {
157            self.colors = colors;
158        }
159    }
160
161    fn is_overlay(&self) -> bool {
162        false // Separate pane
163    }
164
165    fn is_visible(&self) -> bool {
166        self.visible
167    }
168
169    fn set_visible(&mut self, visible: bool) {
170        self.visible = visible;
171    }
172
173    fn clone_box(&self) -> Box<dyn Indicator> {
174        Box::new(self.clone())
175    }
176
177    fn line_names(&self) -> Vec<String> {
178        vec![
179            format!("TRIX({})", self.period),
180            format!("Signal({})", self.signal_period),
181        ]
182    }
183}