egui_charts/studies/builtin/
ultimate_momentum.rs1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::update_window;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7#[derive(Clone)]
11pub struct UltimateMomentum {
12 short_period: usize,
14 medium_period: usize,
16 long_period: usize,
18 values: IndicatorValues,
19 color: Color32,
20 visible: bool,
21}
22
23impl UltimateMomentum {
24 pub fn new(short: usize, medium: usize, long: usize) -> Self {
26 Self {
27 short_period: short.max(1),
28 medium_period: medium.max(1),
29 long_period: long.max(1),
30 values: IndicatorValues::new(),
31 color: palette::PURPLE,
32 visible: true,
33 }
34 }
35
36 pub fn with_color(mut self, color: Color32) -> Self {
38 self.color = color;
39 self
40 }
41
42 fn calculate_roc(closes: &[f64], period: usize, index: usize) -> Option<f64> {
43 if index < period || closes[index - period] == 0.0 {
44 None
45 } else {
46 Some((closes[index] - closes[index - period]) / closes[index - period] * 100.0)
47 }
48 }
49}
50
51impl Default for UltimateMomentum {
52 fn default() -> Self {
53 Self::new(5, 10, 20)
54 }
55}
56
57impl UltimateMomentum {
58 fn compute(&self, data: &[Bar]) -> IndicatorValues {
59 let mut values = IndicatorValues::new();
60
61 if data.len() < self.long_period {
62 for _ in 0..data.len() {
63 values.push(IndicatorValue::None);
64 }
65 return values;
66 }
67
68 let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
70
71 for i in 0..data.len() {
72 let roc_short = Self::calculate_roc(&closes, self.short_period, i);
73 let roc_medium = Self::calculate_roc(&closes, self.medium_period, i);
74 let roc_long = Self::calculate_roc(&closes, self.long_period, i);
75
76 match (roc_short, roc_medium, roc_long) {
77 (Some(s), Some(m), Some(l)) => {
78 let momentum = (s * 1.0 + m * 2.0 + l * 3.0) / 6.0;
80 values.push(IndicatorValue::Single(momentum));
81 }
82 _ => {
83 values.push(IndicatorValue::None);
84 }
85 }
86 }
87 values
88 }
89}
90
91impl Indicator for UltimateMomentum {
92 fn source(&self) -> Option<crate::studies::StudySource> {
93 Some(crate::studies::StudySource::Price(
94 crate::model::PriceSource::Close,
95 ))
96 }
97
98 crate::studies::inputs::study_inputs!(self;
99 short_period: length("Short period", 1, usize::MAX),
100 medium_period: length("Medium period", 1, usize::MAX),
101 long_period: length("Long period", 1, usize::MAX)
102 ; {});
103
104 fn name(&self) -> &str {
105 "UltMom"
106 }
107
108 fn desc(&self) -> &str {
109 "Ultimate Momentum - Multi-timeframe momentum"
110 }
111
112 fn calculate(&mut self, data: &[Bar]) {
113 self.update(data, 0);
114 }
115
116 fn update(&mut self, data: &[Bar], from: usize) {
117 let mut values = std::mem::take(&mut self.values);
118 update_window(
119 &mut values,
120 data,
121 from,
122 |_data, from| {
123 from.saturating_sub(
124 self.short_period
125 .max(self.medium_period)
126 .max(self.long_period),
127 )
128 },
129 |data| self.compute(data),
130 );
131 self.values = values;
132 }
133
134 fn is_incremental(&self) -> bool {
135 true
136 }
137
138 fn values(&self) -> &IndicatorValues {
139 &self.values
140 }
141
142 fn colors(&self) -> Vec<Color32> {
143 vec![self.color]
144 }
145
146 fn set_colors(&mut self, colors: Vec<Color32>) {
147 if !colors.is_empty() {
148 self.color = colors[0];
149 }
150 }
151
152 fn is_overlay(&self) -> bool {
153 false
154 }
155
156 fn is_visible(&self) -> bool {
157 self.visible
158 }
159
160 fn set_visible(&mut self, visible: bool) {
161 self.visible = visible;
162 }
163
164 fn clone_box(&self) -> Box<dyn Indicator> {
165 Box::new(self.clone())
166 }
167
168 fn line_names(&self) -> Vec<String> {
169 vec![format!(
170 "UltMom({},{},{})",
171 self.short_period, self.medium_period, self.long_period
172 )]
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::studies::test_support::make_bar;
180
181 #[test]
182 fn test_ultimate_momentum() {
183 let mut um = UltimateMomentum::new(3, 5, 10);
184
185 let data: Vec<Bar> = (0..30).map(|i| make_bar(100.0 + i as f64)).collect();
186
187 um.calculate(&data);
188
189 assert_eq!(um.values.len(), 30);
190
191 let rows = um.values.to_rows();
193 let IndicatorValue::Single(v) = rows.last().unwrap() else {
194 panic!("no value: {:?}", um.values.to_rows().last().unwrap());
195 };
196 assert!(*v > 0.0, "Momentum should be positive in uptrend");
197 }
198}