1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
44pub struct Tweezer {
45 tolerance: f64,
46 prev: Option<Candle>,
47 has_emitted: bool,
48}
49
50impl Default for Tweezer {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl Tweezer {
57 pub const fn new() -> Self {
59 Self {
60 tolerance: 0.001,
61 prev: None,
62 has_emitted: false,
63 }
64 }
65
66 pub fn with_tolerance(tolerance: f64) -> Result<Self> {
70 if !(0.0..1.0).contains(&tolerance) {
71 return Err(Error::InvalidPeriod {
72 message: "tweezer tolerance must lie in [0, 1)",
73 });
74 }
75 Ok(Self {
76 tolerance,
77 prev: None,
78 has_emitted: false,
79 })
80 }
81
82 pub fn tolerance(&self) -> f64 {
84 self.tolerance
85 }
86}
87
88impl Indicator for Tweezer {
89 type Input = Candle;
90 type Output = f64;
91
92 #[inline]
93 fn update(&mut self, candle: Candle) -> Option<f64> {
94 let prev = self.prev;
95 self.prev = Some(candle);
96 let p = prev?;
97 self.has_emitted = true;
98 let tol_high = self.tolerance * p.high.abs().max(candle.high.abs());
99 let tol_low = self.tolerance * p.low.abs().max(candle.low.abs());
100 let match_low = (candle.low - p.low).abs() <= tol_low;
101 let match_high = (candle.high - p.high).abs() <= tol_high;
102 if match_low {
103 Some(1.0)
104 } else if match_high {
105 Some(-1.0)
106 } else {
107 Some(0.0)
108 }
109 }
110
111 fn reset(&mut self) {
112 self.prev = None;
113 self.has_emitted = false;
114 }
115
116 #[inline]
117 fn warmup_period(&self) -> usize {
118 2
119 }
120
121 #[inline]
122 fn is_ready(&self) -> bool {
123 self.has_emitted
124 }
125
126 #[inline]
127 fn name(&self) -> &'static str {
128 "Tweezer"
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::traits::BatchExt;
136
137 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
138 Candle::new(open, high, low, close, 1.0, ts).unwrap()
139 }
140
141 #[test]
142 fn rejects_invalid_tolerance() {
143 assert!(Tweezer::with_tolerance(-0.01).is_err());
144 assert!(Tweezer::with_tolerance(1.0).is_err());
145 }
146
147 #[test]
148 fn accepts_valid_tolerance() {
149 let t = Tweezer::with_tolerance(0.0).unwrap();
150 assert!((t.tolerance() - 0.0).abs() < 1e-12);
151 }
152
153 #[test]
154 fn accessors_and_metadata() {
155 let t = Tweezer::default();
156 assert_eq!(t.name(), "Tweezer");
157 assert_eq!(t.warmup_period(), 2);
158 assert!(!t.is_ready());
159 assert!((t.tolerance() - 0.001).abs() < 1e-12);
160 }
161
162 #[test]
163 fn tweezer_bottom_is_plus_one() {
164 let mut t = Tweezer::new();
165 assert_eq!(t.update(c(11.0, 12.0, 9.5, 9.6, 0)), None);
166 assert_eq!(t.update(c(9.7, 10.5, 9.5, 10.2, 1)), Some(1.0));
168 }
169
170 #[test]
171 fn tweezer_top_is_minus_one() {
172 let mut t = Tweezer::new();
173 assert_eq!(t.update(c(9.0, 12.0, 8.5, 11.0, 0)), None);
174 assert_eq!(t.update(c(11.5, 12.0, 11.0, 11.4, 1)), Some(-1.0));
176 }
177
178 #[test]
179 fn distinct_extremes_yield_zero() {
180 let mut t = Tweezer::new();
181 t.update(c(10.0, 11.0, 9.0, 10.5, 0));
182 assert_eq!(t.update(c(10.6, 11.5, 9.6, 11.2, 1)), Some(0.0));
183 }
184
185 #[test]
186 fn first_bar_returns_zero() {
187 let mut t = Tweezer::new();
188 assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.5, 0)), None);
189 }
190
191 #[test]
192 fn matched_both_extremes_prefers_bottom() {
193 let mut t = Tweezer::new();
195 t.update(c(10.0, 11.0, 9.0, 10.5, 0));
196 assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.5, 1)), Some(1.0));
197 }
198
199 #[test]
200 fn batch_equals_streaming() {
201 let candles: Vec<Candle> = (0..40)
202 .map(|i| {
203 let base = 100.0 + (i as f64 * 0.1).sin();
204 c(base, base + 2.0, base - 2.0, base + 0.5, i)
205 })
206 .collect();
207 let mut a = Tweezer::new();
208 let mut b = Tweezer::new();
209 assert_eq!(
210 a.batch(&candles),
211 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
212 );
213 }
214
215 #[test]
216 fn reset_clears_state() {
217 let mut t = Tweezer::new();
218 t.update(c(10.0, 11.0, 9.0, 10.5, 0));
219 t.update(c(10.0, 11.0, 9.0, 10.5, 1));
220 assert!(t.is_ready());
221 t.reset();
222 assert!(!t.is_ready());
223 assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.5, 0)), None);
224 }
225}