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#[derive(Debug, Clone, Default)]
23pub struct TdClopwin {
24    prev: Option<Candle>,
25    last_value: Option<f64>,
26}
27
28impl TdClopwin {
29    /// Construct a new `TdClopwin`.
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 TdClopwin {
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 body_low = prev.open.min(prev.close);
52        let body_high = prev.open.max(prev.close);
53        let open_in = candle.open >= body_low && candle.open <= body_high;
54        let close_in = candle.close >= body_low && candle.close <= body_high;
55        let v = if open_in && close_in {
56            if candle.close >= candle.open {
57                1.0
58            } else {
59                -1.0
60            }
61        } else {
62            0.0
63        };
64        self.prev = Some(candle);
65        self.last_value = Some(v);
66        Some(v)
67    }
68
69    fn reset(&mut self) {
70        self.prev = None;
71        self.last_value = None;
72    }
73
74    fn warmup_period(&self) -> usize {
75        2
76    }
77
78    fn is_ready(&self) -> bool {
79        self.last_value.is_some()
80    }
81
82    fn name(&self) -> &'static str {
83        "TDClopwin"
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::traits::BatchExt;
91
92    fn c(open: f64, close: f64) -> Candle {
93        let high = open.max(close) + 1.0;
94        let low = open.min(close) - 1.0;
95        Candle::new_unchecked(open, high, low, close, 0.0, 0)
96    }
97
98    #[test]
99    fn accessors_and_metadata() {
100        let td = TdClopwin::new();
101        assert_eq!(td.warmup_period(), 2);
102        assert_eq!(td.name(), "TDClopwin");
103        assert!(!td.is_ready());
104        assert_eq!(td.value(), None);
105    }
106
107    #[test]
108    fn first_bar_seeds_without_signal() {
109        let mut td = TdClopwin::new();
110        assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
111        assert!(td.update(c(11.0, 13.0)).is_some());
112    }
113
114    #[test]
115    fn bullish_inside_body_buy() {
116        // prev body [10, 14]. Current open 11, close 13 both inside, close>open -> +1.
117        let mut td = TdClopwin::new();
118        td.update(c(10.0, 14.0));
119        assert_eq!(td.update(c(11.0, 13.0)), Some(1.0));
120    }
121
122    #[test]
123    fn bearish_inside_body_sell() {
124        // prev body [10, 14]. Current open 13, close 11 inside, close<open -> -1.
125        let mut td = TdClopwin::new();
126        td.update(c(10.0, 14.0));
127        assert_eq!(td.update(c(13.0, 11.0)), Some(-1.0));
128    }
129
130    #[test]
131    fn outside_body_is_zero() {
132        let mut td = TdClopwin::new();
133        td.update(c(10.0, 14.0));
134        // close 16 outside the prior body -> 0.
135        assert_eq!(td.update(c(11.0, 16.0)), Some(0.0));
136    }
137
138    #[test]
139    fn reset_clears_state() {
140        let mut td = TdClopwin::new();
141        td.update(c(10.0, 14.0));
142        td.update(c(11.0, 13.0));
143        assert!(td.is_ready());
144        td.reset();
145        assert!(!td.is_ready());
146        assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
147    }
148
149    #[test]
150    fn batch_equals_streaming() {
151        let candles: Vec<Candle> = (0..40)
152            .map(|i| {
153                let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
154                c(b, b + 0.3)
155            })
156            .collect();
157        let batch = TdClopwin::new().batch(&candles);
158        let mut b = TdClopwin::new();
159        let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
160        assert_eq!(batch, streamed);
161    }
162}