1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
25
26use crate::error::{Error, Result};
27use crate::ohlcv::Candle;
28use crate::traits::Indicator;
29
30#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct TdLinesOutput {
38 pub resistance: f64,
40 pub support: f64,
42}
43
44#[derive(Debug, Clone)]
61pub struct TdLines {
62 lookback: usize,
63 target: usize,
64 closes: VecDeque<f64>,
65 buy_count: usize,
66 sell_count: usize,
67 buy_run_max_high: f64,
70 sell_run_min_low: f64,
72 resistance: f64,
73 support: f64,
74 ready: bool,
75}
76
77impl TdLines {
78 pub fn new(lookback: usize, target: usize) -> Result<Self> {
85 if lookback == 0 || target == 0 {
86 return Err(Error::PeriodZero);
87 }
88 Ok(Self {
89 lookback,
90 target,
91 closes: VecDeque::with_capacity(lookback + 1),
92 buy_count: 0,
93 sell_count: 0,
94 buy_run_max_high: f64::NEG_INFINITY,
95 sell_run_min_low: f64::INFINITY,
96 resistance: f64::NAN,
97 support: f64::NAN,
98 ready: false,
99 })
100 }
101
102 pub fn classic() -> Self {
104 Self::new(4, 9).expect("classic TD Lines parameters are valid")
105 }
106
107 pub const fn params(&self) -> (usize, usize) {
109 (self.lookback, self.target)
110 }
111}
112
113impl Indicator for TdLines {
114 type Input = Candle;
115 type Output = TdLinesOutput;
116
117 fn update(&mut self, candle: Candle) -> Option<TdLinesOutput> {
118 if self.closes.len() > self.lookback {
119 self.closes.pop_front();
120 }
121 if self.closes.len() < self.lookback {
122 self.closes.push_back(candle.close);
123 return None;
124 }
125 let reference = *self.closes.front().expect("non-empty after the guard");
126 self.closes.push_back(candle.close);
127
128 if candle.close < reference {
129 if self.buy_count == 0 {
132 self.buy_run_max_high = candle.high;
133 } else {
134 self.buy_run_max_high = self.buy_run_max_high.max(candle.high);
135 }
136 self.buy_count = (self.buy_count + 1).min(self.target);
137 self.sell_count = 0;
138 self.sell_run_min_low = f64::INFINITY;
139 if self.buy_count == self.target {
140 self.resistance = self.buy_run_max_high;
141 }
142 } else if candle.close > reference {
143 if self.sell_count == 0 {
144 self.sell_run_min_low = candle.low;
145 } else {
146 self.sell_run_min_low = self.sell_run_min_low.min(candle.low);
147 }
148 self.sell_count = (self.sell_count + 1).min(self.target);
149 self.buy_count = 0;
150 self.buy_run_max_high = f64::NEG_INFINITY;
151 if self.sell_count == self.target {
152 self.support = self.sell_run_min_low;
153 }
154 } else {
155 self.buy_count = 0;
157 self.sell_count = 0;
158 self.buy_run_max_high = f64::NEG_INFINITY;
159 self.sell_run_min_low = f64::INFINITY;
160 }
161
162 self.ready = true;
163 Some(TdLinesOutput {
164 resistance: self.resistance,
165 support: self.support,
166 })
167 }
168
169 fn reset(&mut self) {
170 self.closes.clear();
171 self.buy_count = 0;
172 self.sell_count = 0;
173 self.buy_run_max_high = f64::NEG_INFINITY;
174 self.sell_run_min_low = f64::INFINITY;
175 self.resistance = f64::NAN;
176 self.support = f64::NAN;
177 self.ready = false;
178 }
179
180 fn warmup_period(&self) -> usize {
181 self.lookback + 1
182 }
183
184 fn is_ready(&self) -> bool {
185 self.ready
186 }
187
188 fn name(&self) -> &'static str {
189 "TDLines"
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::traits::BatchExt;
197 use approx::assert_relative_eq;
198
199 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
200 Candle::new_unchecked(close, high, low, close, 0.0, ts)
201 }
202
203 #[test]
204 fn uptrend_completes_sell_setup_and_sets_support() {
205 let candles: Vec<Candle> = (1..=20)
209 .map(|i| {
210 c(
211 f64::from(i) + 0.5,
212 f64::from(i) - 0.5,
213 f64::from(i),
214 i64::from(i),
215 )
216 })
217 .collect();
218 let mut lines = TdLines::classic();
219 let out = lines.batch(&candles);
220 let early = out[5].expect("ready");
223 assert!(early.support.is_nan());
224 assert!(early.resistance.is_nan());
225 let after = out[12].expect("ready");
227 assert!(after.resistance.is_nan());
228 assert_relative_eq!(after.support, 4.5, epsilon = 1e-12);
229 let final_out = out[19].expect("ready");
232 assert_relative_eq!(final_out.support, 4.5, epsilon = 1e-12);
233 }
234
235 #[test]
236 fn downtrend_completes_buy_setup_and_sets_resistance() {
237 let candles: Vec<Candle> = (1..=20)
238 .rev()
239 .enumerate()
240 .map(|(i, v)| {
241 c(
242 f64::from(v) + 0.5,
243 f64::from(v) - 0.5,
244 f64::from(v),
245 i64::try_from(i).unwrap(),
246 )
247 })
248 .collect();
249 let mut lines = TdLines::classic();
250 let out = lines.batch(&candles);
251 let after = out[12].expect("ready");
255 assert!(after.support.is_nan());
256 assert_relative_eq!(after.resistance, 16.5, epsilon = 1e-12);
258 }
259
260 #[test]
261 fn flat_series_never_sets_levels() {
262 let candles: Vec<Candle> = (0..30).map(|i| c(10.5, 9.5, 10.0, i64::from(i))).collect();
264 let mut lines = TdLines::classic();
265 for v in lines.batch(&candles).into_iter().flatten() {
266 assert!(v.support.is_nan());
267 assert!(v.resistance.is_nan());
268 }
269 }
270
271 #[test]
272 fn batch_equals_streaming() {
273 let candles: Vec<Candle> = (0..80)
274 .map(|i| {
275 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
276 c(m + 1.0, m - 1.0, m, i64::from(i))
277 })
278 .collect();
279 let mut a = TdLines::classic();
280 let mut b = TdLines::classic();
281 let av = a.batch(&candles);
282 let bv: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
283 assert_eq!(av.len(), bv.len());
284 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
285 assert_eq!(x.is_some(), y.is_some(), "row {i} option mismatch");
286 if let (Some(a), Some(b)) = (x, y) {
287 assert_eq!(
288 a.support.is_nan(),
289 b.support.is_nan(),
290 "row {i} support nan flag"
291 );
292 assert_eq!(
293 a.resistance.is_nan(),
294 b.resistance.is_nan(),
295 "row {i} resistance nan flag"
296 );
297 if !a.support.is_nan() {
298 assert_relative_eq!(a.support, b.support, epsilon = 1e-12);
299 }
300 if !a.resistance.is_nan() {
301 assert_relative_eq!(a.resistance, b.resistance, epsilon = 1e-12);
302 }
303 }
304 }
305 }
306
307 #[test]
308 fn rejects_invalid_params() {
309 assert!(matches!(TdLines::new(0, 9), Err(Error::PeriodZero)));
310 assert!(matches!(TdLines::new(4, 0), Err(Error::PeriodZero)));
311 }
312
313 #[test]
314 fn reset_clears_state() {
315 let candles: Vec<Candle> = (1..=20)
316 .map(|i| {
317 c(
318 f64::from(i) + 0.5,
319 f64::from(i) - 0.5,
320 f64::from(i),
321 i64::from(i),
322 )
323 })
324 .collect();
325 let mut lines = TdLines::classic();
326 lines.batch(&candles);
327 assert!(lines.is_ready());
328 lines.reset();
329 assert!(!lines.is_ready());
330 assert_eq!(lines.update(candles[0]), None);
331 }
332
333 #[test]
334 fn accessors_and_metadata() {
335 let lines = TdLines::classic();
336 assert_eq!(lines.params(), (4, 9));
337 assert_eq!(lines.warmup_period(), 5);
338 assert_eq!(lines.name(), "TDLines");
339 }
340}