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