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