egui_charts/studies/builtin/
momentum.rs1use crate::model::Bar;
11use crate::studies::palette;
12use crate::studies::stepper::update_window;
13use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
14use egui::Color32;
15
16#[derive(Clone)]
18pub struct Momentum {
19 period: usize,
20 values: IndicatorValues,
21 color: Color32,
22 visible: bool,
23}
24
25impl Momentum {
26 pub fn new(period: usize) -> Self {
31 Self {
32 period: period.max(1),
33 values: IndicatorValues::new(),
34 color: palette::BLUE,
35 visible: true,
36 }
37 }
38}
39
40impl Default for Momentum {
42 fn default() -> Self {
43 Self::new(10)
44 }
45}
46
47impl Momentum {
48 fn compute(&self, data: &[Bar]) -> IndicatorValues {
49 (0..data.len())
50 .map(|i| match i.checked_sub(self.period) {
51 Some(prev) => IndicatorValue::Single(data[i].close - data[prev].close),
52 None => IndicatorValue::None,
53 })
54 .collect()
55 }
56}
57
58impl Indicator for Momentum {
59 fn source(&self) -> Option<crate::studies::StudySource> {
60 Some(crate::studies::StudySource::Price(
61 crate::model::PriceSource::Close,
62 ))
63 }
64
65 crate::studies::inputs::study_inputs!(self;
66 period: length("Period", 1, usize::MAX)
67 ; {});
68
69 fn value_format(&self) -> crate::scales::StudyFormat {
70 crate::scales::StudyFormat::Price
71 }
72
73 fn name(&self) -> &str {
74 "Momentum"
75 }
76
77 fn desc(&self) -> &str {
78 "Momentum - Price change over the period"
79 }
80
81 fn calculate(&mut self, data: &[Bar]) {
82 self.update(data, 0);
83 }
84
85 fn update(&mut self, data: &[Bar], from: usize) {
86 let mut values = std::mem::take(&mut self.values);
87 update_window(
88 &mut values,
89 data,
90 from,
91 |_data, from| from.saturating_sub(self.period),
92 |data| self.compute(data),
93 );
94 self.values = values;
95 }
96
97 fn is_incremental(&self) -> bool {
98 true
99 }
100
101 fn values(&self) -> &IndicatorValues {
102 &self.values
103 }
104
105 fn colors(&self) -> Vec<Color32> {
106 vec![self.color]
107 }
108
109 fn set_colors(&mut self, colors: Vec<Color32>) {
110 if let Some(&color) = colors.first() {
111 self.color = color;
112 }
113 }
114
115 fn is_overlay(&self) -> bool {
116 false
117 }
118
119 fn is_visible(&self) -> bool {
120 self.visible
121 }
122
123 fn set_visible(&mut self, visible: bool) {
124 self.visible = visible;
125 }
126
127 fn clone_box(&self) -> Box<dyn Indicator> {
128 Box::new(self.clone())
129 }
130
131 fn line_names(&self) -> Vec<String> {
132 vec![format!("MOM({})", self.period)]
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use chrono::{Duration, Utc};
140
141 #[test]
142 fn matches_reference_values() {
143 let closes = [10.0, 10.5, 11.25, 10.75, 12.0, 11.5, 13.25, 12.5];
144 let start = Utc::now();
145 let bars: Vec<Bar> = closes
146 .iter()
147 .enumerate()
148 .map(|(i, &c)| Bar {
149 time: start + Duration::minutes(i as i64),
150 open: c,
151 high: c,
152 low: c,
153 close: c,
154 volume: 1.0,
155 colors: None,
156 payload: None,
157 })
158 .collect();
159
160 let mut momentum = Momentum::new(3);
161 momentum.calculate(&bars);
162
163 let expected = [
165 None,
166 None,
167 None,
168 Some(0.75),
169 Some(1.5),
170 Some(0.25),
171 Some(2.5),
172 Some(0.5),
173 ];
174 let actual: Vec<Option<f64>> = momentum
175 .values()
176 .to_rows()
177 .iter()
178 .map(|v| match v {
179 IndicatorValue::Single(x) => Some(*x),
180 _ => None,
181 })
182 .collect();
183 assert_eq!(actual, expected);
184 }
185}