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#[derive(Clone)]
14pub struct TrueStrengthIndex {
15 long_period: usize,
17 short_period: usize,
19 signal_period: usize,
21 values: IndicatorValues,
22 stepper: Stepper<(usize, Option<f64>, [Smoothing; 5])>,
23 colors: Vec<Color32>,
25 visible: bool,
26}
27
28impl TrueStrengthIndex {
29 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, palette::PINK, ],
42 visible: true,
43 }
44 }
45
46 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 #[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}