wickra_core/indicators/
matching_low.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
40pub struct MatchingLow {
41 prev: Option<Candle>,
42 has_emitted: bool,
43}
44
45impl MatchingLow {
46 pub const fn new() -> Self {
48 Self {
49 prev: None,
50 has_emitted: false,
51 }
52 }
53}
54
55impl Indicator for MatchingLow {
56 type Input = Candle;
57 type Output = f64;
58
59 #[inline]
60 fn update(&mut self, candle: Candle) -> Option<f64> {
61 let prev = self.prev;
62 self.prev = Some(candle);
63 let bar1 = prev?;
64 self.has_emitted = true;
65 let mean_range = f64::midpoint(bar1.high - bar1.low, candle.high - candle.low);
66 let tol = 0.05 * mean_range;
67 if bar1.close < bar1.open
68 && candle.close < candle.open
69 && (candle.close - bar1.close).abs() <= tol
70 {
71 return Some(1.0);
72 }
73 Some(0.0)
74 }
75
76 fn reset(&mut self) {
77 self.prev = None;
78 self.has_emitted = false;
79 }
80
81 #[inline]
82 fn warmup_period(&self) -> usize {
83 2
84 }
85
86 #[inline]
87 fn is_ready(&self) -> bool {
88 self.has_emitted
89 }
90
91 #[inline]
92 fn name(&self) -> &'static str {
93 "MatchingLow"
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::traits::BatchExt;
101
102 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
103 Candle::new(open, high, low, close, 1.0, ts).unwrap()
104 }
105
106 #[test]
107 fn accessors_and_metadata() {
108 let t = MatchingLow::new();
109 assert_eq!(t.name(), "MatchingLow");
110 assert_eq!(t.warmup_period(), 2);
111 assert!(!t.is_ready());
112 }
113
114 #[test]
115 fn matching_low_is_plus_one() {
116 let mut t = MatchingLow::new();
117 assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), None);
118 assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 1)), Some(1.0));
119 }
120
121 #[test]
122 fn different_close_yields_zero() {
123 let mut t = MatchingLow::new();
124 t.update(c(15.0, 15.1, 9.9, 10.0, 0));
125 assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 1)), Some(0.0));
127 }
128
129 #[test]
130 fn second_bar_white_yields_zero() {
131 let mut t = MatchingLow::new();
132 t.update(c(15.0, 15.1, 9.9, 10.0, 0));
133 assert_eq!(t.update(c(9.0, 10.1, 8.9, 10.0, 1)), Some(0.0));
134 }
135
136 #[test]
137 fn first_bar_returns_zero() {
138 let mut t = MatchingLow::new();
139 assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), None);
140 }
141
142 #[test]
143 fn batch_equals_streaming() {
144 let candles: Vec<Candle> = (0..40)
145 .map(|i| {
146 let base = 100.0 - i as f64;
147 c(base + 2.0, base + 2.1, base - 0.1, base, i)
148 })
149 .collect();
150 let mut a = MatchingLow::new();
151 let mut b = MatchingLow::new();
152 assert_eq!(
153 a.batch(&candles),
154 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
155 );
156 }
157
158 #[test]
159 fn reset_clears_state() {
160 let mut t = MatchingLow::new();
161 t.update(c(15.0, 15.1, 9.9, 10.0, 0));
162 t.update(c(13.0, 13.1, 9.9, 10.0, 1));
163 assert!(t.is_ready());
164 t.reset();
165 assert!(!t.is_ready());
166 assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), None);
167 }
168}