Skip to main content

wickra_core/indicators/
td_propulsion.rs

1#![allow(clippy::doc_markdown)]
2
3//! Tom DeMark TD Propulsion — a 2-bar trend-continuation thrust signal.
4//!
5//! TD Propulsion qualifies a continuation thrust: the bar opens on the trend side
6//! of the prior close and then closes beyond the prior bar's extreme, "propelling"
7//! the move forward.
8//!
9//! - **Propulsion up** (`+1.0`): `open >= close[-1]` (opens at or above the prior
10//!   close) AND `close > high[-1]` (closes above the prior high).
11//! - **Propulsion down** (`-1.0`): `open <= close[-1]` AND `close < low[-1]`.
12//! - Otherwise the output is `0.0`.
13//!
14//! The one-bar lookback means the first value lands on the second candle.
15
16use crate::ohlcv::Candle;
17use crate::traits::Indicator;
18
19/// TD Propulsion — 2-bar trend-continuation thrust detector.
20#[derive(Debug, Clone, Default)]
21pub struct TdPropulsion {
22    prev: Option<Candle>,
23    last_value: Option<f64>,
24}
25
26impl TdPropulsion {
27    /// Construct a new `TdPropulsion`.
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Latest emitted signal if available.
34    pub const fn value(&self) -> Option<f64> {
35        self.last_value
36    }
37}
38
39impl Indicator for TdPropulsion {
40    type Input = Candle;
41    type Output = f64;
42
43    fn update(&mut self, candle: Candle) -> Option<f64> {
44        let Some(prev) = self.prev else {
45            self.prev = Some(candle);
46            self.last_value = Some(0.0);
47            return Some(0.0);
48        };
49        let v = if candle.open >= prev.close && candle.close > prev.high {
50            1.0
51        } else if candle.open <= prev.close && candle.close < prev.low {
52            -1.0
53        } else {
54            0.0
55        };
56        self.prev = Some(candle);
57        self.last_value = Some(v);
58        Some(v)
59    }
60
61    fn reset(&mut self) {
62        self.prev = None;
63        self.last_value = None;
64    }
65
66    fn warmup_period(&self) -> usize {
67        2
68    }
69
70    fn is_ready(&self) -> bool {
71        self.last_value.is_some()
72    }
73
74    fn name(&self) -> &'static str {
75        "TDPropulsion"
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::traits::BatchExt;
83
84    fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
85        Candle::new_unchecked(open, high, low, close, 0.0, 0)
86    }
87
88    #[test]
89    fn accessors_and_metadata() {
90        let td = TdPropulsion::new();
91        assert_eq!(td.warmup_period(), 2);
92        assert_eq!(td.name(), "TDPropulsion");
93        assert!(!td.is_ready());
94        assert_eq!(td.value(), None);
95    }
96
97    #[test]
98    fn first_bar_seeds_without_signal() {
99        let mut td = TdPropulsion::new();
100        assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
101        assert!(td.update(c(10.5, 12.0, 10.0, 11.5)).is_some());
102    }
103
104    #[test]
105    fn propulsion_up() {
106        // prev close 10, high 11. Current open 10.5 >= 10, close 11.5 > 11 -> +1.
107        let mut td = TdPropulsion::new();
108        td.update(c(9.5, 11.0, 9.0, 10.0));
109        assert_eq!(td.update(c(10.5, 12.0, 10.0, 11.5)), Some(1.0));
110    }
111
112    #[test]
113    fn propulsion_down() {
114        // prev close 10, low 9. Current open 9.5 <= 10, close 8.5 < 9 -> -1.
115        let mut td = TdPropulsion::new();
116        td.update(c(10.5, 11.0, 9.0, 10.0));
117        assert_eq!(td.update(c(9.5, 10.0, 8.0, 8.5)), Some(-1.0));
118    }
119
120    #[test]
121    fn no_thrust_is_zero() {
122        let mut td = TdPropulsion::new();
123        td.update(c(9.5, 11.0, 9.0, 10.0));
124        // close 10.5 not above prior high 11 -> 0.
125        assert_eq!(td.update(c(10.5, 10.8, 10.0, 10.5)), Some(0.0));
126    }
127
128    #[test]
129    fn reset_clears_state() {
130        let mut td = TdPropulsion::new();
131        td.update(c(9.5, 11.0, 9.0, 10.0));
132        td.update(c(10.5, 12.0, 10.0, 11.5));
133        assert!(td.is_ready());
134        td.reset();
135        assert!(!td.is_ready());
136        assert_eq!(td.update(c(9.5, 11.0, 9.0, 10.0)), Some(0.0));
137    }
138
139    #[test]
140    fn batch_equals_streaming() {
141        let candles: Vec<Candle> = (0..40)
142            .map(|i| {
143                let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
144                c(b, b + 1.0, b - 1.0, b + 0.3)
145            })
146            .collect();
147        let batch = TdPropulsion::new().batch(&candles);
148        let mut b = TdPropulsion::new();
149        let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
150        assert_eq!(batch, streamed);
151    }
152}