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 fn update(&mut self, candle: Candle) -> Option<f64> {
93 self.has_emitted = true;
94 let prev = self.prev;
95 self.prev = Some(candle);
96 let Some(p) = prev else {
97 return Some(0.0);
98 };
99 let tol_high = self.tolerance * p.high.abs().max(candle.high.abs());
100 let tol_low = self.tolerance * p.low.abs().max(candle.low.abs());
101 let match_low = (candle.low - p.low).abs() <= tol_low;
102 let match_high = (candle.high - p.high).abs() <= tol_high;
103 if match_low {
104 Some(1.0)
105 } else if match_high {
106 Some(-1.0)
107 } else {
108 Some(0.0)
109 }
110 }
111
112 fn reset(&mut self) {
113 self.prev = None;
114 self.has_emitted = false;
115 }
116
117 fn warmup_period(&self) -> usize {
118 2
119 }
120
121 fn is_ready(&self) -> bool {
122 self.has_emitted
123 }
124
125 fn name(&self) -> &'static str {
126 "Tweezer"
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::traits::BatchExt;
134
135 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
136 Candle::new(open, high, low, close, 1.0, ts).unwrap()
137 }
138
139 #[test]
140 fn rejects_invalid_tolerance() {
141 assert!(Tweezer::with_tolerance(-0.01).is_err());
142 assert!(Tweezer::with_tolerance(1.0).is_err());
143 }
144
145 #[test]
146 fn accepts_valid_tolerance() {
147 let t = Tweezer::with_tolerance(0.0).unwrap();
148 assert!((t.tolerance() - 0.0).abs() < 1e-12);
149 }
150
151 #[test]
152 fn accessors_and_metadata() {
153 let t = Tweezer::default();
154 assert_eq!(t.name(), "Tweezer");
155 assert_eq!(t.warmup_period(), 2);
156 assert!(!t.is_ready());
157 assert!((t.tolerance() - 0.001).abs() < 1e-12);
158 }
159
160 #[test]
161 fn tweezer_bottom_is_plus_one() {
162 let mut t = Tweezer::new();
163 assert_eq!(t.update(c(11.0, 12.0, 9.5, 9.6, 0)), Some(0.0));
164 assert_eq!(t.update(c(9.7, 10.5, 9.5, 10.2, 1)), Some(1.0));
166 }
167
168 #[test]
169 fn tweezer_top_is_minus_one() {
170 let mut t = Tweezer::new();
171 assert_eq!(t.update(c(9.0, 12.0, 8.5, 11.0, 0)), Some(0.0));
172 assert_eq!(t.update(c(11.5, 12.0, 11.0, 11.4, 1)), Some(-1.0));
174 }
175
176 #[test]
177 fn distinct_extremes_yield_zero() {
178 let mut t = Tweezer::new();
179 t.update(c(10.0, 11.0, 9.0, 10.5, 0));
180 assert_eq!(t.update(c(10.6, 11.5, 9.6, 11.2, 1)), Some(0.0));
181 }
182
183 #[test]
184 fn first_bar_returns_zero() {
185 let mut t = Tweezer::new();
186 assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.5, 0)), Some(0.0));
187 }
188
189 #[test]
190 fn matched_both_extremes_prefers_bottom() {
191 let mut t = Tweezer::new();
193 t.update(c(10.0, 11.0, 9.0, 10.5, 0));
194 assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.5, 1)), Some(1.0));
195 }
196
197 #[test]
198 fn batch_equals_streaming() {
199 let candles: Vec<Candle> = (0..40)
200 .map(|i| {
201 let base = 100.0 + (i as f64 * 0.1).sin();
202 c(base, base + 2.0, base - 2.0, base + 0.5, i)
203 })
204 .collect();
205 let mut a = Tweezer::new();
206 let mut b = Tweezer::new();
207 assert_eq!(
208 a.batch(&candles),
209 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
210 );
211 }
212
213 #[test]
214 fn reset_clears_state() {
215 let mut t = Tweezer::new();
216 t.update(c(10.0, 11.0, 9.0, 10.5, 0));
217 t.update(c(10.0, 11.0, 9.0, 10.5, 1));
218 assert!(t.is_ready());
219 t.reset();
220 assert!(!t.is_ready());
221 assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.5, 0)), Some(0.0));
222 }
223}