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)]
41pub struct TowerTopBottom {
42 c1: Option<Candle>,
43 c2: Option<Candle>,
44 last_value: Option<f64>,
45}
46
47impl TowerTopBottom {
48 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub const fn value(&self) -> Option<f64> {
56 self.last_value
57 }
58}
59
60impl Indicator for TowerTopBottom {
61 type Input = Candle;
62 type Output = f64;
63
64 fn update(&mut self, candle: Candle) -> Option<f64> {
65 let (Some(first), Some(middle)) = (self.c1, self.c2) else {
66 self.c1 = self.c2;
67 self.c2 = Some(candle);
68 self.last_value = Some(0.0);
69 return Some(0.0);
70 };
71 let pause = is_small(middle);
72 let first_tall = is_tall(first);
73 let last_tall = is_tall(candle);
74 let v = if pause && first_tall && last_tall {
75 let first_up = first.close > first.open;
76 let last_up = candle.close > candle.open;
77 if !first_up && last_up {
78 1.0
79 } else if first_up && !last_up {
80 -1.0
81 } else {
82 0.0
83 }
84 } else {
85 0.0
86 };
87 self.c1 = self.c2;
88 self.c2 = Some(candle);
89 self.last_value = Some(v);
90 Some(v)
91 }
92
93 fn reset(&mut self) {
94 self.c1 = None;
95 self.c2 = None;
96 self.last_value = None;
97 }
98
99 fn warmup_period(&self) -> usize {
100 3
101 }
102
103 fn is_ready(&self) -> bool {
104 self.last_value.is_some()
105 }
106
107 fn name(&self) -> &'static str {
108 "TowerTopBottom"
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::traits::BatchExt;
116
117 fn tall(open: f64, close: f64) -> Candle {
119 Candle::new_unchecked(
120 open,
121 open.max(close) + 0.1,
122 open.min(close) - 0.1,
123 close,
124 0.0,
125 0,
126 )
127 }
128
129 fn small(mid: f64) -> Candle {
131 Candle::new_unchecked(mid, mid + 2.0, mid - 2.0, mid + 0.1, 0.0, 0)
132 }
133
134 #[test]
135 fn accessors_and_metadata() {
136 let t = TowerTopBottom::new();
137 assert_eq!(t.warmup_period(), 3);
138 assert_eq!(t.name(), "TowerTopBottom");
139 assert!(!t.is_ready());
140 assert_eq!(t.value(), None);
141 }
142
143 #[test]
144 fn first_two_bars_seed_without_signal() {
145 let mut t = TowerTopBottom::new();
146 assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
147 assert_eq!(t.update(small(105.0)), Some(0.0));
148 assert!(t.update(tall(110.0, 100.0)).is_some());
149 }
150
151 #[test]
152 fn tower_top() {
153 let mut t = TowerTopBottom::new();
155 t.update(tall(100.0, 110.0));
156 t.update(small(110.0));
157 assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
158 }
159
160 #[test]
161 fn tower_bottom() {
162 let mut t = TowerTopBottom::new();
163 t.update(tall(110.0, 100.0));
164 t.update(small(100.0));
165 assert_eq!(t.update(tall(100.0, 110.0)), Some(1.0));
166 }
167
168 #[test]
169 fn same_direction_is_zero() {
170 let mut t = TowerTopBottom::new();
171 t.update(tall(100.0, 110.0));
172 t.update(small(110.0));
173 assert_eq!(t.update(tall(110.0, 120.0)), Some(0.0));
175 }
176
177 #[test]
178 fn no_pause_is_zero() {
179 let mut t = TowerTopBottom::new();
180 t.update(tall(100.0, 110.0));
181 t.update(tall(110.0, 120.0)); assert_eq!(t.update(tall(120.0, 110.0)), Some(0.0));
183 }
184
185 #[test]
186 fn reset_clears_state() {
187 let mut t = TowerTopBottom::new();
188 t.update(tall(100.0, 110.0));
189 t.update(small(110.0));
190 t.update(tall(110.0, 100.0));
191 assert!(t.is_ready());
192 t.reset();
193 assert!(!t.is_ready());
194 assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
195 }
196
197 #[test]
198 fn zero_range_bar_has_zero_body_fraction() {
199 fn flat(mid: f64) -> Candle {
202 Candle::new_unchecked(mid, mid, mid, mid, 0.0, 0)
203 }
204 let mut t = TowerTopBottom::new();
205 t.update(tall(100.0, 110.0));
206 t.update(flat(110.0));
207 assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
208 }
209
210 #[test]
211 fn batch_equals_streaming() {
212 let candles: Vec<Candle> = (0..30)
213 .map(|i| match i % 3 {
214 0 => tall(100.0, 110.0),
215 1 => small(110.0),
216 _ => tall(110.0, 100.0),
217 })
218 .collect();
219 let batch = TowerTopBottom::new().batch(&candles);
220 let mut b = TowerTopBottom::new();
221 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
222 assert_eq!(batch, streamed);
223 }
224}