Skip to main content

wickra_core/indicators/
td_camouflage.rs

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