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)]
28pub struct TdDifferential {
29 prev: Option<Candle>,
30 last_value: Option<f64>,
31}
32
33impl TdDifferential {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub const fn value(&self) -> Option<f64> {
41 self.last_value
42 }
43}
44
45impl Indicator for TdDifferential {
46 type Input = Candle;
47 type Output = f64;
48
49 fn update(&mut self, candle: Candle) -> Option<f64> {
50 let Some(prev) = self.prev else {
51 self.prev = Some(candle);
52 return None;
53 };
54 let buying_now = candle.close - candle.low;
55 let buying_prev = prev.close - prev.low;
56 let selling_now = candle.high - candle.close;
57 let selling_prev = prev.high - prev.close;
58
59 let v = if candle.close < prev.close
60 && buying_now > buying_prev
61 && selling_now < selling_prev
62 {
63 1.0
64 } else if candle.close > prev.close
65 && selling_now > selling_prev
66 && buying_now < buying_prev
67 {
68 -1.0
69 } else {
70 0.0
71 };
72
73 self.prev = Some(candle);
74 self.last_value = Some(v);
75 Some(v)
76 }
77
78 fn reset(&mut self) {
79 self.prev = None;
80 self.last_value = None;
81 }
82
83 fn warmup_period(&self) -> usize {
84 2
85 }
86
87 fn is_ready(&self) -> bool {
88 self.last_value.is_some()
89 }
90
91 fn name(&self) -> &'static str {
92 "TDDifferential"
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::traits::BatchExt;
100 use approx::assert_relative_eq;
101
102 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
103 Candle::new_unchecked(close, high, low, close, 0.0, ts)
104 }
105
106 #[test]
107 fn buy_signal_on_strong_down_close_with_more_buying_pressure() {
108 let mut td = TdDifferential::new();
112 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
113 assert_eq!(td.update(c(9.0, 7.0, 8.5, 1)), Some(1.0));
114 }
115
116 #[test]
117 fn sell_signal_on_strong_up_close_with_more_selling_pressure() {
118 let mut td = TdDifferential::new();
129 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
130 assert_relative_eq!(td.update(c(12.0, 9.8, 10.5, 1)).unwrap(), -1.0);
131 }
132
133 #[test]
134 fn no_signal_on_neutral_bar() {
135 let mut td = TdDifferential::new();
137 assert_eq!(td.update(c(10.0, 8.0, 9.0, 0)), None);
138 assert_eq!(td.update(c(10.0, 8.0, 9.0, 1)), Some(0.0));
139 }
140
141 #[test]
142 fn batch_equals_streaming() {
143 let candles: Vec<Candle> = (0..40)
144 .map(|i| {
145 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
146 c(m + 1.0, m - 1.0, m, i64::from(i))
147 })
148 .collect();
149 let mut a = TdDifferential::new();
150 let mut b = TdDifferential::new();
151 assert_eq!(
152 a.batch(&candles),
153 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
154 );
155 }
156
157 #[test]
158 fn output_only_in_canonical_set() {
159 let candles: Vec<Candle> = (0..120)
161 .map(|i| {
162 let m = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
163 c(m + 1.0, m - 1.0, m, i64::from(i))
164 })
165 .collect();
166 let mut td = TdDifferential::new();
167 for v in td.batch(&candles).into_iter().flatten() {
168 assert!(v == -1.0 || v == 0.0 || v == 1.0, "unexpected value {v}");
169 }
170 }
171
172 #[test]
173 fn reset_clears_state() {
174 let mut td = TdDifferential::new();
175 td.update(c(10.0, 8.0, 9.0, 0));
176 td.update(c(11.0, 9.0, 10.0, 1));
177 assert!(td.is_ready());
178 td.reset();
179 assert!(!td.is_ready());
180 assert_eq!(td.update(c(10.0, 8.0, 9.0, 2)), None);
181 assert_eq!(td.value(), None);
182 }
183
184 #[test]
185 fn accessors_and_metadata() {
186 let td = TdDifferential::new();
187 assert_eq!(td.warmup_period(), 2);
188 assert_eq!(td.name(), "TDDifferential");
189 assert_eq!(td.value(), None);
190 }
191}