1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
26use crate::traits::Indicator;
27
28#[derive(Debug, Clone, Default)]
45pub struct TdDifferential {
46 prev2: Option<Candle>,
47 prev: Option<Candle>,
48 last_value: Option<f64>,
49}
50
51impl TdDifferential {
52 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub const fn value(&self) -> Option<f64> {
59 self.last_value
60 }
61}
62
63fn pressures(bar: &Candle, prev_close: f64) -> (f64, f64) {
65 let true_low = bar.low.min(prev_close);
66 let true_high = bar.high.max(prev_close);
67 (bar.close - true_low, true_high - bar.close)
68}
69
70impl Indicator for TdDifferential {
71 type Input = Candle;
72 type Output = f64;
73
74 #[inline]
75 fn update(&mut self, candle: Candle) -> Option<f64> {
76 let (Some(prev2), Some(prev)) = (self.prev2, self.prev) else {
77 self.prev2 = self.prev;
78 self.prev = Some(candle);
79 return None;
80 };
81 let (buying_now, selling_now) = pressures(&candle, prev.close);
82 let (buying_prev, selling_prev) = pressures(&prev, prev2.close);
83
84 let v = if candle.close < prev.close
85 && prev.close < prev2.close
86 && buying_now > buying_prev
87 && selling_now < selling_prev
88 {
89 1.0
90 } else if candle.close > prev.close
91 && prev.close > prev2.close
92 && selling_now > selling_prev
93 && buying_now < buying_prev
94 {
95 -1.0
96 } else {
97 0.0
98 };
99
100 self.prev2 = Some(prev);
101 self.prev = Some(candle);
102 self.last_value = Some(v);
103 Some(v)
104 }
105
106 fn reset(&mut self) {
107 self.prev2 = None;
108 self.prev = None;
109 self.last_value = None;
110 }
111
112 #[inline]
113 fn warmup_period(&self) -> usize {
114 3
115 }
116
117 #[inline]
118 fn is_ready(&self) -> bool {
119 self.last_value.is_some()
120 }
121
122 #[inline]
123 fn name(&self) -> &'static str {
124 "TDDifferential"
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::traits::BatchExt;
132 use approx::assert_relative_eq;
133
134 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
135 Candle::new_unchecked(close, high, low, close, 0.0, ts)
136 }
137
138 #[test]
139 fn buy_signal_after_two_lower_closes_with_shifting_pressure() {
140 let mut td = TdDifferential::new();
144 assert_eq!(td.update(c(11.0, 9.0, 10.0, 0)), None);
145 assert_eq!(td.update(c(10.0, 8.0, 9.0, 1)), None);
146 assert_eq!(td.update(c(9.0, 7.0, 8.5, 2)), Some(1.0));
147 }
148
149 #[test]
150 fn single_lower_close_is_not_enough() {
151 let mut td = TdDifferential::new();
153 td.update(c(9.0, 7.0, 8.0, 0));
154 td.update(c(10.0, 8.0, 9.0, 1));
155 assert_eq!(td.update(c(9.0, 7.0, 8.5, 2)), Some(0.0));
156 }
157
158 #[test]
159 fn sell_signal_after_two_higher_closes_with_shifting_pressure() {
160 let mut td = TdDifferential::new();
164 assert_eq!(td.update(c(9.0, 7.0, 8.0, 0)), None);
165 assert_eq!(td.update(c(10.0, 8.0, 9.0, 1)), None);
166 assert_relative_eq!(td.update(c(11.5, 9.5, 9.8, 2)).unwrap(), -1.0);
167 }
168
169 #[test]
170 fn no_signal_on_neutral_bars() {
171 let mut td = TdDifferential::new();
173 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
174 assert_eq!(td.update(c(10.0, 8.0, 9.0, 1)), None);
175 assert_eq!(td.update(c(10.0, 8.0, 9.0, 2)), Some(0.0));
176 }
177
178 #[test]
179 fn batch_equals_streaming() {
180 let candles: Vec<Candle> = (0..40)
181 .map(|i| {
182 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
183 c(m + 1.0, m - 1.0, m, i64::from(i))
184 })
185 .collect();
186 let mut a = TdDifferential::new();
187 let mut b = TdDifferential::new();
188 assert_eq!(
189 a.batch(&candles),
190 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
191 );
192 }
193
194 #[test]
195 fn output_only_in_canonical_set() {
196 let candles: Vec<Candle> = (0..120)
198 .map(|i| {
199 let m = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
200 c(m + 1.0, m - 1.0, m, i64::from(i))
201 })
202 .collect();
203 let mut td = TdDifferential::new();
204 for v in td.batch(&candles).into_iter().flatten() {
205 assert!(v == -1.0 || v == 0.0 || v == 1.0, "unexpected value {v}");
206 }
207 }
208
209 #[test]
210 fn reset_clears_state() {
211 let mut td = TdDifferential::new();
212 td.update(c(10.0, 8.0, 9.0, 0));
213 td.update(c(11.0, 9.0, 10.0, 1));
214 td.update(c(12.0, 10.0, 11.0, 2));
215 assert!(td.is_ready());
216 td.reset();
217 assert!(!td.is_ready());
218 assert_eq!(td.update(c(10.0, 8.0, 9.0, 3)), None);
219 assert_eq!(td.value(), None);
220 }
221
222 #[test]
223 fn accessors_and_metadata() {
224 let td = TdDifferential::new();
225 assert_eq!(td.warmup_period(), 3);
226 assert_eq!(td.name(), "TDDifferential");
227 assert_eq!(td.value(), None);
228 }
229
230 #[test]
231 fn buy_signal_needs_true_high_on_gap_down() {
232 let mut td = TdDifferential::new();
239 assert_eq!(td.update(c(11.0, 9.0, 10.0, 0)), None);
240 assert_eq!(td.update(c(9.2, 8.8, 9.0, 1)), None);
241 assert_eq!(td.update(c(9.0, 8.0, 8.5, 2)), Some(1.0));
242 assert_eq!(td.value(), Some(1.0));
243 }
244
245 #[test]
246 fn sell_signal_needs_true_low_on_gap_up() {
247 let mut td = TdDifferential::new();
253 assert_eq!(td.update(c(11.0, 9.0, 10.0, 0)), None);
254 assert_eq!(td.update(c(11.2, 10.8, 11.0, 1)), None);
255 assert_eq!(td.update(c(12.0, 11.0, 11.5, 2)), Some(-1.0));
256 }
257
258 #[test]
259 fn two_lower_closes_without_pressure_shift_is_neutral() {
260 let mut td = TdDifferential::new();
264 td.update(c(11.0, 9.0, 10.0, 0));
265 td.update(c(10.0, 8.0, 9.0, 1));
266 assert_eq!(td.update(c(9.0, 7.0, 8.0, 2)), Some(0.0));
267 }
268
269 #[test]
270 fn two_higher_closes_without_pressure_shift_is_neutral() {
271 let mut td = TdDifferential::new();
273 td.update(c(9.0, 7.0, 8.0, 0));
274 td.update(c(10.0, 8.0, 9.0, 1));
275 assert_eq!(td.update(c(11.0, 9.0, 10.0, 2)), Some(0.0));
276 }
277
278 #[test]
279 fn pressures_use_true_range() {
280 let (buying, selling) = pressures(&c(11.0, 9.0, 10.0, 0), 12.0);
282 assert_eq!((buying, selling), (1.0, 2.0));
283 let (buying, selling) = pressures(&c(11.0, 9.0, 10.0, 0), 7.0);
285 assert_eq!((buying, selling), (3.0, 1.0));
286 }
287
288 #[test]
289 fn first_value_lands_at_warmup_minus_one() {
290 let candles: Vec<Candle> = (0..6)
291 .map(|i| c(11.0, 9.0, 10.0 + f64::from(i), i64::from(i)))
292 .collect();
293 let mut td = TdDifferential::new();
294 let warm = td.warmup_period();
295 let out = td.batch(&candles);
296 assert!(out[..warm - 1].iter().all(Option::is_none));
297 assert!(out[warm - 1..].iter().all(Option::is_some));
298 }
299
300 #[test]
301 fn reset_reproduces_fresh_run() {
302 let candles: Vec<Candle> = (0..60)
303 .map(|i| {
304 let m = 100.0 + (f64::from(i) * 0.7).sin() * 5.0;
305 c(
306 m + 1.0 + (f64::from(i) * 0.3).cos(),
307 m - 1.0,
308 m,
309 i64::from(i),
310 )
311 })
312 .collect();
313 let mut fresh = TdDifferential::new();
314 let expected = fresh.batch(&candles);
315 let mut td = TdDifferential::new();
316 td.batch(&candles[..17]);
317 td.reset();
318 assert_eq!(td.batch(&candles), expected);
319 }
320
321 #[test]
322 fn batch_nan_into_matches_streaming() {
323 let candles: Vec<Candle> = (0..60)
324 .map(|i| {
325 let m = 100.0 + (f64::from(i) * 0.7).sin() * 5.0;
326 c(
327 m + 1.0 + (f64::from(i) * 0.3).cos(),
328 m - 1.0,
329 m,
330 i64::from(i),
331 )
332 })
333 .collect();
334 let mut a = TdDifferential::new();
335 let mut out = vec![0.0; candles.len()];
336 a.batch_nan_into(&candles, &mut out);
337 let mut b = TdDifferential::new();
338 let streamed: Vec<f64> = candles
339 .iter()
340 .map(|x| b.update(*x).unwrap_or(f64::NAN))
341 .collect();
342 assert!(out
343 .iter()
344 .zip(&streamed)
345 .all(|(x, y)| x.to_bits() == y.to_bits()));
346 }
347}