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 fn update(&mut self, candle: Candle) -> Option<f64> {
80 let (Some(first), Some(middle)) = (self.c1, self.c2) else {
81 self.c1 = self.c2;
82 self.c2 = Some(candle);
83 self.last_value = Some(0.0);
84 return Some(0.0);
85 };
86 let pause = is_small(middle);
87 let first_tall = is_tall(first);
88 let last_tall = is_tall(candle);
89 let v = if pause && first_tall && last_tall {
90 let first_up = first.close > first.open;
91 let last_up = candle.close > candle.open;
92 if !first_up && last_up {
93 1.0
94 } else if first_up && !last_up {
95 -1.0
96 } else {
97 0.0
98 }
99 } else {
100 0.0
101 };
102 self.c1 = self.c2;
103 self.c2 = Some(candle);
104 self.last_value = Some(v);
105 Some(v)
106 }
107
108 fn reset(&mut self) {
109 self.c1 = None;
110 self.c2 = None;
111 self.last_value = None;
112 }
113
114 fn warmup_period(&self) -> usize {
115 3
116 }
117
118 fn is_ready(&self) -> bool {
119 self.last_value.is_some()
120 }
121
122 fn name(&self) -> &'static str {
123 "TowerTopBottom"
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::traits::BatchExt;
131
132 fn tall(open: f64, close: f64) -> Candle {
134 Candle::new_unchecked(
135 open,
136 open.max(close) + 0.1,
137 open.min(close) - 0.1,
138 close,
139 0.0,
140 0,
141 )
142 }
143
144 fn small(mid: f64) -> Candle {
146 Candle::new_unchecked(mid, mid + 2.0, mid - 2.0, mid + 0.1, 0.0, 0)
147 }
148
149 #[test]
150 fn accessors_and_metadata() {
151 let t = TowerTopBottom::new();
152 assert_eq!(t.warmup_period(), 3);
153 assert_eq!(t.name(), "TowerTopBottom");
154 assert!(!t.is_ready());
155 assert_eq!(t.value(), None);
156 }
157
158 #[test]
159 fn first_two_bars_seed_without_signal() {
160 let mut t = TowerTopBottom::new();
161 assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
162 assert_eq!(t.update(small(105.0)), Some(0.0));
163 assert!(t.update(tall(110.0, 100.0)).is_some());
164 }
165
166 #[test]
167 fn tower_top() {
168 let mut t = TowerTopBottom::new();
170 t.update(tall(100.0, 110.0));
171 t.update(small(110.0));
172 assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
173 }
174
175 #[test]
176 fn tower_bottom() {
177 let mut t = TowerTopBottom::new();
178 t.update(tall(110.0, 100.0));
179 t.update(small(100.0));
180 assert_eq!(t.update(tall(100.0, 110.0)), Some(1.0));
181 }
182
183 #[test]
184 fn same_direction_is_zero() {
185 let mut t = TowerTopBottom::new();
186 t.update(tall(100.0, 110.0));
187 t.update(small(110.0));
188 assert_eq!(t.update(tall(110.0, 120.0)), Some(0.0));
190 }
191
192 #[test]
193 fn no_pause_is_zero() {
194 let mut t = TowerTopBottom::new();
195 t.update(tall(100.0, 110.0));
196 t.update(tall(110.0, 120.0)); assert_eq!(t.update(tall(120.0, 110.0)), Some(0.0));
198 }
199
200 #[test]
201 fn reset_clears_state() {
202 let mut t = TowerTopBottom::new();
203 t.update(tall(100.0, 110.0));
204 t.update(small(110.0));
205 t.update(tall(110.0, 100.0));
206 assert!(t.is_ready());
207 t.reset();
208 assert!(!t.is_ready());
209 assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
210 }
211
212 #[test]
213 fn zero_range_bar_has_zero_body_fraction() {
214 fn flat(mid: f64) -> Candle {
217 Candle::new_unchecked(mid, mid, mid, mid, 0.0, 0)
218 }
219 let mut t = TowerTopBottom::new();
220 t.update(tall(100.0, 110.0));
221 t.update(flat(110.0));
222 assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
223 }
224
225 #[test]
226 fn batch_equals_streaming() {
227 let candles: Vec<Candle> = (0..30)
228 .map(|i| match i % 3 {
229 0 => tall(100.0, 110.0),
230 1 => small(110.0),
231 _ => tall(110.0, 100.0),
232 })
233 .collect();
234 let batch = TowerTopBottom::new().batch(&candles);
235 let mut b = TowerTopBottom::new();
236 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
237 assert_eq!(batch, streamed);
238 }
239}