wickra_core/indicators/
murrey_math_lines.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct MurreyMathLinesOutput {
13 pub mm8_8: f64,
15 pub mm7_8: f64,
17 pub mm6_8: f64,
19 pub mm5_8: f64,
21 pub mm4_8: f64,
23 pub mm3_8: f64,
25 pub mm2_8: f64,
27 pub mm1_8: f64,
29 pub mm0_8: f64,
31}
32
33#[derive(Debug, Clone)]
70pub struct MurreyMathLines {
71 period: usize,
72 highs: VecDeque<f64>,
73 lows: VecDeque<f64>,
74 last: Option<MurreyMathLinesOutput>,
75}
76
77impl MurreyMathLines {
78 pub fn new(period: usize) -> Result<Self> {
84 if period == 0 {
85 return Err(Error::PeriodZero);
86 }
87 if period > crate::error::MAX_PERIOD {
88 return Err(Error::InvalidPeriod {
89 message: crate::error::PERIOD_ABOVE_MAX,
90 });
91 }
92 Ok(Self {
93 period,
94 highs: VecDeque::with_capacity(period),
95 lows: VecDeque::with_capacity(period),
96 last: None,
97 })
98 }
99
100 pub const fn period(&self) -> usize {
102 self.period
103 }
104
105 pub const fn value(&self) -> Option<MurreyMathLinesOutput> {
107 self.last
108 }
109}
110
111impl Indicator for MurreyMathLines {
112 type Input = Candle;
113 type Output = MurreyMathLinesOutput;
114
115 #[inline]
116 fn update(&mut self, candle: Candle) -> Option<MurreyMathLinesOutput> {
117 if self.highs.len() == self.period {
118 self.highs.pop_front();
119 self.lows.pop_front();
120 }
121 self.highs.push_back(candle.high);
122 self.lows.push_back(candle.low);
123 if self.highs.len() < self.period {
124 return None;
125 }
126 let hh = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
127 let ll = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
128 let step = (hh - ll) / 8.0;
129 let level = |i: f64| ll + i * step;
130 let out = MurreyMathLinesOutput {
131 mm0_8: level(0.0),
132 mm1_8: level(1.0),
133 mm2_8: level(2.0),
134 mm3_8: level(3.0),
135 mm4_8: level(4.0),
136 mm5_8: level(5.0),
137 mm6_8: level(6.0),
138 mm7_8: level(7.0),
139 mm8_8: level(8.0),
140 };
141 self.last = Some(out);
142 Some(out)
143 }
144
145 fn reset(&mut self) {
146 self.highs.clear();
147 self.lows.clear();
148 self.last = None;
149 }
150
151 #[inline]
152 fn warmup_period(&self) -> usize {
153 self.period
154 }
155
156 #[inline]
157 fn is_ready(&self) -> bool {
158 self.last.is_some()
159 }
160
161 #[inline]
162 fn name(&self) -> &'static str {
163 "MurreyMathLines"
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::traits::BatchExt;
171 use approx::assert_relative_eq;
172
173 fn c(high: f64, low: f64) -> Candle {
174 Candle::new_unchecked(low, high, low, f64::midpoint(high, low), 1_000.0, 0)
175 }
176
177 #[test]
178 fn rejects_zero_period() {
179 assert!(matches!(MurreyMathLines::new(0), Err(Error::PeriodZero)));
180 }
181
182 #[test]
183 fn accessors_and_metadata() {
184 let m = MurreyMathLines::new(64).unwrap();
185 assert_eq!(m.period(), 64);
186 assert_eq!(m.warmup_period(), 64);
187 assert_eq!(m.name(), "MurreyMathLines");
188 assert!(!m.is_ready());
189 assert_eq!(m.value(), None);
190 }
191
192 #[test]
193 fn first_emission_at_warmup_period() {
194 let mut m = MurreyMathLines::new(4).unwrap();
195 let candles: Vec<Candle> = (0..6)
196 .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
197 .collect();
198 let out = m.batch(&candles);
199 for v in out.iter().take(3) {
200 assert!(v.is_none());
201 }
202 assert!(out[3].is_some());
203 }
204
205 #[test]
206 fn eighths_are_evenly_spaced() {
207 let mut m = MurreyMathLines::new(2).unwrap();
209 let out = m
210 .batch(&[c(180.0, 100.0), c(180.0, 100.0)])
211 .into_iter()
212 .flatten()
213 .last()
214 .unwrap();
215 assert_relative_eq!(out.mm0_8, 100.0, epsilon = 1e-9);
216 assert_relative_eq!(out.mm4_8, 140.0, epsilon = 1e-9);
217 assert_relative_eq!(out.mm8_8, 180.0, epsilon = 1e-9);
218 assert_relative_eq!(out.mm1_8 - out.mm0_8, 10.0, epsilon = 1e-9);
219 }
220
221 #[test]
222 fn levels_are_ordered() {
223 let mut m = MurreyMathLines::new(10).unwrap();
224 let candles: Vec<Candle> = (0..30)
225 .map(|i| {
226 c(
227 110.0 + (f64::from(i) * 0.3).sin() * 8.0,
228 90.0 + (f64::from(i) * 0.3).cos() * 8.0,
229 )
230 })
231 .collect();
232 for o in m.batch(&candles).into_iter().flatten() {
233 assert!(o.mm0_8 <= o.mm4_8 && o.mm4_8 <= o.mm8_8);
234 assert!(o.mm3_8 <= o.mm5_8);
235 }
236 }
237
238 #[test]
239 fn flat_frame_collapses() {
240 let mut m = MurreyMathLines::new(3).unwrap();
241 let out = m
242 .batch(&[c(50.0, 50.0), c(50.0, 50.0), c(50.0, 50.0)])
243 .into_iter()
244 .flatten()
245 .last()
246 .unwrap();
247 assert_relative_eq!(out.mm0_8, 50.0, epsilon = 1e-12);
248 assert_relative_eq!(out.mm8_8, 50.0, epsilon = 1e-12);
249 }
250
251 #[test]
252 fn reset_clears_state() {
253 let mut m = MurreyMathLines::new(4).unwrap();
254 m.batch(
255 &(0..6)
256 .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
257 .collect::<Vec<_>>(),
258 );
259 assert!(m.is_ready());
260 m.reset();
261 assert!(!m.is_ready());
262 assert_eq!(m.value(), None);
263 assert_eq!(m.update(c(101.0, 99.0)), None);
264 }
265
266 #[test]
267 fn batch_equals_streaming() {
268 let candles: Vec<Candle> = (0..120)
269 .map(|i| {
270 c(
271 110.0 + (f64::from(i) * 0.25).sin() * 9.0,
272 90.0 + (f64::from(i) * 0.25).cos() * 9.0,
273 )
274 })
275 .collect();
276 let batch = MurreyMathLines::new(64).unwrap().batch(&candles);
277 let mut b = MurreyMathLines::new(64).unwrap();
278 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
279 assert_eq!(batch, streamed);
280 }
281}