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    #[inline]
66    fn update(&mut self, candle: Candle) -> Option<f64> {
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 None;
73        };
74        self.has_emitted = true;
75        // bar1 is a long black body.
76        if bar1.open <= bar1.close {
77            return Some(0.0);
78        }
79        let range1 = bar1.high - bar1.low;
80        if bar1.open - bar1.close < 0.5 * range1 {
81            return Some(0.0);
82        }
83        // bar2 is black, its body inside bar1's body, with a new low.
84        if bar2.open <= bar2.close {
85            return Some(0.0);
86        }
87        if bar2.open > bar1.open || bar2.close < bar1.close {
88            return Some(0.0);
89        }
90        if bar2.low >= bar1.low {
91            return Some(0.0);
92        }
93        // bar3 is a small white candle contained below bar2's body.
94        if candle.close <= candle.open {
95            return Some(0.0);
96        }
97        let range3 = candle.high - candle.low;
98        if candle.close - candle.open > 0.3 * range3 {
99            return Some(0.0);
100        }
101        if candle.high > bar2.close {
102            return Some(0.0);
103        }
104        Some(1.0)
105    }
106
107    fn reset(&mut self) {
108        self.c1 = None;
109        self.c2 = None;
110        self.has_emitted = false;
111    }
112
113    #[inline]
114    fn warmup_period(&self) -> usize {
115        3
116    }
117
118    #[inline]
119    fn is_ready(&self) -> bool {
120        self.has_emitted
121    }
122
123    #[inline]
124    fn name(&self) -> &'static str {
125        "UniqueThreeRiver"
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::traits::BatchExt;
133
134    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
135        Candle::new(open, high, low, close, 1.0, ts).unwrap()
136    }
137
138    #[test]
139    fn accessors_and_metadata() {
140        let t = UniqueThreeRiver::new();
141        assert_eq!(t.name(), "UniqueThreeRiver");
142        assert_eq!(t.warmup_period(), 3);
143        assert!(!t.is_ready());
144    }
145
146    #[test]
147    fn unique_three_river_is_plus_one() {
148        let mut t = UniqueThreeRiver::new();
149        assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), None);
150        assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), None);
151        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(1.0));
152    }
153
154    #[test]
155    fn first_two_bars_return_zero() {
156        let mut t = UniqueThreeRiver::new();
157        assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), None);
158        assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), None);
159    }
160
161    #[test]
162    fn first_bar_not_black_yields_zero() {
163        let mut t = UniqueThreeRiver::new();
164        // bar1 white.
165        t.update(c(10.5, 15.1, 10.0, 15.0, 0));
166        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
167        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
168    }
169
170    #[test]
171    fn first_bar_short_body_yields_zero() {
172        let mut t = UniqueThreeRiver::new();
173        // bar1 black but its body is short relative to range.
174        t.update(c(15.0, 15.1, 10.0, 14.5, 0));
175        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
176        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
177    }
178
179    #[test]
180    fn second_bar_not_black_yields_zero() {
181        let mut t = UniqueThreeRiver::new();
182        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
183        // bar2 white.
184        t.update(c(11.0, 14.1, 9.0, 13.0, 1));
185        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
186    }
187
188    #[test]
189    fn second_bar_not_inside_yields_zero() {
190        let mut t = UniqueThreeRiver::new();
191        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
192        // bar2 black but opens above bar1's open -> body not inside.
193        t.update(c(16.0, 16.1, 9.0, 11.0, 1));
194        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
195    }
196
197    #[test]
198    fn second_bar_no_new_low_yields_zero() {
199        let mut t = UniqueThreeRiver::new();
200        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
201        // bar2 black, inside, but does not make a new low.
202        t.update(c(14.0, 14.1, 10.5, 11.0, 1));
203        assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
204    }
205
206    #[test]
207    fn third_bar_not_white_yields_zero() {
208        let mut t = UniqueThreeRiver::new();
209        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
210        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
211        // bar3 black.
212        assert_eq!(t.update(c(10.6, 10.9, 9.5, 10.2, 2)), Some(0.0));
213    }
214
215    #[test]
216    fn third_bar_large_body_yields_zero() {
217        let mut t = UniqueThreeRiver::new();
218        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
219        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
220        // bar3 white but with a large body.
221        assert_eq!(t.update(c(9.6, 10.9, 9.5, 10.8, 2)), Some(0.0));
222    }
223
224    #[test]
225    fn third_bar_not_below_second_yields_zero() {
226        let mut t = UniqueThreeRiver::new();
227        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
228        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
229        // bar3 small white but pokes above bar2's close.
230        assert_eq!(t.update(c(10.5, 11.5, 10.4, 10.7, 2)), Some(0.0));
231    }
232
233    #[test]
234    fn batch_equals_streaming() {
235        let candles: Vec<Candle> = (0..40)
236            .map(|i| {
237                let base = 200.0 - i as f64;
238                c(base, base + 0.1, base - 5.2, base - 5.0, i)
239            })
240            .collect();
241        let mut a = UniqueThreeRiver::new();
242        let mut b = UniqueThreeRiver::new();
243        assert_eq!(
244            a.batch(&candles),
245            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
246        );
247    }
248
249    #[test]
250    fn reset_clears_state() {
251        let mut t = UniqueThreeRiver::new();
252        t.update(c(15.0, 15.1, 10.0, 10.5, 0));
253        t.update(c(14.0, 14.1, 9.0, 11.0, 1));
254        t.update(c(10.2, 10.9, 9.5, 10.4, 2));
255        assert!(t.is_ready());
256        t.reset();
257        assert!(!t.is_ready());
258        assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), None);
259    }
260}