egui_charts/studies/builtin/
commodity_selection.rs1use super::clamp_param;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::{Stepper, update_window};
5use crate::studies::{ADX, ATR, Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8#[derive(Clone)]
17pub struct CommoditySelectionIndex {
18 period: usize,
20 point_value: f64,
21 margin: f64,
22 commission: f64,
23 values: IndicatorValues,
24 adx: ADX,
25 atr: ATR,
26 stepper: Stepper<(usize, std::collections::VecDeque<f64>)>,
27 color: Color32,
28 visible: bool,
29}
30
31impl CommoditySelectionIndex {
32 pub fn new(period: usize) -> Self {
34 Self {
35 period: period.max(1),
36 point_value: 1.0,
37 margin: 1.0,
38 commission: 0.0,
39 values: IndicatorValues::new(),
40 adx: ADX::new(period),
41 atr: ATR::new(period),
42 stepper: Stepper::default(),
43 color: palette::BROWN, visible: true,
45 }
46 }
47
48 pub fn with_point_value(mut self, point_value: f64) -> Self {
50 self.point_value = clamp_param(point_value, 0.0, f64::MAX);
51 self
52 }
53
54 pub fn with_margin(mut self, margin: f64) -> Self {
56 self.margin = clamp_param(margin, f64::EPSILON, f64::MAX);
57 self
58 }
59
60 pub fn with_commission(mut self, commission: f64) -> Self {
62 self.commission = clamp_param(commission, 0.0, f64::MAX);
63 self
64 }
65
66 pub fn with_color(mut self, color: Color32) -> Self {
68 self.color = color;
69 self
70 }
71}
72
73impl Default for CommoditySelectionIndex {
74 fn default() -> Self {
75 Self::new(14)
76 }
77}
78
79impl Indicator for CommoditySelectionIndex {
80 crate::studies::inputs::study_inputs!(self;
81 period: length("Period", 1, usize::MAX),
82 point_value: number("Point value", 0.0, f64::MAX),
83 margin: number("Margin", f64::EPSILON, f64::MAX),
84 commission: number("Commission", 0.0, f64::MAX)
85 ; {});
86
87 fn name(&self) -> &str {
88 "CSI"
89 }
90
91 fn desc(&self) -> &str {
92 "Commodity Selection Index - Pos sizing metric"
93 }
94
95 fn calculate(&mut self, data: &[Bar]) {
96 self.update(data, 0);
97 }
98
99 fn update(&mut self, data: &[Bar], from: usize) {
100 if from == 0 {
101 self.adx = ADX::new(self.period);
102 }
103 self.adx.update(data, from);
104 if from == 0 {
105 self.atr = ATR::new(self.period);
106 }
107 self.atr.update(data, from);
108 self.stepper
109 .run(data, from, &mut self.values, |state, bar| {
110 let index = state.0;
111 state.0 += 1;
112 if ![bar.high, bar.low, bar.close].iter().all(|x| x.is_finite()) {
113 return IndicatorValue::None;
114 }
115 let adx = self.adx.values().value(0, index).unwrap_or(f64::NAN);
116 state.1.push_back(adx);
117 let past = if state.1.len() > self.period {
118 state.1.pop_front().unwrap()
119 } else {
120 f64::NAN
121 };
122 let atr = self.atr.values().value(0, index);
123 match atr.filter(|_| adx.is_finite() && past.is_finite()) {
124 Some(atr) => {
125 let adxr = (adx + past) / 2.0;
126 let scale =
127 self.point_value / self.margin.sqrt() / (150.0 + self.commission)
128 * 100.0;
129 IndicatorValue::Single(adxr * atr * scale)
130 }
131 None => IndicatorValue::None,
132 }
133 });
134 }
135
136 fn is_incremental(&self) -> bool {
137 true
138 }
139
140 fn values(&self) -> &IndicatorValues {
141 &self.values
142 }
143
144 fn colors(&self) -> Vec<Color32> {
145 vec![self.color]
146 }
147
148 fn set_colors(&mut self, colors: Vec<Color32>) {
149 if !colors.is_empty() {
150 self.color = colors[0];
151 }
152 }
153
154 fn is_overlay(&self) -> bool {
155 false
156 }
157
158 fn is_visible(&self) -> bool {
159 self.visible
160 }
161
162 fn set_visible(&mut self, visible: bool) {
163 self.visible = visible;
164 }
165
166 fn clone_box(&self) -> Box<dyn Indicator> {
167 Box::new(self.clone())
168 }
169
170 fn line_names(&self) -> Vec<String> {
171 vec![format!("CSI({})", self.period)]
172 }
173}
174
175#[derive(Clone)]
181pub struct TrendDetectionIndex {
182 period: usize,
183 values: IndicatorValues,
184 color: Color32,
185 visible: bool,
186}
187
188impl TrendDetectionIndex {
189 pub fn new(period: usize) -> Self {
191 Self {
192 period: period.max(1),
193 values: IndicatorValues::new(),
194 color: palette::BLUE, visible: true,
196 }
197 }
198
199 pub fn with_color(mut self, color: Color32) -> Self {
201 self.color = color;
202 self
203 }
204}
205
206impl Default for TrendDetectionIndex {
207 fn default() -> Self {
208 Self::new(20)
209 }
210}
211
212impl TrendDetectionIndex {
213 fn compute(&self, data: &[Bar]) -> IndicatorValues {
214 let mut values = IndicatorValues::new();
215
216 let n = self.period;
217 let mom: Vec<f64> = (0..data.len())
219 .map(|i| {
220 if i >= n {
221 data[i].close - data[i - n].close
222 } else {
223 0.0
224 }
225 })
226 .collect();
227 let first = n.saturating_mul(3) - 1;
229
230 for i in 0..data.len() {
231 if i < first {
232 values.push(IndicatorValue::None);
233 continue;
234 }
235 let net: f64 = mom[i + 1 - n..=i].iter().sum();
236 let older_abs: f64 = mom[i + 1 - 2 * n..=i - n].iter().map(|m| m.abs()).sum();
237 values.push(IndicatorValue::Single(net.abs() - older_abs));
238 }
239 values
240 }
241}
242
243impl Indicator for TrendDetectionIndex {
244 crate::studies::inputs::study_inputs!(self;
245 period: length("Period", 1, usize::MAX)
246 ; {});
247
248 fn name(&self) -> &str {
249 "TDI"
250 }
251
252 fn desc(&self) -> &str {
253 "Trend Detection Index - Positive when trending, negative when consolidating"
254 }
255
256 fn calculate(&mut self, data: &[Bar]) {
257 self.update(data, 0);
258 }
259
260 fn update(&mut self, data: &[Bar], from: usize) {
261 let mut values = std::mem::take(&mut self.values);
262 update_window(
263 &mut values,
264 data,
265 from,
266 |_data, from| from.saturating_sub(self.period.saturating_mul(3) - 1),
267 |data| self.compute(data),
268 );
269 self.values = values;
270 }
271
272 fn is_incremental(&self) -> bool {
273 true
274 }
275
276 fn values(&self) -> &IndicatorValues {
277 &self.values
278 }
279
280 fn colors(&self) -> Vec<Color32> {
281 vec![self.color]
282 }
283
284 fn set_colors(&mut self, colors: Vec<Color32>) {
285 if !colors.is_empty() {
286 self.color = colors[0];
287 }
288 }
289
290 fn is_overlay(&self) -> bool {
291 false
292 }
293
294 fn is_visible(&self) -> bool {
295 self.visible
296 }
297
298 fn set_visible(&mut self, visible: bool) {
299 self.visible = visible;
300 }
301
302 fn clone_box(&self) -> Box<dyn Indicator> {
303 Box::new(self.clone())
304 }
305
306 fn line_names(&self) -> Vec<String> {
307 vec![format!("TDI({})", self.period)]
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use chrono::Utc;
315
316 fn make_bar(close: f64) -> Bar {
317 Bar {
318 time: Utc::now(),
319 open: close,
320 high: close + 1.0,
321 low: close - 1.0,
322 close,
323 volume: 1000.0,
324 colors: None,
325 payload: None,
326 }
327 }
328
329 #[test]
332 fn tdi_matches_published_definition() {
333 let flat_then_rising: Vec<f64> = (0..60)
334 .map(|i| 100.0 + (i as f64 - 39.0).max(0.0))
335 .collect();
336 let reversal = [
337 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 14.0, 13.0, 12.0, 11.0, 12.0, 13.0,
338 ];
339 let cases: [(usize, &[f64], &[f64]); 2] = [
340 (20, &flat_then_rising, &[210.0]),
341 (3, &reversal, &[-6.0, 0.0, 2.0, -2.0]),
342 ];
343 for (period, closes, expected) in cases {
344 let data: Vec<Bar> = closes.iter().map(|&c| make_bar(c)).collect();
345 let mut tdi = TrendDetectionIndex::new(period);
346 tdi.calculate(&data);
347 let first = 3 * period - 1;
348 assert_eq!(tdi.values.len(), closes.len());
349 for (k, want) in expected.iter().enumerate() {
350 match tdi.values.to_rows()[first + k] {
351 IndicatorValue::Single(got) => {
352 assert!(
353 (got - want).abs() < 1e-12,
354 "period {period}, bar {}: {got} != {want}",
355 first + k
356 )
357 }
358 _ => panic!("period {period}, bar {}: no value", first + k),
359 }
360 }
361 assert!(
362 tdi.values.to_rows()[..first]
363 .iter()
364 .all(|v| matches!(v, IndicatorValue::None))
365 );
366 }
367 }
368}