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/// # Example
22///
23/// ```
24/// use wickra_core::{TdCamouflage, Candle, Indicator};
25///
26/// let mut indicator = TdCamouflage::new();
27/// // `None` during warmup, then `Some(_)` once enough bars are seen.
28/// let mut out = None;
29/// for i in 0..40i64 {
30///     let p = 100.0 + (i as f64 * 0.4).sin() * 5.0;
31///     let candle = Candle::new(p, p + 1.5, p - 1.5, p + 0.3, 1_000.0, i).unwrap();
32///     out = indicator.update(candle);
33/// }
34/// let _ = out;
35/// ```
36#[derive(Debug, Clone, Default)]
37pub struct TdCamouflage {
38    prev: Option<Candle>,
39    last_value: Option<f64>,
40}
41
42impl TdCamouflage {
43    /// Construct a new `TdCamouflage`.
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Latest emitted signal if available.
50    pub const fn value(&self) -> Option<f64> {
51        self.last_value
52    }
53}
54
55impl Indicator for TdCamouflage {
56    type Input = Candle;
57    type Output = f64;
58
59    #[inline]
60    fn update(&mut self, candle: Candle) -> Option<f64> {
61        let Some(prev) = self.prev else {
62            self.prev = Some(candle);
63            self.last_value = None;
64            return None;
65        };
66        let v = if candle.close < prev.close && candle.close > candle.open && candle.low < prev.low
67        {
68            1.0
69        } else if candle.close > prev.close && candle.close < candle.open && candle.high > prev.high
70        {
71            -1.0
72        } else {
73            0.0
74        };
75        self.prev = Some(candle);
76        self.last_value = Some(v);
77        Some(v)
78    }
79
80    fn reset(&mut self) {
81        self.prev = None;
82        self.last_value = None;
83    }
84
85    #[inline]
86    fn warmup_period(&self) -> usize {
87        2
88    }
89
90    #[inline]
91    fn is_ready(&self) -> bool {
92        self.last_value.is_some()
93    }
94
95    #[inline]
96    fn name(&self) -> &'static str {
97        "TDCamouflage"
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::traits::BatchExt;
105
106    fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
107        Candle::new_unchecked(open, high, low, close, 0.0, 0)
108    }
109
110    #[test]
111    fn accessors_and_metadata() {
112        let td = TdCamouflage::new();
113        assert_eq!(td.warmup_period(), 2);
114        assert_eq!(td.name(), "TDCamouflage");
115        assert!(!td.is_ready());
116        assert_eq!(td.value(), None);
117    }
118
119    #[test]
120    fn first_bar_seeds_without_signal() {
121        let mut td = TdCamouflage::new();
122        assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), None);
123        assert!(td.update(c(10.0, 11.0, 8.0, 9.5)).is_some());
124    }
125
126    #[test]
127    fn bullish_camouflage_buy() {
128        // prev close 10. Current: close 9.5 < 10 (lower close), close 9.5 > open 9.0,
129        // low 7.0 < prev low 8.0 -> buy.
130        let mut td = TdCamouflage::new();
131        td.update(c(10.0, 11.0, 8.0, 10.0));
132        assert_eq!(td.update(c(9.0, 10.0, 7.0, 9.5)), Some(1.0));
133    }
134
135    #[test]
136    fn bearish_camouflage_sell() {
137        // prev close 10. Current: close 10.5 > 10, close 10.5 < open 11.0,
138        // high 12.0 > prev high 11.0 -> sell.
139        let mut td = TdCamouflage::new();
140        td.update(c(10.0, 11.0, 8.0, 10.0));
141        assert_eq!(td.update(c(11.0, 12.0, 10.0, 10.5)), Some(-1.0));
142    }
143
144    #[test]
145    fn no_pattern_is_zero() {
146        let mut td = TdCamouflage::new();
147        td.update(c(10.0, 11.0, 9.0, 10.0));
148        assert_eq!(td.update(c(10.0, 11.5, 9.5, 11.0)), Some(0.0));
149    }
150
151    #[test]
152    fn reset_clears_state() {
153        let mut td = TdCamouflage::new();
154        td.update(c(10.0, 11.0, 9.0, 10.0));
155        td.update(c(9.0, 10.0, 7.0, 9.5));
156        assert!(td.is_ready());
157        td.reset();
158        assert!(!td.is_ready());
159        assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), None);
160    }
161
162    #[test]
163    fn batch_equals_streaming() {
164        let candles: Vec<Candle> = (0..40)
165            .map(|i| {
166                let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
167                c(b, b + 1.0, b - 1.0, b + 0.2)
168            })
169            .collect();
170        let batch = TdCamouflage::new().batch(&candles);
171        let mut b = TdCamouflage::new();
172        let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
173        assert_eq!(batch, streamed);
174    }
175}