Skip to main content

egui_charts/studies/builtin/
true_strength.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/// True Strength Index (TSI), William Blau, "Momentum, Direction, and Divergence" (1995).
9///
10/// `TSI = 100 * EMA(EMA(m, long), short) / EMA(EMA(|m|, long), short)` with `m = close - previous close`,
11/// plus a signal line, the EMA of the TSI. Every EMA starts at its first input, and the price changes start
12/// at the first change, bar 1. Values start on bar `long + short`; earlier bars are `None`.
13#[derive(Clone)]
14pub struct TrueStrengthIndex {
15    /// Long period (typically 25)
16    long_period: usize,
17    /// Short period (typically 13)
18    short_period: usize,
19    /// Signal line period (typically 13)
20    signal_period: usize,
21    values: IndicatorValues,
22    stepper: Stepper<(usize, Option<f64>, [Smoothing; 5])>,
23    /// Colors: [TSI, Signal]
24    colors: Vec<Color32>,
25    visible: bool,
26}
27
28impl TrueStrengthIndex {
29    /// Create with `long_period`, `short_period`, `signal_period` in bars, each clamped to at
30    /// least one.
31    pub fn new(long_period: usize, short_period: usize, signal_period: usize) -> Self {
32        Self {
33            long_period: long_period.max(1),
34            short_period: short_period.max(1),
35            signal_period: signal_period.max(1),
36            values: IndicatorValues::new(),
37            stepper: Stepper::default(),
38            colors: vec![
39                palette::BLUE, // TSI
40                palette::PINK, // Signal
41            ],
42            visible: true,
43        }
44    }
45
46    /// Set plot colors in the order returned by [`Indicator::line_names`].
47    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
48        self.colors = colors;
49        self
50    }
51}
52
53impl Default for TrueStrengthIndex {
54    fn default() -> Self {
55        Self::new(25, 13, 13)
56    }
57}
58
59impl Indicator for TrueStrengthIndex {
60    fn source(&self) -> Option<crate::studies::StudySource> {
61        Some(crate::studies::StudySource::Price(
62            crate::model::PriceSource::Close,
63        ))
64    }
65
66    crate::studies::inputs::study_inputs!(self;
67        long_period: length("Long period", 1, usize::MAX),
68        short_period: length("Short period", 1, usize::MAX),
69        signal_period: length("Signal period", 1, usize::MAX)
70    ; {});
71
72    fn name(&self) -> &str {
73        "TSI"
74    }
75
76    fn desc(&self) -> &str {
77        "True Strength Index - Double-smoothed momentum"
78    }
79
80    fn calculate(&mut self, data: &[Bar]) {
81        self.update(data, 0);
82    }
83
84    fn update(&mut self, data: &[Bar], from: usize) {
85        self.stepper
86            .run(data, from, &mut self.values, |state, bar| {
87                if !bar.close.is_finite() {
88                    return IndicatorValue::None;
89                }
90                state.0 += 1;
91                let previous = state.1.replace(bar.close);
92                let Some(previous) = previous else {
93                    return IndicatorValue::None;
94                };
95                let change = bar.close - previous;
96                let first_alpha = 2.0 / (self.long_period as f64 + 1.0);
97                let second_alpha = 2.0 / (self.short_period as f64 + 1.0);
98                let first = state.2[0].next(change, 1, first_alpha);
99                let second = state.2[1].next(first, 1, second_alpha);
100                let abs_first = state.2[2].next(change.abs(), 1, first_alpha);
101                let abs_second = state.2[3].next(abs_first, 1, second_alpha);
102                let tsi = if abs_second != 0.0 {
103                    100.0 * second / abs_second
104                } else {
105                    0.0
106                };
107                let signal = state.2[4].next(tsi, 1, 2.0 / (self.signal_period as f64 + 1.0));
108                if state.0
109                    < self
110                        .long_period
111                        .saturating_add(self.short_period)
112                        .saturating_add(1)
113                {
114                    IndicatorValue::None
115                } else {
116                    IndicatorValue::Multiple(vec![tsi, signal])
117                }
118            });
119    }
120
121    fn is_incremental(&self) -> bool {
122        true
123    }
124
125    fn values(&self) -> &IndicatorValues {
126        &self.values
127    }
128
129    fn colors(&self) -> Vec<Color32> {
130        self.colors.clone()
131    }
132
133    fn set_colors(&mut self, colors: Vec<Color32>) {
134        if colors.len() >= 2 {
135            self.colors = colors;
136        }
137    }
138
139    fn is_overlay(&self) -> bool {
140        false
141    }
142
143    fn is_visible(&self) -> bool {
144        self.visible
145    }
146
147    fn set_visible(&mut self, visible: bool) {
148        self.visible = visible;
149    }
150
151    fn clone_box(&self) -> Box<dyn Indicator> {
152        Box::new(self.clone())
153    }
154
155    fn line_count(&self) -> usize {
156        2
157    }
158
159    fn line_names(&self) -> Vec<String> {
160        vec![
161            format!("TSI({},{})", self.long_period, self.short_period),
162            format!("Signal({})", self.signal_period),
163        ]
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::studies::test_support::make_bar;
171
172    /// Blau, "Momentum, Direction, and Divergence" (1995): TSI = 100 * EMA(EMA(m, r), s) / EMA(EMA(|m|, r), s),
173    /// m = close - previous close, signal = EMA(TSI, q), each EMA seeded with its first input, the change on bar 1.
174    /// Expected values computed independently from that definition.
175    #[test]
176    fn matches_blau_from_the_first_price_change() {
177        let data: Vec<Bar> = (0..45)
178            .map(|i| make_bar(100.0 + ((7 * i) % 11) as f64 * 0.5 + 0.25 * i as f64))
179            .collect();
180        let expected: [(usize, f64, f64); 37] = [
181            (8, 7.733160885593845, 23.105328984649127),
182            (9, 22.895446351093035, 23.00038766787108),
183            (10, 13.559429204247552, 18.279908436059316),
184            (11, -3.963663540739121, 7.158122447660097),
185            (12, 15.901736419454355, 11.529929433557227),
186            (13, 8.499761248063109, 10.014845340810169),
187            (14, 27.695613274277775, 18.855229307543972),
188            (15, 19.32380136640505, 19.08951533697451),
189            (16, 1.7491579409064582, 10.419336638940484),
190            (17, 19.782709407674567, 15.101023023307526),
191            (18, 11.553161103469721, 13.327092063388623),
192            (19, -5.360304089082964, 3.983393987152829),
193            (20, 15.152793499512779, 9.568093743332804),
194            (21, 7.999846354811868, 8.783970049072336),
195            (22, -8.191866413110832, 0.29605181798075186),
196            (23, 13.469528628351672, 6.882790223166212),
197            (24, 6.781726711786284, 6.832258467476247),
198            (25, 26.70845833486408, 16.770358401170164),
199            (26, 18.61951357662223, 17.694935988896198),
200            (27, 1.2122951925180985, 9.453615590707148),
201            (28, 19.4702811998816, 14.461948395294375),
202            (29, 11.331080578433813, 12.896514486864094),
203            (30, -5.528067840318502, 3.684223323272796),
204            (31, 15.056684499060852, 9.370453911166823),
205            (32, 7.93217590340769, 8.651314907287256),
206            (33, -8.242630490746267, 0.20434220827049465),
207            (34, 13.440660068325553, 6.822501138298024),
208            (35, 6.761481101120903, 6.791991119709463),
209            (36, 26.696890975662672, 16.744441047686067),
210            (37, 18.61129062462302, 17.677865836154545),
211            (38, 1.2060431173990807, 9.441954476776813),
212            (39, 19.466650340338127, 14.45430240855747),
213            (40, 11.328503246861152, 12.89140282770931),
214            (41, -5.5300128725456, 3.680694977581855),
215            (42, 15.05557110285549, 9.368133040218673),
216            (43, 7.931392380445344, 8.64976271033201),
217            (44, -8.24321803448801, 0.20327233792199983),
218        ];
219
220        let mut tsi = TrueStrengthIndex::new(5, 3, 3);
221        tsi.calculate(&data);
222
223        assert_eq!(tsi.values.len(), data.len());
224        assert!(
225            tsi.values.to_rows()[..8]
226                .iter()
227                .all(|v| matches!(v, IndicatorValue::None))
228        );
229        for (bar, main, signal) in expected {
230            let IndicatorValue::Multiple(ref got) = tsi.values.to_rows()[bar] else {
231                panic!(
232                    "bar {bar}: expected a value, got {:?}",
233                    tsi.values.to_rows()[bar]
234                );
235            };
236            assert_eq!(got.len(), tsi.line_count());
237            assert!(
238                (got[0] - main).abs() < 1e-12,
239                "bar {bar}: TSI {} vs {main}",
240                got[0]
241            );
242            assert!(
243                (got[1] - signal).abs() < 1e-12,
244                "bar {bar}: signal {} vs {signal}",
245                got[1]
246            );
247        }
248    }
249}