Skip to main content

wickra_core/indicators/
unique_three_river.rs

1//! Unique Three River candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Unique Three River (Bottom) — a 3-bar bullish reversal. A long black candle is
7/// followed by a smaller black candle whose body sits inside the first but whose
8/// long lower shadow probes a new low, then a small white candle that stays below
9/// the second body. The fresh low that fails to hold marks an exhausted decline.
10///
11/// ```text
12/// bar1 long black: open1 − close1 >= 0.5 * (high1 − low1)
13/// bar2 black, body inside bar1's body, with a new low (low2 < low1)
14/// bar3 small white, contained below bar2's body (high3 <= close2)
15/// small body: close3 − open3 <= 0.3 * (high3 − low3)
16/// ```
17///
18/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Unique Three
19/// River is a single-direction (bullish-only) reversal, so it never emits `−1.0`.
20/// The first two bars always return `0.0` because the three-bar window is not yet
21/// filled. Body thresholds follow the geometric house style rather than TA-Lib's
22/// rolling averages. Pattern-shape check only — no trend filter is applied; combine
23/// 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` bullish, `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, Indicator, UniqueThreeRiver};
35///
36/// let mut indicator = UniqueThreeRiver::new();
37/// indicator.update(Candle::new(15.0, 15.1, 10.0, 10.5, 1.0, 0).unwrap());
38/// indicator.update(Candle::new(14.0, 14.1, 9.0, 11.0, 1.0, 1).unwrap());
39/// let out = indicator
40///     .update(Candle::new(10.2, 10.9, 9.5, 10.4, 1.0, 2).unwrap());
41/// assert_eq!(out, Some(1.0));
42/// ```
43#[derive(Debug, Clone, Default)]
44pub struct UniqueThreeRiver {
45    c1: Option<Candle>,
46    c2: Option<Candle>,
47    has_emitted: bool,
48}
49
50impl UniqueThreeRiver {
51    /// Construct a new Unique Three River detector.
52    pub const fn new() -> Self {
53        Self {
54            c1: None,
55            c2: None,
56            has_emitted: false,
57        }
58    }
59}
60
61impl Indicator for UniqueThreeRiver {
62    type Input = Candle;
63    type Output = f64;
64
65    fn update(&mut self, candle: Candle) -> Option<f64> {
66        self.has_emitted = true;
67        let bar1 = self.c1;
68        let bar2 = self.c2;
69        self.c1 = self.c2;
70        self.c2 = Some(candle);
71        let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
72            return Some(0.0);
73        };
74        // bar1 is a long black body.
75        if bar1.open <= bar1.close {
76            return Some(0.0);
77        }
78        let range1 = bar1.high - bar1.low;
79        if bar1.open - bar1.close < 0.5 * range1 {
80            return Some(0.0);
81        }
82        // bar2 is black, its body inside bar1's body, with a new low.
83        if bar2.open <= bar2.close {
84            return Some(0.0);
85        }
86        if bar2.open > bar1.open || bar2.close < bar1.close {
87            return Some(0.0);
88        }
89        if bar2.low >= bar1.low {
90            return Some(0.0);
91        }
92        // bar3 is a small white candle contained below bar2's body.
93        if candle.close <= candle.open {
94            return Some(0.0);
95        }
96        let range3 = candle.high - candle.low;
97        if candle.close - candle.open > 0.3 * range3 {
98            return Some(0.0);
99        }
100        if candle.high > bar2.close {
101            return Some(0.0);
102        }
103        Some(1.0)
104    }
105
106    fn reset(&mut self) {
107        self.c1 = None;
108        self.c2 = None;
109        self.has_emitted = false;
110    }
111
112    fn warmup_period(&self) -> usize {
113        3
114    }
115
116    fn is_ready(&self) -> bool {
117        self.has_emitted
118    }
119
120    fn name(&self) -> &'static str {
121        "UniqueThreeRiver"
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::traits::BatchExt;
129
130    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
131        Candle::new(open, high, low, close, 1.0, ts).unwrap()
132    }
133
134    #[test]
135    fn accessors_and_metadata() {
136        let t = UniqueThreeRiver::new();
137        assert_eq!(t.name(), "UniqueThreeRiver");
138        assert_eq!(t.warmup_period(), 3);
139        assert!(!t.is_ready());
140    }
141
142    #[test]
143    fn unique_three_river_is_plus_one() {
144        let mut t = UniqueThreeRiver::new();
145        assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
146        assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), Some(0.0));
147        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(1.0));
148    }
149
150    #[test]
151    fn first_two_bars_return_zero() {
152        let mut t = UniqueThreeRiver::new();
153        assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
154        assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), Some(0.0));
155    }
156
157    #[test]
158    fn first_bar_not_black_yields_zero() {
159        let mut t = UniqueThreeRiver::new();
160        // bar1 white.
161        t.update(c(10.5, 15.1, 10.0, 15.0, 0));
162        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
163        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
164    }
165
166    #[test]
167    fn first_bar_short_body_yields_zero() {
168        let mut t = UniqueThreeRiver::new();
169        // bar1 black but its body is short relative to range.
170        t.update(c(15.0, 15.1, 10.0, 14.5, 0));
171        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
172        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
173    }
174
175    #[test]
176    fn second_bar_not_black_yields_zero() {
177        let mut t = UniqueThreeRiver::new();
178        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
179        // bar2 white.
180        t.update(c(11.0, 14.1, 9.0, 13.0, 1));
181        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
182    }
183
184    #[test]
185    fn second_bar_not_inside_yields_zero() {
186        let mut t = UniqueThreeRiver::new();
187        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
188        // bar2 black but opens above bar1's open -> body not inside.
189        t.update(c(16.0, 16.1, 9.0, 11.0, 1));
190        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
191    }
192
193    #[test]
194    fn second_bar_no_new_low_yields_zero() {
195        let mut t = UniqueThreeRiver::new();
196        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
197        // bar2 black, inside, but does not make a new low.
198        t.update(c(14.0, 14.1, 10.5, 11.0, 1));
199        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
200    }
201
202    #[test]
203    fn third_bar_not_white_yields_zero() {
204        let mut t = UniqueThreeRiver::new();
205        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
206        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
207        // bar3 black.
208        assert_eq!(t.update(c(10.6, 10.9, 9.5, 10.2, 2)), Some(0.0));
209    }
210
211    #[test]
212    fn third_bar_large_body_yields_zero() {
213        let mut t = UniqueThreeRiver::new();
214        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
215        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
216        // bar3 white but with a large body.
217        assert_eq!(t.update(c(9.6, 10.9, 9.5, 10.8, 2)), Some(0.0));
218    }
219
220    #[test]
221    fn third_bar_not_below_second_yields_zero() {
222        let mut t = UniqueThreeRiver::new();
223        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
224        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
225        // bar3 small white but pokes above bar2's close.
226        assert_eq!(t.update(c(10.5, 11.5, 10.4, 10.7, 2)), Some(0.0));
227    }
228
229    #[test]
230    fn batch_equals_streaming() {
231        let candles: Vec<Candle> = (0..40)
232            .map(|i| {
233                let base = 200.0 - i as f64;
234                c(base, base + 0.1, base - 5.2, base - 5.0, i)
235            })
236            .collect();
237        let mut a = UniqueThreeRiver::new();
238        let mut b = UniqueThreeRiver::new();
239        assert_eq!(
240            a.batch(&candles),
241            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
242        );
243    }
244
245    #[test]
246    fn reset_clears_state() {
247        let mut t = UniqueThreeRiver::new();
248        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
249        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
250        t.update(c(10.2, 10.9, 9.5, 10.4, 2));
251        assert!(t.is_ready());
252        t.reset();
253        assert!(!t.is_ready());
254        assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
255    }
256}