egui_charts/studies/builtin/
trend_intensity.rs1use super::over_finite_bars;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::{update_window, usable_behind};
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8#[derive(Clone)]
14pub struct TrendIntensityIndex {
15 period: usize,
17 values: IndicatorValues,
18 color: Color32,
19 visible: bool,
20}
21
22impl TrendIntensityIndex {
23 pub fn new(period: usize) -> Self {
25 Self {
26 period: period.max(1),
27 values: IndicatorValues::new(),
28 color: palette::BLUE, visible: true,
30 }
31 }
32
33 pub fn with_color(mut self, color: Color32) -> Self {
35 self.color = color;
36 self
37 }
38}
39
40impl Default for TrendIntensityIndex {
41 fn default() -> Self {
42 Self::new(30)
43 }
44}
45
46impl TrendIntensityIndex {
47 fn compute(&self, data: &[Bar]) -> IndicatorValues {
49 let mut values = IndicatorValues::new();
50 let n = self.period;
51 let ma_len = n.saturating_mul(2);
52 let first = ma_len.saturating_add(n) - 2;
53 let dev: Vec<f64> = data
55 .iter()
56 .enumerate()
57 .map(|(i, bar)| {
58 if i + 1 < ma_len {
59 return 0.0;
60 }
61 let window = &data[i + 1 - ma_len..=i];
63 window.iter().map(|b| bar.close - b.close).sum::<f64>() / ma_len as f64
64 })
65 .collect();
66 for i in 0..data.len() {
67 if i < first {
68 values.push(IndicatorValue::None);
69 continue;
70 }
71 let (mut up, mut down) = (0.0, 0.0);
72 for &d in &dev[i + 1 - n..=i] {
73 if d > 0.0 {
74 up += d;
75 } else {
76 down -= d;
77 }
78 }
79 let total = up + down;
80 let tii = if total > 0.0 {
81 100.0 * up / total
82 } else {
83 50.0
84 };
85 values.push(IndicatorValue::Single(tii));
86 }
87
88 values
89 }
90}
91
92impl Indicator for TrendIntensityIndex {
93 fn source(&self) -> Option<crate::studies::StudySource> {
94 Some(crate::studies::StudySource::Price(
95 crate::model::PriceSource::Close,
96 ))
97 }
98
99 crate::studies::inputs::study_inputs!(self;
100 period: length("Period", 1, usize::MAX)
101 ; {});
102
103 fn name(&self) -> &str {
104 "TII"
105 }
106
107 fn desc(&self) -> &str {
108 "Trend Intensity Index - Measures trend strength"
109 }
110
111 fn calculate(&mut self, data: &[Bar]) {
112 self.update(data, 0);
113 }
114
115 fn update(&mut self, data: &[Bar], from: usize) {
116 let mut values = std::mem::take(&mut self.values);
117 update_window(
118 &mut values,
119 data,
120 from,
121 usable_behind(
122 self.period
123 .saturating_mul(2)
124 .saturating_add(self.period)
125 .saturating_sub(2),
126 |b| b.close.is_finite(),
127 ),
128 |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
129 );
130 self.values = values;
131 }
132
133 fn is_incremental(&self) -> bool {
134 true
135 }
136
137 fn values(&self) -> &IndicatorValues {
138 &self.values
139 }
140
141 fn colors(&self) -> Vec<Color32> {
142 vec![self.color]
143 }
144
145 fn set_colors(&mut self, colors: Vec<Color32>) {
146 if !colors.is_empty() {
147 self.color = colors[0];
148 }
149 }
150
151 fn is_overlay(&self) -> bool {
152 false
153 }
154
155 fn is_visible(&self) -> bool {
156 self.visible
157 }
158
159 fn set_visible(&mut self, visible: bool) {
160 self.visible = visible;
161 }
162
163 fn clone_box(&self) -> Box<dyn Indicator> {
164 Box::new(self.clone())
165 }
166
167 fn line_names(&self) -> Vec<String> {
168 vec![format!("TII({})", self.period)]
169 }
170}
171
172#[derive(Clone)]
176pub struct Qstick {
177 period: usize,
178 values: IndicatorValues,
179 color: Color32,
180 visible: bool,
181}
182
183impl Qstick {
184 pub fn new(period: usize) -> Self {
186 Self {
187 period: period.max(1),
188 values: IndicatorValues::new(),
189 color: palette::PURPLE,
190 visible: true,
191 }
192 }
193
194 pub fn with_color(mut self, color: Color32) -> Self {
196 self.color = color;
197 self
198 }
199}
200
201impl Default for Qstick {
202 fn default() -> Self {
203 Self::new(8)
204 }
205}
206
207impl Qstick {
208 fn compute(&self, data: &[Bar]) -> IndicatorValues {
209 let mut values = IndicatorValues::new();
210
211 if data.len() < self.period {
212 for _ in 0..data.len() {
213 values.push(IndicatorValue::None);
214 }
215 return values;
216 }
217
218 for i in 0..data.len() {
219 if i < self.period - 1 {
220 values.push(IndicatorValue::None);
221 } else {
222 let window = &data[i + 1 - self.period..=i];
223
224 let avg: f64 =
226 window.iter().map(|b| b.close - b.open).sum::<f64>() / self.period as f64;
227
228 values.push(IndicatorValue::Single(avg));
229 }
230 }
231 values
232 }
233}
234
235impl Indicator for Qstick {
236 fn value_format(&self) -> crate::scales::StudyFormat {
237 crate::scales::StudyFormat::Price
238 }
239
240 crate::studies::inputs::study_inputs!(self;
241 period: length("Period", 1, usize::MAX)
242 ; {});
243
244 fn name(&self) -> &str {
245 "Qstick"
246 }
247
248 fn desc(&self) -> &str {
249 "Qstick - Moving avg of candlestick body"
250 }
251
252 fn calculate(&mut self, data: &[Bar]) {
253 self.update(data, 0);
254 }
255
256 fn update(&mut self, data: &[Bar], from: usize) {
257 let mut values = std::mem::take(&mut self.values);
258 update_window(
259 &mut values,
260 data,
261 from,
262 |_data, from| from.saturating_sub(self.period - 1),
263 |data| self.compute(data),
264 );
265 self.values = values;
266 }
267
268 fn is_incremental(&self) -> bool {
269 true
270 }
271
272 fn values(&self) -> &IndicatorValues {
273 &self.values
274 }
275
276 fn colors(&self) -> Vec<Color32> {
277 vec![self.color]
278 }
279
280 fn set_colors(&mut self, colors: Vec<Color32>) {
281 if !colors.is_empty() {
282 self.color = colors[0];
283 }
284 }
285
286 fn is_overlay(&self) -> bool {
287 false
288 }
289
290 fn is_visible(&self) -> bool {
291 self.visible
292 }
293
294 fn set_visible(&mut self, visible: bool) {
295 self.visible = visible;
296 }
297
298 fn clone_box(&self) -> Box<dyn Indicator> {
299 Box::new(self.clone())
300 }
301
302 fn line_names(&self) -> Vec<String> {
303 vec![format!("Qstick({})", self.period)]
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use chrono::Utc;
311
312 fn make_bar(open: f64, close: f64) -> Bar {
313 let high = open.max(close) + 1.0;
314 let low = open.min(close) - 1.0;
315 Bar {
316 time: Utc::now(),
317 open,
318 high,
319 low,
320 close,
321 volume: 1000.0,
322 colors: None,
323 payload: None,
324 }
325 }
326
327 #[test]
329 fn tii_matches_published_definition() {
330 let rising: Vec<f64> = (0..90).map(|i| 100.0 + i as f64).collect();
331 let falling: Vec<f64> = rising.iter().rev().copied().collect();
332 let mixed = [
333 10.0, 12.0, 11.0, 14.0, 13.0, 15.0, 12.0, 16.0, 14.0, 13.0, 17.0, 15.0,
334 ];
335 let cases: [(usize, &[f64], usize, &[f64]); 4] = [
337 (30, &rising, 88, &[100.0, 100.0]),
338 (30, &falling, 88, &[0.0, 0.0]),
339 (3, &[0.1; 9], 7, &[50.0, 50.0]),
340 (
341 3,
342 &mixed,
343 7,
344 &[600.0 / 7.0, 75.0, 75.0, 75.0, 1800.0 / 23.0],
345 ),
346 ];
347 for (period, closes, first, expected) in cases {
348 let data: Vec<Bar> = closes.iter().map(|&c| make_bar(c, c)).collect();
349 let mut tii = TrendIntensityIndex::new(period);
350 tii.calculate(&data);
351 let got: Vec<Option<f64>> = tii
352 .values()
353 .to_rows()
354 .iter()
355 .map(|v| match v {
356 IndicatorValue::Single(x) => Some(*x),
357 _ => None,
358 })
359 .collect();
360 assert_eq!(got.len(), closes.len());
361 assert!(got[..first].iter().all(Option::is_none), "{got:?}");
362 for (i, want) in expected.iter().enumerate() {
363 let v = got[first + i].expect("value after warm-up");
364 assert!((v - want).abs() < 1e-12, "bar {}: {v} != {want}", first + i);
365 }
366 }
367 }
368
369 #[test]
370 fn test_qstick() {
371 let mut qs = Qstick::new(5);
372
373 let data: Vec<Bar> = (0..10)
375 .map(|i| make_bar(100.0 + i as f64, 102.0 + i as f64))
376 .collect();
377
378 qs.calculate(&data);
379
380 assert_eq!(qs.values.len(), 10);
381
382 let rows = qs.values.to_rows();
384 let IndicatorValue::Single(v) = rows.last().unwrap() else {
385 panic!("no value: {:?}", qs.values.to_rows().last().unwrap());
386 };
387 assert!(*v > 0.0);
388 }
389}