Skip to main content

wickra_core/indicators/
td_clop.rs

1#![allow(clippy::doc_markdown)]
2
3//! Tom DeMark TD Clop — a 2-bar open/close engulfing reversal.
4//!
5//! TD Clop ("CLose/OPen") fires when the current bar's open opens beyond **both**
6//! the prior bar's open and close, and its close finishes back beyond both — an
7//! open-gap that fully reverses, signalling a turn.
8//!
9//! - **Buy signal** (`+1.0`): `open < open[-1]` AND `open < close[-1]`
10//!   (opens below the whole prior body) AND `close > open[-1]` AND
11//!   `close > close[-1]` (closes above it).
12//! - **Sell signal** (`-1.0`): `open > open[-1]` AND `open > close[-1]` AND
13//!   `close < open[-1]` AND `close < close[-1]`.
14//! - Otherwise the output is `0.0`.
15//!
16//! The one-bar lookback means the first value lands on the second candle.
17
18use crate::ohlcv::Candle;
19use crate::traits::Indicator;
20
21/// TD Clop — 2-bar open/close engulfing reversal detector.
22#[derive(Debug, Clone, Default)]
23pub struct TdClop {
24    prev: Option<Candle>,
25    last_value: Option<f64>,
26}
27
28impl TdClop {
29    /// Construct a new `TdClop`.
30    #[must_use]
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Latest emitted signal if available.
36    pub const fn value(&self) -> Option<f64> {
37        self.last_value
38    }
39}
40
41impl Indicator for TdClop {
42    type Input = Candle;
43    type Output = f64;
44
45    fn update(&mut self, candle: Candle) -> Option<f64> {
46        let Some(prev) = self.prev else {
47            self.prev = Some(candle);
48            self.last_value = Some(0.0);
49            return Some(0.0);
50        };
51        let below_body = candle.open < prev.open && candle.open < prev.close;
52        let above_body = candle.close > prev.open && candle.close > prev.close;
53        let over_body = candle.open > prev.open && candle.open > prev.close;
54        let under_body = candle.close < prev.open && candle.close < prev.close;
55        let v = if below_body && above_body {
56            1.0
57        } else if over_body && under_body {
58            -1.0
59        } else {
60            0.0
61        };
62        self.prev = Some(candle);
63        self.last_value = Some(v);
64        Some(v)
65    }
66
67    fn reset(&mut self) {
68        self.prev = None;
69        self.last_value = None;
70    }
71
72    fn warmup_period(&self) -> usize {
73        2
74    }
75
76    fn is_ready(&self) -> bool {
77        self.last_value.is_some()
78    }
79
80    fn name(&self) -> &'static str {
81        "TDClop"
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::traits::BatchExt;
89
90    fn c(open: f64, close: f64) -> Candle {
91        let high = open.max(close) + 1.0;
92        let low = open.min(close) - 1.0;
93        Candle::new_unchecked(open, high, low, close, 0.0, 0)
94    }
95
96    #[test]
97    fn accessors_and_metadata() {
98        let td = TdClop::new();
99        assert_eq!(td.warmup_period(), 2);
100        assert_eq!(td.name(), "TDClop");
101        assert!(!td.is_ready());
102        assert_eq!(td.value(), None);
103    }
104
105    #[test]
106    fn first_bar_seeds_without_signal() {
107        let mut td = TdClop::new();
108        assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
109        assert!(td.update(c(9.0, 12.0)).is_some());
110    }
111
112    #[test]
113    fn bullish_clop_buy() {
114        // prev body [10, 11]. Current open 9 < both, close 12 > both -> buy.
115        let mut td = TdClop::new();
116        td.update(c(10.0, 11.0));
117        assert_eq!(td.update(c(9.0, 12.0)), Some(1.0));
118    }
119
120    #[test]
121    fn bearish_clop_sell() {
122        // prev body [10, 11]. Current open 12 > both, close 9 < both -> sell.
123        let mut td = TdClop::new();
124        td.update(c(10.0, 11.0));
125        assert_eq!(td.update(c(12.0, 9.0)), Some(-1.0));
126    }
127
128    #[test]
129    fn no_pattern_is_zero() {
130        let mut td = TdClop::new();
131        td.update(c(10.0, 11.0));
132        assert_eq!(td.update(c(10.5, 11.5)), Some(0.0));
133    }
134
135    #[test]
136    fn reset_clears_state() {
137        let mut td = TdClop::new();
138        td.update(c(10.0, 11.0));
139        td.update(c(9.0, 12.0));
140        assert!(td.is_ready());
141        td.reset();
142        assert!(!td.is_ready());
143        assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
144    }
145
146    #[test]
147    fn batch_equals_streaming() {
148        let candles: Vec<Candle> = (0..40)
149            .map(|i| {
150                let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
151                c(b, b + 0.5)
152            })
153            .collect();
154        let batch = TdClop::new().batch(&candles);
155        let mut b = TdClop::new();
156        let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
157        assert_eq!(batch, streamed);
158    }
159}