1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
51pub struct MatHold {
52 penetration: f64,
53 c1: Option<Candle>,
54 c2: Option<Candle>,
55 c3: Option<Candle>,
56 c4: Option<Candle>,
57 has_emitted: bool,
58}
59
60impl Default for MatHold {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl MatHold {
67 pub const fn new() -> Self {
69 Self {
70 penetration: 0.5,
71 c1: None,
72 c2: None,
73 c3: None,
74 c4: None,
75 has_emitted: false,
76 }
77 }
78
79 pub fn with_penetration(penetration: f64) -> Result<Self> {
83 if !(0.0..1.0).contains(&penetration) {
84 return Err(Error::InvalidPeriod {
85 message: "mat hold penetration must lie in [0, 1)",
86 });
87 }
88 Ok(Self {
89 penetration,
90 c1: None,
91 c2: None,
92 c3: None,
93 c4: None,
94 has_emitted: false,
95 })
96 }
97
98 pub fn penetration(&self) -> f64 {
100 self.penetration
101 }
102}
103
104impl Indicator for MatHold {
105 type Input = Candle;
106 type Output = f64;
107
108 fn update(&mut self, candle: Candle) -> Option<f64> {
109 let bar1 = self.c1;
110 let bar2 = self.c2;
111 let bar3 = self.c3;
112 let bar4 = self.c4;
113 self.c1 = self.c2;
114 self.c2 = self.c3;
115 self.c3 = self.c4;
116 self.c4 = Some(candle);
117 let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
118 return None;
119 };
120 self.has_emitted = true;
121 let range1 = bar1.high - bar1.low;
122 if range1 <= 0.0 {
123 return Some(0.0);
124 }
125 let body1 = bar1.close - bar1.open;
126 if body1 < 0.5 * range1 {
127 return Some(0.0); }
129 let small = 0.5 * body1;
130 if (bar2.close - bar2.open).abs() > small
131 || (bar3.close - bar3.open).abs() > small
132 || (bar4.close - bar4.open).abs() > small
133 {
134 return Some(0.0); }
136 if bar2.open.min(bar2.close) <= bar1.close {
138 return Some(0.0);
139 }
140 let hold_line = bar1.close - self.penetration * body1;
142 if bar2.low.min(bar3.low).min(bar4.low) <= hold_line {
143 return Some(0.0);
144 }
145 let max_high = bar1.high.max(bar2.high).max(bar3.high).max(bar4.high);
147 if candle.close > candle.open && candle.close > max_high {
148 return Some(1.0);
149 }
150 Some(0.0)
151 }
152
153 fn reset(&mut self) {
154 self.c1 = None;
155 self.c2 = None;
156 self.c3 = None;
157 self.c4 = None;
158 self.has_emitted = false;
159 }
160
161 #[inline]
162 fn warmup_period(&self) -> usize {
163 5
164 }
165
166 #[inline]
167 fn is_ready(&self) -> bool {
168 self.has_emitted
169 }
170
171 #[inline]
172 fn name(&self) -> &'static str {
173 "MatHold"
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::traits::BatchExt;
181
182 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
183 Candle::new(open, high, low, close, 1.0, ts).unwrap()
184 }
185
186 #[test]
187 fn rejects_invalid_penetration() {
188 assert!(MatHold::with_penetration(-0.01).is_err());
189 assert!(MatHold::with_penetration(1.0).is_err());
190 }
191
192 #[test]
193 fn accepts_valid_penetration() {
194 let t = MatHold::with_penetration(0.3).unwrap();
195 assert!((t.penetration() - 0.3).abs() < 1e-12);
196 }
197
198 #[test]
199 fn accessors_and_metadata() {
200 let t = MatHold::default();
201 assert_eq!(t.name(), "MatHold");
202 assert_eq!(t.warmup_period(), 5);
203 assert!(!t.is_ready());
204 assert!((t.penetration() - 0.5).abs() < 1e-12);
205 }
206
207 #[test]
208 fn mat_hold_is_plus_one() {
209 let mut t = MatHold::new();
210 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
211 assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), None);
212 assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), None);
213 assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), None);
214 assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(1.0));
215 }
216
217 #[test]
218 fn pullback_breaks_hold_yields_zero() {
219 let mut t = MatHold::new();
220 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
221 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
222 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
223 t.update(c(13.0, 13.1, 12.0, 12.4, 3));
225 assert_eq!(t.update(c(14.5, 17.1, 12.0, 17.0, 4)), Some(0.0));
226 }
227
228 #[test]
229 fn no_new_high_yields_zero() {
230 let mut t = MatHold::new();
231 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
232 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
233 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
234 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
235 assert_eq!(t.update(c(14.5, 16.0, 14.4, 15.9, 4)), Some(0.0));
237 }
238
239 #[test]
240 fn first_four_bars_return_zero() {
241 let mut t = MatHold::new();
242 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
243 assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), None);
244 assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), None);
245 assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), None);
246 }
247
248 #[test]
249 fn batch_equals_streaming() {
250 let candles: Vec<Candle> = (0..40)
251 .map(|i| {
252 let base = 100.0 + i as f64;
253 c(base, base + 5.2, base - 0.1, base + 5.0, i)
254 })
255 .collect();
256 let mut a = MatHold::new();
257 let mut b = MatHold::new();
258 assert_eq!(
259 a.batch(&candles),
260 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
261 );
262 }
263
264 #[test]
265 fn reset_clears_state() {
266 let mut t = MatHold::new();
267 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
268 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
269 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
270 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
271 t.update(c(14.5, 17.1, 14.4, 17.0, 4));
272 assert!(t.is_ready());
273 t.reset();
274 assert!(!t.is_ready());
275 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
276 }
277
278 #[test]
279 fn zero_range_first_bar_yields_zero() {
280 let mut t = MatHold::new();
281 t.update(c(10.0, 10.0, 10.0, 10.0, 0));
283 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
284 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
285 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
286 assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0));
287 }
288
289 #[test]
290 fn short_first_body_yields_zero() {
291 let mut t = MatHold::new();
292 t.update(c(10.0, 16.0, 9.0, 10.5, 0));
294 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
295 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
296 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
297 assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0));
298 }
299
300 #[test]
301 fn no_gap_up_yields_zero() {
302 let mut t = MatHold::new();
303 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
306 t.update(c(14.5, 14.7, 14.3, 14.5, 1));
307 t.update(c(14.5, 14.7, 14.3, 14.6, 2));
308 t.update(c(14.6, 14.8, 14.4, 14.7, 3));
309 assert_eq!(t.update(c(14.7, 17.1, 14.6, 17.0, 4)), Some(0.0));
310 }
311}