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 fn update(&mut self, candle: Candle) -> Option<f64> {
65 let Some(prev) = self.prev else {
66 self.prev = Some(candle);
67 return None;
68 };
69 let buying_now = candle.close - candle.low;
70 let buying_prev = prev.close - prev.low;
71 let selling_now = candle.high - candle.close;
72 let selling_prev = prev.high - prev.close;
73
74 let v = if candle.close < prev.close
75 && buying_now > buying_prev
76 && selling_now < selling_prev
77 {
78 1.0
79 } else if candle.close > prev.close
80 && selling_now > selling_prev
81 && buying_now < buying_prev
82 {
83 -1.0
84 } else {
85 0.0
86 };
87
88 self.prev = Some(candle);
89 self.last_value = Some(v);
90 Some(v)
91 }
92
93 fn reset(&mut self) {
94 self.prev = None;
95 self.last_value = None;
96 }
97
98 fn warmup_period(&self) -> usize {
99 2
100 }
101
102 fn is_ready(&self) -> bool {
103 self.last_value.is_some()
104 }
105
106 fn name(&self) -> &'static str {
107 "TDDifferential"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::traits::BatchExt;
115 use approx::assert_relative_eq;
116
117 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
118 Candle::new_unchecked(close, high, low, close, 0.0, ts)
119 }
120
121 #[test]
122 fn buy_signal_on_strong_down_close_with_more_buying_pressure() {
123 let mut td = TdDifferential::new();
127 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
128 assert_eq!(td.update(c(9.0, 7.0, 8.5, 1)), Some(1.0));
129 }
130
131 #[test]
132 fn sell_signal_on_strong_up_close_with_more_selling_pressure() {
133 let mut td = TdDifferential::new();
144 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
145 assert_relative_eq!(td.update(c(12.0, 9.8, 10.5, 1)).unwrap(), -1.0);
146 }
147
148 #[test]
149 fn no_signal_on_neutral_bar() {
150 let mut td = TdDifferential::new();
152 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
153 assert_eq!(td.update(c(10.0, 8.0, 9.0, 1)), Some(0.0));
154 }
155
156 #[test]
157 fn batch_equals_streaming() {
158 let candles: Vec<Candle> = (0..40)
159 .map(|i| {
160 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
161 c(m + 1.0, m - 1.0, m, i64::from(i))
162 })
163 .collect();
164 let mut a = TdDifferential::new();
165 let mut b = TdDifferential::new();
166 assert_eq!(
167 a.batch(&candles),
168 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
169 );
170 }
171
172 #[test]
173 fn output_only_in_canonical_set() {
174 let candles: Vec<Candle> = (0..120)
176 .map(|i| {
177 let m = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
178 c(m + 1.0, m - 1.0, m, i64::from(i))
179 })
180 .collect();
181 let mut td = TdDifferential::new();
182 for v in td.batch(&candles).into_iter().flatten() {
183 assert!(v == -1.0 || v == 0.0 || v == 1.0, "unexpected value {v}");
184 }
185 }
186
187 #[test]
188 fn reset_clears_state() {
189 let mut td = TdDifferential::new();
190 td.update(c(10.0, 8.0, 9.0, 0));
191 td.update(c(11.0, 9.0, 10.0, 1));
192 assert!(td.is_ready());
193 td.reset();
194 assert!(!td.is_ready());
195 assert_eq!(td.update(c(10.0, 8.0, 9.0, 2)), None);
196 assert_eq!(td.value(), None);
197 }
198
199 #[test]
200 fn accessors_and_metadata() {
201 let td = TdDifferential::new();
202 assert_eq!(td.warmup_period(), 2);
203 assert_eq!(td.name(), "TDDifferential");
204 assert_eq!(td.value(), None);
205 }
206}