Skip to main content

wickra_core/indicators/
in_neck.rs

1//! In-Neck candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// In-Neck — a 2-bar bearish continuation, slightly stronger than On-Neck. A long
7/// black candle in a decline is followed by a white candle that opens below the
8/// black bar's low and closes just barely *into* the black body, around its close
9/// level. The shallow recovery still favours the sellers.
10///
11/// ```text
12/// long body = |close − open| >= 0.5 * (high − low)
13/// bar1 black & long
14/// bar2 white, opens below bar1's low      (open2 < low1)
15/// bar2 closes just into bar1's body        (close1 <= close2 <= close1 + 0.1 · body1)
16/// ```
17///
18/// Output is `−1.0` when the pattern completes and `0.0` otherwise. In-Neck is a
19/// single-direction (bearish-only) continuation, so it never emits `+1.0`. The
20/// first bar always returns `0.0` because the two-bar window is not yet filled.
21/// Body and neckline thresholds follow the geometric house style rather than
22/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
23/// applied; combine with a trend indicator for actionable signals.
24///
25/// # Signed ±1 encoding
26///
27/// This detector emits the uniform candlestick sign convention shared across the
28/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
29/// a machine-learning feature matrix as a single dimension.
30///
31/// # Example
32///
33/// ```
34/// use wickra_core::{Candle, InNeck, Indicator};
35///
36/// let mut indicator = InNeck::new();
37/// indicator.update(Candle::new(15.0, 15.1, 9.0, 10.0, 1.0, 0).unwrap());
38/// let out = indicator
39///     .update(Candle::new(7.0, 10.3, 6.9, 10.2, 1.0, 1).unwrap());
40/// assert_eq!(out, Some(-1.0));
41/// ```
42#[derive(Debug, Clone, Default)]
43pub struct InNeck {
44    prev: Option<Candle>,
45    has_emitted: bool,
46}
47
48impl InNeck {
49    /// Construct a new In-Neck detector.
50    pub const fn new() -> Self {
51        Self {
52            prev: None,
53            has_emitted: false,
54        }
55    }
56}
57
58impl Indicator for InNeck {
59    type Input = Candle;
60    type Output = f64;
61
62    #[inline]
63    fn update(&mut self, candle: Candle) -> Option<f64> {
64        let prev = self.prev;
65        self.prev = Some(candle);
66        let bar1 = prev?;
67        self.has_emitted = true;
68        let range1 = bar1.high - bar1.low;
69        if range1 <= 0.0 {
70            return Some(0.0);
71        }
72        let body1 = bar1.open - bar1.close;
73        if bar1.close < bar1.open
74            && body1 >= 0.5 * range1
75            && candle.close > candle.open
76            && candle.open < bar1.low
77            && candle.close >= bar1.close
78            && candle.close <= bar1.close + 0.1 * body1
79        {
80            return Some(-1.0);
81        }
82        Some(0.0)
83    }
84
85    fn reset(&mut self) {
86        self.prev = None;
87        self.has_emitted = false;
88    }
89
90    #[inline]
91    fn warmup_period(&self) -> usize {
92        2
93    }
94
95    #[inline]
96    fn is_ready(&self) -> bool {
97        self.has_emitted
98    }
99
100    #[inline]
101    fn name(&self) -> &'static str {
102        "InNeck"
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::traits::BatchExt;
110
111    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
112        Candle::new(open, high, low, close, 1.0, ts).unwrap()
113    }
114
115    #[test]
116    fn accessors_and_metadata() {
117        let t = InNeck::new();
118        assert_eq!(t.name(), "InNeck");
119        assert_eq!(t.warmup_period(), 2);
120        assert!(!t.is_ready());
121    }
122
123    #[test]
124    fn in_neck_is_minus_one() {
125        let mut t = InNeck::new();
126        assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), None);
127        assert_eq!(t.update(c(7.0, 10.3, 6.9, 10.2, 1)), Some(-1.0));
128    }
129
130    #[test]
131    fn close_at_low_yields_zero() {
132        let mut t = InNeck::new();
133        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
134        // Closes at the prior low, not into the body -> on-neck, not in-neck.
135        assert_eq!(t.update(c(7.0, 9.1, 6.9, 9.0, 1)), Some(0.0));
136    }
137
138    #[test]
139    fn close_past_neck_yields_zero() {
140        let mut t = InNeck::new();
141        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
142        // Closes well into the body -> thrusting, not in-neck.
143        assert_eq!(t.update(c(7.0, 11.6, 6.9, 11.5, 1)), Some(0.0));
144    }
145
146    #[test]
147    fn second_bar_black_yields_zero() {
148        let mut t = InNeck::new();
149        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
150        assert_eq!(t.update(c(10.4, 10.5, 6.9, 10.1, 1)), Some(0.0));
151    }
152
153    #[test]
154    fn first_bar_returns_zero() {
155        let mut t = InNeck::new();
156        assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), None);
157    }
158
159    #[test]
160    fn batch_equals_streaming() {
161        let candles: Vec<Candle> = (0..40)
162            .map(|i| {
163                let base = 100.0 + i as f64;
164                c(base + 5.0, base + 5.1, base - 1.0, base, i)
165            })
166            .collect();
167        let mut a = InNeck::new();
168        let mut b = InNeck::new();
169        assert_eq!(
170            a.batch(&candles),
171            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
172        );
173    }
174
175    #[test]
176    fn reset_clears_state() {
177        let mut t = InNeck::new();
178        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
179        t.update(c(7.0, 10.3, 6.9, 10.2, 1));
180        assert!(t.is_ready());
181        t.reset();
182        assert!(!t.is_ready());
183        assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), None);
184    }
185
186    #[test]
187    fn zero_range_first_bar_yields_zero() {
188        let mut t = InNeck::new();
189        // Flat first bar (range1 == 0) -> rejected.
190        t.update(c(10.0, 10.0, 10.0, 10.0, 0));
191        assert_eq!(t.update(c(9.0, 10.0, 8.0, 9.5, 1)), Some(0.0));
192    }
193}