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 self.has_emitted = true;
68 let bar1 = self.c1;
69 let bar2 = self.c2;
70 self.c1 = self.c2;
71 self.c2 = Some(candle);
72 let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
73 return Some(0.0);
74 };
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 fn warmup_period(&self) -> usize {
118 3
119 }
120
121 fn is_ready(&self) -> bool {
122 self.has_emitted
123 }
124
125 fn name(&self) -> &'static str {
126 "TasukiGap"
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 accessors_and_metadata() {
141 let t = TasukiGap::new();
142 assert_eq!(t.name(), "TasukiGap");
143 assert_eq!(t.warmup_period(), 3);
144 assert!(!t.is_ready());
145 }
146
147 #[test]
148 fn upside_tasuki_gap_is_plus_one() {
149 let mut t = TasukiGap::new();
150 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
151 assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), Some(0.0));
152 assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 2)), Some(1.0));
153 }
154
155 #[test]
156 fn downside_tasuki_gap_is_minus_one() {
157 let mut t = TasukiGap::new();
158 assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
159 assert_eq!(t.update(c(11.0, 11.1, 9.5, 10.0, 1)), Some(0.0));
160 assert_eq!(t.update(c(10.5, 11.6, 10.4, 11.5, 2)), Some(-1.0));
161 }
162
163 #[test]
164 fn first_two_bars_return_zero() {
165 let mut t = TasukiGap::new();
166 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
167 assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), Some(0.0));
168 }
169
170 #[test]
171 fn up_no_gap_yields_zero() {
172 let mut t = TasukiGap::new();
173 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
174 t.update(c(10.5, 13.1, 10.4, 13.0, 1));
176 assert_eq!(t.update(c(12.5, 12.6, 10.9, 11.0, 2)), Some(0.0));
177 }
178
179 #[test]
180 fn up_third_not_black_yields_zero() {
181 let mut t = TasukiGap::new();
182 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
183 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
184 assert_eq!(t.update(c(12.5, 13.1, 12.4, 13.0, 2)), Some(0.0));
186 }
187
188 #[test]
189 fn up_third_open_outside_body_yields_zero() {
190 let mut t = TasukiGap::new();
191 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
192 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
193 assert_eq!(t.update(c(14.0, 14.1, 11.4, 11.5, 2)), Some(0.0));
195 }
196
197 #[test]
198 fn up_third_close_not_in_gap_yields_zero() {
199 let mut t = TasukiGap::new();
200 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
201 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
202 assert_eq!(t.update(c(13.0, 13.1, 10.4, 10.5, 2)), Some(0.0));
204 }
205
206 #[test]
207 fn down_no_gap_yields_zero() {
208 let mut t = TasukiGap::new();
209 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
210 t.update(c(12.5, 12.6, 10.4, 10.5, 1));
212 assert_eq!(t.update(c(11.0, 12.6, 10.9, 12.0, 2)), Some(0.0));
213 }
214
215 #[test]
216 fn down_third_not_white_yields_zero() {
217 let mut t = TasukiGap::new();
218 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
219 t.update(c(11.0, 11.1, 9.5, 10.0, 1));
220 assert_eq!(t.update(c(11.5, 11.6, 10.4, 10.5, 2)), Some(0.0));
222 }
223
224 #[test]
225 fn down_third_open_outside_body_yields_zero() {
226 let mut t = TasukiGap::new();
227 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
228 t.update(c(11.0, 11.1, 9.5, 10.0, 1));
229 assert_eq!(t.update(c(9.5, 11.6, 9.4, 11.5, 2)), Some(0.0));
231 }
232
233 #[test]
234 fn down_third_close_not_in_gap_yields_zero() {
235 let mut t = TasukiGap::new();
236 t.update(c(13.0, 13.2, 11.8, 12.0, 0));
237 t.update(c(11.0, 11.1, 9.5, 10.0, 1));
238 assert_eq!(t.update(c(10.5, 13.0, 10.4, 12.5, 2)), Some(0.0));
240 }
241
242 #[test]
243 fn mixed_colours_yield_zero() {
244 let mut t = TasukiGap::new();
245 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
247 t.update(c(13.0, 13.2, 11.0, 11.5, 1));
248 assert_eq!(t.update(c(12.0, 12.6, 10.9, 11.0, 2)), Some(0.0));
249 }
250
251 #[test]
252 fn batch_equals_streaming() {
253 let candles: Vec<Candle> = (0..40)
254 .map(|i| {
255 let base = 100.0 + i as f64;
256 c(base, base + 5.2, base - 0.1, base + 5.0, i)
257 })
258 .collect();
259 let mut a = TasukiGap::new();
260 let mut b = TasukiGap::new();
261 assert_eq!(
262 a.batch(&candles),
263 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
264 );
265 }
266
267 #[test]
268 fn reset_clears_state() {
269 let mut t = TasukiGap::new();
270 t.update(c(10.0, 11.2, 9.8, 11.0, 0));
271 t.update(c(12.0, 14.0, 11.9, 13.5, 1));
272 t.update(c(13.0, 13.1, 11.4, 11.5, 2));
273 assert!(t.is_ready());
274 t.reset();
275 assert!(!t.is_ready());
276 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
277 }
278}