Skip to main content

wickra_core/indicators/
td_clopwin.rs

1#![allow(clippy::doc_markdown)]
2
3//! Tom DeMark TD Clopwin — a 2-bar "close/open within" inside-body pattern.
4//!
5//! TD Clopwin ("CLose/OPen WInthIN") is the inside-body cousin of TD Clop: the
6//! current bar's open **and** close both sit within the prior bar's real body,
7//! marking a compression bar whose direction hints at the next move.
8//!
9//! - **Buy signal** (`+1.0`): current `open` and `close` are both inside the prior
10//!   bar's body `[min(open,close)[-1], max(open,close)[-1]]` AND `close >= open`
11//!   (a bullish inside bar).
12//! - **Sell signal** (`-1.0`): both inside the prior body AND `close < open`
13//!   (a bearish inside bar).
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 Clopwin — 2-bar inside-body compression pattern detector.
22/// # Example
23///
24/// ```
25/// use wickra_core::{TdClopwin, Candle, Indicator};
26///
27/// let mut indicator = TdClopwin::new();
28/// // `None` during warmup, then `Some(_)` once enough bars are seen.
29/// let mut out = None;
30/// for i in 0..40i64 {
31///     let p = 100.0 + (i as f64 * 0.4).sin() * 5.0;
32///     let candle = Candle::new(p, p + 1.5, p - 1.5, p + 0.3, 1_000.0, i).unwrap();
33///     out = indicator.update(candle);
34/// }
35/// let _ = out;
36/// ```
37#[derive(Debug, Clone, Default)]
38pub struct TdClopwin {
39    prev: Option<Candle>,
40    last_value: Option<f64>,
41}
42
43impl TdClopwin {
44    /// Construct a new `TdClopwin`.
45    #[must_use]
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Latest emitted signal if available.
51    pub const fn value(&self) -> Option<f64> {
52        self.last_value
53    }
54}
55
56impl Indicator for TdClopwin {
57    type Input = Candle;
58    type Output = f64;
59
60    #[inline]
61    fn update(&mut self, candle: Candle) -> Option<f64> {
62        let Some(prev) = self.prev else {
63            self.prev = Some(candle);
64            self.last_value = None;
65            return None;
66        };
67        let body_low = prev.open.min(prev.close);
68        let body_high = prev.open.max(prev.close);
69        let open_in = candle.open >= body_low && candle.open <= body_high;
70        let close_in = candle.close >= body_low && candle.close <= body_high;
71        let v = if open_in && close_in {
72            if candle.close >= candle.open {
73                1.0
74            } else {
75                -1.0
76            }
77        } else {
78            0.0
79        };
80        self.prev = Some(candle);
81        self.last_value = Some(v);
82        Some(v)
83    }
84
85    fn reset(&mut self) {
86        self.prev = None;
87        self.last_value = None;
88    }
89
90    #[inline]
91    fn warmup_period(&self) -> usize {
92        2
93    }
94
95    #[inline]
96    fn is_ready(&self) -> bool {
97        self.last_value.is_some()
98    }
99
100    #[inline]
101    fn name(&self) -> &'static str {
102        "TDClopwin"
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::traits::BatchExt;
110
111    fn c(open: f64, close: f64) -> Candle {
112        let high = open.max(close) + 1.0;
113        let low = open.min(close) - 1.0;
114        Candle::new_unchecked(open, high, low, close, 0.0, 0)
115    }
116
117    #[test]
118    fn accessors_and_metadata() {
119        let td = TdClopwin::new();
120        assert_eq!(td.warmup_period(), 2);
121        assert_eq!(td.name(), "TDClopwin");
122        assert!(!td.is_ready());
123        assert_eq!(td.value(), None);
124    }
125
126    #[test]
127    fn first_bar_seeds_without_signal() {
128        let mut td = TdClopwin::new();
129        assert_eq!(td.update(c(10.0, 14.0)), None);
130        assert!(td.update(c(11.0, 13.0)).is_some());
131    }
132
133    #[test]
134    fn bullish_inside_body_buy() {
135        // prev body [10, 14]. Current open 11, close 13 both inside, close>open -> +1.
136        let mut td = TdClopwin::new();
137        td.update(c(10.0, 14.0));
138        assert_eq!(td.update(c(11.0, 13.0)), Some(1.0));
139    }
140
141    #[test]
142    fn bearish_inside_body_sell() {
143        // prev body [10, 14]. Current open 13, close 11 inside, close<open -> -1.
144        let mut td = TdClopwin::new();
145        td.update(c(10.0, 14.0));
146        assert_eq!(td.update(c(13.0, 11.0)), Some(-1.0));
147    }
148
149    #[test]
150    fn outside_body_is_zero() {
151        let mut td = TdClopwin::new();
152        td.update(c(10.0, 14.0));
153        // close 16 outside the prior body -> 0.
154        assert_eq!(td.update(c(11.0, 16.0)), Some(0.0));
155    }
156
157    #[test]
158    fn reset_clears_state() {
159        let mut td = TdClopwin::new();
160        td.update(c(10.0, 14.0));
161        td.update(c(11.0, 13.0));
162        assert!(td.is_ready());
163        td.reset();
164        assert!(!td.is_ready());
165        assert_eq!(td.update(c(10.0, 14.0)), None);
166    }
167
168    #[test]
169    fn batch_equals_streaming() {
170        let candles: Vec<Candle> = (0..40)
171            .map(|i| {
172                let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
173                c(b, b + 0.3)
174            })
175            .collect();
176        let batch = TdClopwin::new().batch(&candles);
177        let mut b = TdClopwin::new();
178        let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
179        assert_eq!(batch, streamed);
180    }
181}