wickra_core/indicators/
wick_ratio.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
36pub struct WickRatio {
37 has_emitted: bool,
38}
39
40impl WickRatio {
41 pub const fn new() -> Self {
43 Self { has_emitted: false }
44 }
45}
46
47impl Indicator for WickRatio {
48 type Input = Candle;
49 type Output = f64;
50
51 fn update(&mut self, candle: Candle) -> Option<f64> {
52 self.has_emitted = true;
53 let range = candle.high - candle.low;
54 let out = if range == 0.0 {
55 0.0
57 } else {
58 let body_top = candle.open.max(candle.close);
59 let body_bottom = candle.open.min(candle.close);
60 let upper_wick = candle.high - body_top;
61 let lower_wick = body_bottom - candle.low;
62 (upper_wick - lower_wick) / range
63 };
64 Some(out)
65 }
66
67 fn reset(&mut self) {
68 self.has_emitted = false;
69 }
70
71 fn warmup_period(&self) -> usize {
72 1
73 }
74
75 fn is_ready(&self) -> bool {
76 self.has_emitted
77 }
78
79 fn name(&self) -> &'static str {
80 "WickRatio"
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::traits::BatchExt;
88 use approx::assert_relative_eq;
89
90 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
91 Candle::new(open, high, low, close, 1.0, ts).unwrap()
92 }
93
94 #[test]
95 fn upper_shadow_dominates_is_positive() {
96 let mut wr = WickRatio::new();
98 assert_relative_eq!(
99 wr.update(candle(10.0, 13.0, 10.0, 10.5, 0)).unwrap(),
100 2.5 / 3.0,
101 epsilon = 1e-12
102 );
103 }
104
105 #[test]
106 fn lower_shadow_dominates_is_negative() {
107 let mut wr = WickRatio::new();
110 assert_relative_eq!(
111 wr.update(candle(12.0, 13.0, 9.0, 12.5, 0)).unwrap(),
112 (0.5 - 3.0) / 4.0,
113 epsilon = 1e-12
114 );
115 }
116
117 #[test]
118 fn symmetric_wicks_are_zero() {
119 let mut wr = WickRatio::new();
121 assert_relative_eq!(
122 wr.update(candle(10.0, 12.0, 8.0, 10.0, 0)).unwrap(),
123 0.0,
124 epsilon = 1e-12
125 );
126 }
127
128 #[test]
129 fn zero_range_bar_yields_zero() {
130 let mut wr = WickRatio::new();
131 assert_relative_eq!(
132 wr.update(candle(10.0, 10.0, 10.0, 10.0, 0)).unwrap(),
133 0.0,
134 epsilon = 1e-12
135 );
136 }
137
138 #[test]
139 fn stays_within_unit_range() {
140 let candles: Vec<Candle> = (0..100)
141 .map(|i| {
142 let mid = 100.0 + (f64::from(i) * 0.2).sin() * 8.0;
143 let close = mid + (f64::from(i) * 0.5).cos() * 2.0;
144 candle(mid, mid + 3.0, mid - 3.0, close, i64::from(i))
145 })
146 .collect();
147 let mut wr = WickRatio::new();
148 for v in wr.batch(&candles).into_iter().flatten() {
149 assert!((-1.0..=1.0).contains(&v), "WickRatio {v} outside [-1, 1]");
150 }
151 }
152
153 #[test]
154 fn name_metadata() {
155 let wr = WickRatio::new();
156 assert_eq!(wr.name(), "WickRatio");
157 }
158
159 #[test]
160 fn emits_from_first_candle() {
161 let mut wr = WickRatio::new();
162 assert_eq!(wr.warmup_period(), 1);
163 assert!(!wr.is_ready());
164 assert!(wr.update(candle(10.0, 11.0, 9.0, 10.0, 0)).is_some());
165 assert!(wr.is_ready());
166 }
167
168 #[test]
169 fn reset_clears_state() {
170 let mut wr = WickRatio::new();
171 wr.update(candle(10.0, 11.0, 9.0, 10.0, 0));
172 assert!(wr.is_ready());
173 wr.reset();
174 assert!(!wr.is_ready());
175 }
176
177 #[test]
178 fn batch_equals_streaming() {
179 let candles: Vec<Candle> = (0..40)
180 .map(|i| {
181 let base = 100.0 + f64::from(i);
182 candle(base, base + 2.0, base - 2.0, base + 1.0, i64::from(i))
183 })
184 .collect();
185 let mut a = WickRatio::new();
186 let mut b = WickRatio::new();
187 assert_eq!(
188 a.batch(&candles),
189 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
190 );
191 }
192}