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 self.has_emitted = true;
110 let bar1 = self.c1;
111 let bar2 = self.c2;
112 let bar3 = self.c3;
113 let bar4 = self.c4;
114 self.c1 = self.c2;
115 self.c2 = self.c3;
116 self.c3 = self.c4;
117 self.c4 = Some(candle);
118 let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
119 return Some(0.0);
120 };
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 fn warmup_period(&self) -> usize {
162 5
163 }
164
165 fn is_ready(&self) -> bool {
166 self.has_emitted
167 }
168
169 fn name(&self) -> &'static str {
170 "MatHold"
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::traits::BatchExt;
178
179 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
180 Candle::new(open, high, low, close, 1.0, ts).unwrap()
181 }
182
183 #[test]
184 fn rejects_invalid_penetration() {
185 assert!(MatHold::with_penetration(-0.01).is_err());
186 assert!(MatHold::with_penetration(1.0).is_err());
187 }
188
189 #[test]
190 fn accepts_valid_penetration() {
191 let t = MatHold::with_penetration(0.3).unwrap();
192 assert!((t.penetration() - 0.3).abs() < 1e-12);
193 }
194
195 #[test]
196 fn accessors_and_metadata() {
197 let t = MatHold::default();
198 assert_eq!(t.name(), "MatHold");
199 assert_eq!(t.warmup_period(), 5);
200 assert!(!t.is_ready());
201 assert!((t.penetration() - 0.5).abs() < 1e-12);
202 }
203
204 #[test]
205 fn mat_hold_is_plus_one() {
206 let mut t = MatHold::new();
207 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
208 assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
209 assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
210 assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
211 assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(1.0));
212 }
213
214 #[test]
215 fn pullback_breaks_hold_yields_zero() {
216 let mut t = MatHold::new();
217 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
218 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
219 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
220 t.update(c(13.0, 13.1, 12.0, 12.4, 3));
222 assert_eq!(t.update(c(14.5, 17.1, 12.0, 17.0, 4)), Some(0.0));
223 }
224
225 #[test]
226 fn no_new_high_yields_zero() {
227 let mut t = MatHold::new();
228 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
229 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
230 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
231 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
232 assert_eq!(t.update(c(14.5, 16.0, 14.4, 15.9, 4)), Some(0.0));
234 }
235
236 #[test]
237 fn first_four_bars_return_zero() {
238 let mut t = MatHold::new();
239 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
240 assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
241 assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
242 assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
243 }
244
245 #[test]
246 fn batch_equals_streaming() {
247 let candles: Vec<Candle> = (0..40)
248 .map(|i| {
249 let base = 100.0 + i as f64;
250 c(base, base + 5.2, base - 0.1, base + 5.0, i)
251 })
252 .collect();
253 let mut a = MatHold::new();
254 let mut b = MatHold::new();
255 assert_eq!(
256 a.batch(&candles),
257 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
258 );
259 }
260
261 #[test]
262 fn reset_clears_state() {
263 let mut t = MatHold::new();
264 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
265 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
266 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
267 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
268 t.update(c(14.5, 17.1, 14.4, 17.0, 4));
269 assert!(t.is_ready());
270 t.reset();
271 assert!(!t.is_ready());
272 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
273 }
274
275 #[test]
276 fn zero_range_first_bar_yields_zero() {
277 let mut t = MatHold::new();
278 t.update(c(10.0, 10.0, 10.0, 10.0, 0));
280 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
281 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
282 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
283 assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0));
284 }
285
286 #[test]
287 fn short_first_body_yields_zero() {
288 let mut t = MatHold::new();
289 t.update(c(10.0, 16.0, 9.0, 10.5, 0));
291 t.update(c(16.0, 16.1, 15.4, 15.5, 1));
292 t.update(c(15.5, 15.6, 14.9, 15.0, 2));
293 t.update(c(15.0, 15.1, 14.4, 14.5, 3));
294 assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0));
295 }
296
297 #[test]
298 fn no_gap_up_yields_zero() {
299 let mut t = MatHold::new();
300 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
303 t.update(c(14.5, 14.7, 14.3, 14.5, 1));
304 t.update(c(14.5, 14.7, 14.3, 14.6, 2));
305 t.update(c(14.6, 14.8, 14.4, 14.7, 3));
306 assert_eq!(t.update(c(14.7, 17.1, 14.6, 17.0, 4)), Some(0.0));
307 }
308}