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    fn update(&mut self, candle: Candle) -> Option<f64> {
63        self.has_emitted = true;
64        let prev = self.prev;
65        self.prev = Some(candle);
66        let Some(bar1) = prev else {
67            return Some(0.0);
68        };
69        let range1 = bar1.high - bar1.low;
70        if range1 <= 0.0 {
71            return Some(0.0);
72        }
73        let body1 = bar1.open - bar1.close;
74        if bar1.close < bar1.open
75            && body1 >= 0.5 * range1
76            && candle.close > candle.open
77            && candle.open < bar1.low
78            && candle.close >= bar1.close
79            && candle.close <= bar1.close + 0.1 * body1
80        {
81            return Some(-1.0);
82        }
83        Some(0.0)
84    }
85
86    fn reset(&mut self) {
87        self.prev = None;
88        self.has_emitted = false;
89    }
90
91    fn warmup_period(&self) -> usize {
92        2
93    }
94
95    fn is_ready(&self) -> bool {
96        self.has_emitted
97    }
98
99    fn name(&self) -> &'static str {
100        "InNeck"
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::traits::BatchExt;
108
109    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
110        Candle::new(open, high, low, close, 1.0, ts).unwrap()
111    }
112
113    #[test]
114    fn accessors_and_metadata() {
115        let t = InNeck::new();
116        assert_eq!(t.name(), "InNeck");
117        assert_eq!(t.warmup_period(), 2);
118        assert!(!t.is_ready());
119    }
120
121    #[test]
122    fn in_neck_is_minus_one() {
123        let mut t = InNeck::new();
124        assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
125        assert_eq!(t.update(c(7.0, 10.3, 6.9, 10.2, 1)), Some(-1.0));
126    }
127
128    #[test]
129    fn close_at_low_yields_zero() {
130        let mut t = InNeck::new();
131        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
132        // Closes at the prior low, not into the body -> on-neck, not in-neck.
133        assert_eq!(t.update(c(7.0, 9.1, 6.9, 9.0, 1)), Some(0.0));
134    }
135
136    #[test]
137    fn close_past_neck_yields_zero() {
138        let mut t = InNeck::new();
139        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
140        // Closes well into the body -> thrusting, not in-neck.
141        assert_eq!(t.update(c(7.0, 11.6, 6.9, 11.5, 1)), Some(0.0));
142    }
143
144    #[test]
145    fn second_bar_black_yields_zero() {
146        let mut t = InNeck::new();
147        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
148        assert_eq!(t.update(c(10.4, 10.5, 6.9, 10.1, 1)), Some(0.0));
149    }
150
151    #[test]
152    fn first_bar_returns_zero() {
153        let mut t = InNeck::new();
154        assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
155    }
156
157    #[test]
158    fn batch_equals_streaming() {
159        let candles: Vec<Candle> = (0..40)
160            .map(|i| {
161                let base = 100.0 + i as f64;
162                c(base + 5.0, base + 5.1, base - 1.0, base, i)
163            })
164            .collect();
165        let mut a = InNeck::new();
166        let mut b = InNeck::new();
167        assert_eq!(
168            a.batch(&candles),
169            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
170        );
171    }
172
173    #[test]
174    fn reset_clears_state() {
175        let mut t = InNeck::new();
176        t.update(c(15.0, 15.1, 9.0, 10.0, 0));
177        t.update(c(7.0, 10.3, 6.9, 10.2, 1));
178        assert!(t.is_ready());
179        t.reset();
180        assert!(!t.is_ready());
181        assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
182    }
183
184    #[test]
185    fn zero_range_first_bar_yields_zero() {
186        let mut t = InNeck::new();
187        // Flat first bar (range1 == 0) -> rejected.
188        t.update(c(10.0, 10.0, 10.0, 10.0, 0));
189        assert_eq!(t.update(c(9.0, 10.0, 8.0, 9.5, 1)), Some(0.0));
190    }
191}