Skip to main content

wickra_core/indicators/
td_differential.rs

1#![allow(clippy::doc_markdown)]
2
3//! Tom DeMark TD Differential — 2-bar momentum-divergence reversal pattern.
4//!
5//! TD Differential flags an exhaustion-and-reversal candle whose buying or
6//! selling pressure has shifted from the prior bar. The rules use the
7//! current bar's close vs the prior bar's close (direction filter), the
8//! buying pressure `close - low` and the selling pressure `high - close`.
9//!
10//! - **Buy signal** (`+1.0`) on bar `i` when:
11//!   1. `close[i]   <  close[i - 1]`                  (down day)
12//!   2. `close[i]   -  low[i]   >  close[i - 1] - low[i - 1]`   (more buying pressure than the prior bar)
13//!   3. `high[i]    -  close[i] <  high[i - 1] - close[i - 1]`  (less selling pressure than the prior bar)
14//! - **Sell signal** (`-1.0`) on bar `i` when:
15//!   1. `close[i]   >  close[i - 1]`
16//!   2. `high[i]    -  close[i] >  high[i - 1] - close[i - 1]`
17//!   3. `close[i]   -  low[i]   <  close[i - 1] - low[i - 1]`
18//! - Otherwise the output is `0.0`.
19//!
20//! The two-bar lookback means the indicator emits its first value on the
21//! second input candle.
22
23use crate::ohlcv::Candle;
24use crate::traits::Indicator;
25
26/// TD Differential — 2-bar reversal pattern detector.
27#[derive(Debug, Clone, Default)]
28pub struct TdDifferential {
29    prev: Option<Candle>,
30    last_value: Option<f64>,
31}
32
33impl TdDifferential {
34    /// Construct a new `TdDifferential`.
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Latest emitted signal if available.
40    pub const fn value(&self) -> Option<f64> {
41        self.last_value
42    }
43}
44
45impl Indicator for TdDifferential {
46    type Input = Candle;
47    type Output = f64;
48
49    fn update(&mut self, candle: Candle) -> Option<f64> {
50        let Some(prev) = self.prev else {
51            self.prev = Some(candle);
52            return None;
53        };
54        let buying_now = candle.close - candle.low;
55        let buying_prev = prev.close - prev.low;
56        let selling_now = candle.high - candle.close;
57        let selling_prev = prev.high - prev.close;
58
59        let v = if candle.close < prev.close
60            && buying_now > buying_prev
61            && selling_now < selling_prev
62        {
63            1.0
64        } else if candle.close > prev.close
65            && selling_now > selling_prev
66            && buying_now < buying_prev
67        {
68            -1.0
69        } else {
70            0.0
71        };
72
73        self.prev = Some(candle);
74        self.last_value = Some(v);
75        Some(v)
76    }
77
78    fn reset(&mut self) {
79        self.prev = None;
80        self.last_value = None;
81    }
82
83    fn warmup_period(&self) -> usize {
84        2
85    }
86
87    fn is_ready(&self) -> bool {
88        self.last_value.is_some()
89    }
90
91    fn name(&self) -> &'static str {
92        "TDDifferential"
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::traits::BatchExt;
100    use approx::assert_relative_eq;
101
102    fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
103        Candle::new_unchecked(close, high, low, close, 0.0, ts)
104    }
105
106    #[test]
107    fn buy_signal_on_strong_down_close_with_more_buying_pressure() {
108        // Prev bar: high=10, low=8, close=9 -> buying=1, selling=1.
109        // Curr bar: high=9, low=7, close=8.5 -> close<prev.close (8.5<9),
110        // buying=1.5 > 1, selling=0.5 < 1 -> buy signal +1.
111        let mut td = TdDifferential::new();
112        assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
113        assert_eq!(td.update(c(9.0, 7.0, 8.5, 1)), Some(1.0));
114    }
115
116    #[test]
117    fn sell_signal_on_strong_up_close_with_more_selling_pressure() {
118        // Prev bar: high=10, low=8, close=9 -> buying=1, selling=1.
119        // Curr bar: high=12, low=9, close=10.5 -> close>prev.close (10.5>9),
120        // selling=1.5 > 1, buying=1.5 > 1 -> condition 3 fails -> no signal.
121        // Build a real sell case:
122        // Curr bar: high=12, low=9.5, close=10.5 ->
123        //   close>prev.close: 10.5>9 ✓
124        //   selling = 12 - 10.5 = 1.5 > prev.selling 1 ✓
125        //   buying  = 10.5 - 9.5 = 1.0 < prev.buying 1 → NO (need strict <).
126        // Curr bar: high=12, low=9.8, close=10.5 ->
127        //   buying = 0.7 < 1 ✓; selling = 1.5 > 1 ✓; close>prev ✓ -> sell.
128        let mut td = TdDifferential::new();
129        assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
130        assert_relative_eq!(td.update(c(12.0, 9.8, 10.5, 1)).unwrap(), -1.0);
131    }
132
133    #[test]
134    fn no_signal_on_neutral_bar() {
135        // Identical bars -> equality everywhere -> zero.
136        let mut td = TdDifferential::new();
137        assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
138        assert_eq!(td.update(c(10.0, 8.0, 9.0, 1)), Some(0.0));
139    }
140
141    #[test]
142    fn batch_equals_streaming() {
143        let candles: Vec<Candle> = (0..40)
144            .map(|i| {
145                let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
146                c(m + 1.0, m - 1.0, m, i64::from(i))
147            })
148            .collect();
149        let mut a = TdDifferential::new();
150        let mut b = TdDifferential::new();
151        assert_eq!(
152            a.batch(&candles),
153            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
154        );
155    }
156
157    #[test]
158    fn output_only_in_canonical_set() {
159        // Every emitted value is in {-1, 0, +1}.
160        let candles: Vec<Candle> = (0..120)
161            .map(|i| {
162                let m = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
163                c(m + 1.0, m - 1.0, m, i64::from(i))
164            })
165            .collect();
166        let mut td = TdDifferential::new();
167        for v in td.batch(&candles).into_iter().flatten() {
168            assert!(v == -1.0 || v == 0.0 || v == 1.0, "unexpected value {v}");
169        }
170    }
171
172    #[test]
173    fn reset_clears_state() {
174        let mut td = TdDifferential::new();
175        td.update(c(10.0, 8.0, 9.0, 0));
176        td.update(c(11.0, 9.0, 10.0, 1));
177        assert!(td.is_ready());
178        td.reset();
179        assert!(!td.is_ready());
180        assert_eq!(td.update(c(10.0, 8.0, 9.0, 2)), None);
181        assert_eq!(td.value(), None);
182    }
183
184    #[test]
185    fn accessors_and_metadata() {
186        let td = TdDifferential::new();
187        assert_eq!(td.warmup_period(), 2);
188        assert_eq!(td.name(), "TDDifferential");
189        assert_eq!(td.value(), None);
190    }
191}