wickra_core/indicators/
tower_top_bottom.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
20use crate::traits::Indicator;
21
22fn body_fraction(candle: Candle) -> f64 {
23 let range = candle.high - candle.low;
24 if range > 0.0 {
25 (candle.close - candle.open).abs() / range
26 } else {
27 0.0
28 }
29}
30
31fn is_tall(candle: Candle) -> bool {
32 body_fraction(candle) >= 0.5
33}
34
35fn is_small(candle: Candle) -> bool {
36 body_fraction(candle) <= 0.3
37}
38
39#[derive(Debug, Clone, Default)]
56pub struct TowerTopBottom {
57 c1: Option<Candle>,
58 c2: Option<Candle>,
59 last_value: Option<f64>,
60}
61
62impl TowerTopBottom {
63 #[must_use]
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub const fn value(&self) -> Option<f64> {
71 self.last_value
72 }
73}
74
75impl Indicator for TowerTopBottom {
76 type Input = Candle;
77 type Output = f64;
78
79 #[inline]
80 fn update(&mut self, candle: Candle) -> Option<f64> {
81 let (Some(first), Some(middle)) = (self.c1, self.c2) else {
82 self.c1 = self.c2;
83 self.c2 = Some(candle);
84 self.last_value = None;
85 return None;
86 };
87 let pause = is_small(middle);
88 let first_tall = is_tall(first);
89 let last_tall = is_tall(candle);
90 let v = if pause && first_tall && last_tall {
91 let first_up = first.close > first.open;
92 let last_up = candle.close > candle.open;
93 if !first_up && last_up {
94 1.0
95 } else if first_up && !last_up {
96 -1.0
97 } else {
98 0.0
99 }
100 } else {
101 0.0
102 };
103 self.c1 = self.c2;
104 self.c2 = Some(candle);
105 self.last_value = Some(v);
106 Some(v)
107 }
108
109 fn reset(&mut self) {
110 self.c1 = None;
111 self.c2 = None;
112 self.last_value = None;
113 }
114
115 #[inline]
116 fn warmup_period(&self) -> usize {
117 3
118 }
119
120 #[inline]
121 fn is_ready(&self) -> bool {
122 self.last_value.is_some()
123 }
124
125 #[inline]
126 fn name(&self) -> &'static str {
127 "TowerTopBottom"
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::traits::BatchExt;
135
136 fn tall(open: f64, close: f64) -> Candle {
138 Candle::new_unchecked(
139 open,
140 open.max(close) + 0.1,
141 open.min(close) - 0.1,
142 close,
143 0.0,
144 0,
145 )
146 }
147
148 fn small(mid: f64) -> Candle {
150 Candle::new_unchecked(mid, mid + 2.0, mid - 2.0, mid + 0.1, 0.0, 0)
151 }
152
153 #[test]
154 fn accessors_and_metadata() {
155 let t = TowerTopBottom::new();
156 assert_eq!(t.warmup_period(), 3);
157 assert_eq!(t.name(), "TowerTopBottom");
158 assert!(!t.is_ready());
159 assert_eq!(t.value(), None);
160 }
161
162 #[test]
163 fn first_two_bars_seed_without_signal() {
164 let mut t = TowerTopBottom::new();
165 assert_eq!(t.update(tall(100.0, 110.0)), None);
166 assert_eq!(t.update(small(105.0)), None);
167 assert!(t.update(tall(110.0, 100.0)).is_some());
168 }
169
170 #[test]
171 fn tower_top() {
172 let mut t = TowerTopBottom::new();
174 t.update(tall(100.0, 110.0));
175 t.update(small(110.0));
176 assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
177 }
178
179 #[test]
180 fn tower_bottom() {
181 let mut t = TowerTopBottom::new();
182 t.update(tall(110.0, 100.0));
183 t.update(small(100.0));
184 assert_eq!(t.update(tall(100.0, 110.0)), Some(1.0));
185 }
186
187 #[test]
188 fn same_direction_is_zero() {
189 let mut t = TowerTopBottom::new();
190 t.update(tall(100.0, 110.0));
191 t.update(small(110.0));
192 assert_eq!(t.update(tall(110.0, 120.0)), Some(0.0));
194 }
195
196 #[test]
197 fn no_pause_is_zero() {
198 let mut t = TowerTopBottom::new();
199 t.update(tall(100.0, 110.0));
200 t.update(tall(110.0, 120.0)); assert_eq!(t.update(tall(120.0, 110.0)), Some(0.0));
202 }
203
204 #[test]
205 fn reset_clears_state() {
206 let mut t = TowerTopBottom::new();
207 t.update(tall(100.0, 110.0));
208 t.update(small(110.0));
209 t.update(tall(110.0, 100.0));
210 assert!(t.is_ready());
211 t.reset();
212 assert!(!t.is_ready());
213 assert_eq!(t.update(tall(100.0, 110.0)), None);
214 }
215
216 #[test]
217 fn zero_range_bar_has_zero_body_fraction() {
218 fn flat(mid: f64) -> Candle {
221 Candle::new_unchecked(mid, mid, mid, mid, 0.0, 0)
222 }
223 let mut t = TowerTopBottom::new();
224 t.update(tall(100.0, 110.0));
225 t.update(flat(110.0));
226 assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
227 }
228
229 #[test]
230 fn batch_equals_streaming() {
231 let candles: Vec<Candle> = (0..30)
232 .map(|i| match i % 3 {
233 0 => tall(100.0, 110.0),
234 1 => small(110.0),
235 _ => tall(110.0, 100.0),
236 })
237 .collect();
238 let batch = TowerTopBottom::new().batch(&candles);
239 let mut b = TowerTopBottom::new();
240 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
241 assert_eq!(batch, streamed);
242 }
243}