wickra_core/indicators/
td_differential.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
24use crate::traits::Indicator;
25
26#[derive(Debug, Clone, Default)]
43pub struct TdDifferential {
44 prev: Option<Candle>,
45 last_value: Option<f64>,
46}
47
48impl TdDifferential {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub const fn value(&self) -> Option<f64> {
56 self.last_value
57 }
58}
59
60impl Indicator for TdDifferential {
61 type Input = Candle;
62 type Output = f64;
63
64 #[inline]
65 fn update(&mut self, candle: Candle) -> Option<f64> {
66 let Some(prev) = self.prev else {
67 self.prev = Some(candle);
68 return None;
69 };
70 let buying_now = candle.close - candle.low;
71 let buying_prev = prev.close - prev.low;
72 let selling_now = candle.high - candle.close;
73 let selling_prev = prev.high - prev.close;
74
75 let v = if candle.close < prev.close
76 && buying_now > buying_prev
77 && selling_now < selling_prev
78 {
79 1.0
80 } else if candle.close > prev.close
81 && selling_now > selling_prev
82 && buying_now < buying_prev
83 {
84 -1.0
85 } else {
86 0.0
87 };
88
89 self.prev = Some(candle);
90 self.last_value = Some(v);
91 Some(v)
92 }
93
94 fn reset(&mut self) {
95 self.prev = None;
96 self.last_value = None;
97 }
98
99 #[inline]
100 fn warmup_period(&self) -> usize {
101 2
102 }
103
104 #[inline]
105 fn is_ready(&self) -> bool {
106 self.last_value.is_some()
107 }
108
109 #[inline]
110 fn name(&self) -> &'static str {
111 "TDDifferential"
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::traits::BatchExt;
119 use approx::assert_relative_eq;
120
121 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
122 Candle::new_unchecked(close, high, low, close, 0.0, ts)
123 }
124
125 #[test]
126 fn buy_signal_on_strong_down_close_with_more_buying_pressure() {
127 let mut td = TdDifferential::new();
131 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
132 assert_eq!(td.update(c(9.0, 7.0, 8.5, 1)), Some(1.0));
133 }
134
135 #[test]
136 fn sell_signal_on_strong_up_close_with_more_selling_pressure() {
137 let mut td = TdDifferential::new();
148 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
149 assert_relative_eq!(td.update(c(12.0, 9.8, 10.5, 1)).unwrap(), -1.0);
150 }
151
152 #[test]
153 fn no_signal_on_neutral_bar() {
154 let mut td = TdDifferential::new();
156 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
157 assert_eq!(td.update(c(10.0, 8.0, 9.0, 1)), Some(0.0));
158 }
159
160 #[test]
161 fn batch_equals_streaming() {
162 let candles: Vec<Candle> = (0..40)
163 .map(|i| {
164 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
165 c(m + 1.0, m - 1.0, m, i64::from(i))
166 })
167 .collect();
168 let mut a = TdDifferential::new();
169 let mut b = TdDifferential::new();
170 assert_eq!(
171 a.batch(&candles),
172 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
173 );
174 }
175
176 #[test]
177 fn output_only_in_canonical_set() {
178 let candles: Vec<Candle> = (0..120)
180 .map(|i| {
181 let m = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
182 c(m + 1.0, m - 1.0, m, i64::from(i))
183 })
184 .collect();
185 let mut td = TdDifferential::new();
186 for v in td.batch(&candles).into_iter().flatten() {
187 assert!(v == -1.0 || v == 0.0 || v == 1.0, "unexpected value {v}");
188 }
189 }
190
191 #[test]
192 fn reset_clears_state() {
193 let mut td = TdDifferential::new();
194 td.update(c(10.0, 8.0, 9.0, 0));
195 td.update(c(11.0, 9.0, 10.0, 1));
196 assert!(td.is_ready());
197 td.reset();
198 assert!(!td.is_ready());
199 assert_eq!(td.update(c(10.0, 8.0, 9.0, 2)), None);
200 assert_eq!(td.value(), None);
201 }
202
203 #[test]
204 fn accessors_and_metadata() {
205 let td = TdDifferential::new();
206 assert_eq!(td.warmup_period(), 2);
207 assert_eq!(td.name(), "TDDifferential");
208 assert_eq!(td.value(), None);
209 }
210}