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