Skip to main content

wickra_core/indicators/
wick_ratio.rs

1//! Wick Ratio — the shadow imbalance of a bar.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Wick Ratio — the signed imbalance between the upper and lower shadows as a
7/// fraction of the bar's range.
8///
9/// ```text
10/// upper_wick  = high − max(open, close)
11/// lower_wick  = min(open, close) − low
12/// WickRatio   = (upper_wick − lower_wick) / (high − low)
13/// ```
14///
15/// The result lives in `[−1, +1]`: `+1` is a bar that is all upper shadow (a
16/// long rejection of higher prices, classic shooting-star geometry), `−1` all
17/// lower shadow (a long rejection of lower prices, hammer geometry), and `0`
18/// either a symmetric bar or a wickless one. Where
19/// [`BodySizePct`](crate::BodySizePct) measures how much of the range is body,
20/// this measures *which side* the wicks fall on — the rejection asymmetry many
21/// reversal setups depend on. A zero-range bar yields `0`.
22///
23/// This is a stateless per-bar transform: every candle produces one value.
24///
25/// # Example
26///
27/// ```
28/// use wickra_core::{Candle, Indicator, WickRatio};
29///
30/// let mut indicator = WickRatio::new();
31/// // upper 13 - 10.5 = 2.5, lower 10 - 10 = 0, range 3 -> +0.8333.
32/// let c = Candle::new(10.0, 13.0, 10.0, 10.5, 10.0, 0).unwrap();
33/// assert!((indicator.update(c).unwrap() - 2.5 / 3.0).abs() < 1e-12);
34/// ```
35#[derive(Debug, Clone, Default)]
36pub struct WickRatio {
37    has_emitted: bool,
38}
39
40impl WickRatio {
41    /// Construct a new Wick Ratio transform.
42    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            // A zero-range bar has no shadows to compare.
56            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        // upper 13 - 10.5 = 2.5, lower 10 - 10 = 0, range 3 -> +2.5/3.
97        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        // Hammer: long lower shadow -> negative.
108        // open 12, close 12.5, high 13, low 9: upper 0.5, lower 3, range 4.
109        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        // Equal upper and lower shadows -> 0.
120        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}