1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
45pub struct TasukiGap {
46 c1: Option<Candle>,
47 c2: Option<Candle>,
48 has_emitted: bool,
49}
50
51impl TasukiGap {
52 pub const fn new() -> Self {
54 Self {
55 c1: None,
56 c2: None,
57 has_emitted: false,
58 }
59 }
60}
61
62impl Indicator for TasukiGap {
63 type Input = Candle;
64 type Output = f64;
65
66 fn update(&mut self, candle: Candle) -> Option<f64> {
67 let bar1 = self.c1;
68 let bar2 = self.c2;
69 self.c1 = self.c2;
70 self.c2 = Some(candle);
71 let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
72 return None;
73 };
74 self.has_emitted = true;
75
76 let up = bar1.close > bar1.open && bar2.close > bar2.open;
77 let down = bar1.close < bar1.open && bar2.close < bar2.open;
78 if up {
79 if bar2.open <= bar1.close {
80 return Some(0.0); }
82 if candle.close >= candle.open {
83 return Some(0.0); }
85 if candle.open <= bar2.open || candle.open >= bar2.close {
86 return Some(0.0); }
88 if candle.close < bar2.open && candle.close > bar1.close {
89 return Some(1.0); }
91 return Some(0.0);
92 }
93 if down {
94 if bar2.open >= bar1.close {
95 return Some(0.0); }
97 if candle.close <= candle.open {
98 return Some(0.0); }
100 if candle.open >= bar2.open || candle.open <= bar2.close {
101 return Some(0.0); }
103 if candle.close > bar2.open && candle.close < bar1.close {
104 return Some(-1.0); }
106 return Some(0.0);
107 }
108 Some(0.0)
109 }
110
111 fn reset(&mut self) {
112 self.c1 = None;
113 self.c2 = None;
114 self.has_emitted = false;
115 }
116
117 #[inline]
118 fn warmup_period(&self) -> usize {
119 3
120 }
121
122 #[inline]
123 fn is_ready(&self) -> bool {
124 self.has_emitted
125 }
126
127 #[inline]
128 fn name(&self) -> &'static str {
129 "TasukiGap"
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::traits::BatchExt;
137
138 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
139 Candle::new(open, high, low, close, 1.0, ts).unwrap()
140 }
141
142 #[test]
143 fn accessors_and_metadata() {
144 let t = TasukiGap::new();
145 assert_eq!(t.name(), "TasukiGap");
146 assert_eq!(t.warmup_period(), 3);
147 assert!(!t.is_ready());
148 }
149
150 #[test]
151 fn upside_tasuki_gap_is_plus_one() {
152 let mut t = TasukiGap::new();
153 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), None);
154 assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), None);
155 assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 2)), Some(1.0));
156 }
157
158 #[test]
159 fn downside_tasuki_gap_is_minus_one() {
160 let mut t = TasukiGap::new();
161 assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), None);
162 assert_eq!(t.update(c(11.0, 11.1, 9.5, 10.0, 1)), None);
163 assert_eq!(t.update(c(10.5, 11.6, 10.4, 11.5, 2)), Some(-1.0));
164 }
165
166 #[test]
167 fn first_two_bars_return_zero() {
168 let mut t = TasukiGap::new();
169 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), None);
170 assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), None);
171 }
172
173 #[test]
174 fn up_no_gap_yields_zero() {
175 let mut t = TasukiGap::new();
176 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
177 t.update(c(10.5, 13.1, 10.4, 13.0, 1));
179 assert_eq!(t.update(c(12.5, 12.6, 10.9, 11.0, 2)), Some(0.0));
180 }
181
182 #[test]
183 fn up_third_not_black_yields_zero() {
184 let mut t = TasukiGap::new();
185 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
186 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
187 assert_eq!(t.update(c(12.5, 13.1, 12.4, 13.0, 2)), Some(0.0));
189 }
190
191 #[test]
192 fn up_third_open_outside_body_yields_zero() {
193 let mut t = TasukiGap::new();
194 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
195 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
196 assert_eq!(t.update(c(14.0, 14.1, 11.4, 11.5, 2)), Some(0.0));
198 }
199
200 #[test]
201 fn up_third_close_not_in_gap_yields_zero() {
202 let mut t = TasukiGap::new();
203 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
204 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
205 assert_eq!(t.update(c(13.0, 13.1, 10.4, 10.5, 2)), Some(0.0));
207 }
208
209 #[test]
210 fn down_no_gap_yields_zero() {
211 let mut t = TasukiGap::new();
212 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
213 t.update(c(12.5, 12.6, 10.4, 10.5, 1));
215 assert_eq!(t.update(c(11.0, 12.6, 10.9, 12.0, 2)), Some(0.0));
216 }
217
218 #[test]
219 fn down_third_not_white_yields_zero() {
220 let mut t = TasukiGap::new();
221 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
222 t.update(c(11.0, 11.1, 9.5, 10.0, 1));
223 assert_eq!(t.update(c(11.5, 11.6, 10.4, 10.5, 2)), Some(0.0));
225 }
226
227 #[test]
228 fn down_third_open_outside_body_yields_zero() {
229 let mut t = TasukiGap::new();
230 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
231 t.update(c(11.0, 11.1, 9.5, 10.0, 1));
232 assert_eq!(t.update(c(9.5, 11.6, 9.4, 11.5, 2)), Some(0.0));
234 }
235
236 #[test]
237 fn down_third_close_not_in_gap_yields_zero() {
238 let mut t = TasukiGap::new();
239 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
240 t.update(c(11.0, 11.1, 9.5, 10.0, 1));
241 assert_eq!(t.update(c(10.5, 13.0, 10.4, 12.5, 2)), Some(0.0));
243 }
244
245 #[test]
246 fn mixed_colours_yield_zero() {
247 let mut t = TasukiGap::new();
248 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
250 t.update(c(13.0, 13.2, 11.0, 11.5, 1));
251 assert_eq!(t.update(c(12.0, 12.6, 10.9, 11.0, 2)), Some(0.0));
252 }
253
254 #[test]
255 fn batch_equals_streaming() {
256 let candles: Vec<Candle> = (0..40)
257 .map(|i| {
258 let base = 100.0 + i as f64;
259 c(base, base + 5.2, base - 0.1, base + 5.0, i)
260 })
261 .collect();
262 let mut a = TasukiGap::new();
263 let mut b = TasukiGap::new();
264 assert_eq!(
265 a.batch(&candles),
266 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
267 );
268 }
269
270 #[test]
271 fn reset_clears_state() {
272 let mut t = TasukiGap::new();
273 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
274 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
275 t.update(c(13.0, 13.1, 11.4, 11.5, 2));
276 assert!(t.is_ready());
277 t.reset();
278 assert!(!t.is_ready());
279 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), None);
280 }
281}