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)]
46pub struct TdLines {
47 lookback: usize,
48 target: usize,
49 closes: VecDeque<f64>,
50 buy_count: usize,
51 sell_count: usize,
52 buy_run_max_high: f64,
55 sell_run_min_low: f64,
57 resistance: f64,
58 support: f64,
59 ready: bool,
60}
61
62impl TdLines {
63 pub fn new(lookback: usize, target: usize) -> Result<Self> {
70 if lookback == 0 || target == 0 {
71 return Err(Error::PeriodZero);
72 }
73 Ok(Self {
74 lookback,
75 target,
76 closes: VecDeque::with_capacity(lookback + 1),
77 buy_count: 0,
78 sell_count: 0,
79 buy_run_max_high: f64::NEG_INFINITY,
80 sell_run_min_low: f64::INFINITY,
81 resistance: f64::NAN,
82 support: f64::NAN,
83 ready: false,
84 })
85 }
86
87 pub fn classic() -> Self {
89 Self::new(4, 9).expect("classic TD Lines parameters are valid")
90 }
91
92 pub const fn params(&self) -> (usize, usize) {
94 (self.lookback, self.target)
95 }
96}
97
98impl Indicator for TdLines {
99 type Input = Candle;
100 type Output = TdLinesOutput;
101
102 fn update(&mut self, candle: Candle) -> Option<TdLinesOutput> {
103 if self.closes.len() > self.lookback {
104 self.closes.pop_front();
105 }
106 if self.closes.len() < self.lookback {
107 self.closes.push_back(candle.close);
108 return None;
109 }
110 let reference = *self.closes.front().expect("non-empty after the guard");
111 self.closes.push_back(candle.close);
112
113 if candle.close < reference {
114 if self.buy_count == 0 {
117 self.buy_run_max_high = candle.high;
118 } else {
119 self.buy_run_max_high = self.buy_run_max_high.max(candle.high);
120 }
121 self.buy_count = (self.buy_count + 1).min(self.target);
122 self.sell_count = 0;
123 self.sell_run_min_low = f64::INFINITY;
124 if self.buy_count == self.target {
125 self.resistance = self.buy_run_max_high;
126 }
127 } else if candle.close > reference {
128 if self.sell_count == 0 {
129 self.sell_run_min_low = candle.low;
130 } else {
131 self.sell_run_min_low = self.sell_run_min_low.min(candle.low);
132 }
133 self.sell_count = (self.sell_count + 1).min(self.target);
134 self.buy_count = 0;
135 self.buy_run_max_high = f64::NEG_INFINITY;
136 if self.sell_count == self.target {
137 self.support = self.sell_run_min_low;
138 }
139 } else {
140 self.buy_count = 0;
142 self.sell_count = 0;
143 self.buy_run_max_high = f64::NEG_INFINITY;
144 self.sell_run_min_low = f64::INFINITY;
145 }
146
147 self.ready = true;
148 Some(TdLinesOutput {
149 resistance: self.resistance,
150 support: self.support,
151 })
152 }
153
154 fn reset(&mut self) {
155 self.closes.clear();
156 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 self.resistance = f64::NAN;
161 self.support = f64::NAN;
162 self.ready = false;
163 }
164
165 fn warmup_period(&self) -> usize {
166 self.lookback + 1
167 }
168
169 fn is_ready(&self) -> bool {
170 self.ready
171 }
172
173 fn name(&self) -> &'static str {
174 "TDLines"
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::traits::BatchExt;
182 use approx::assert_relative_eq;
183
184 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
185 Candle::new_unchecked(close, high, low, close, 0.0, ts)
186 }
187
188 #[test]
189 fn uptrend_completes_sell_setup_and_sets_support() {
190 let candles: Vec<Candle> = (1..=20)
194 .map(|i| {
195 c(
196 f64::from(i) + 0.5,
197 f64::from(i) - 0.5,
198 f64::from(i),
199 i64::from(i),
200 )
201 })
202 .collect();
203 let mut lines = TdLines::classic();
204 let out = lines.batch(&candles);
205 let early = out[5].expect("ready");
208 assert!(early.support.is_nan());
209 assert!(early.resistance.is_nan());
210 let after = out[12].expect("ready");
212 assert!(after.resistance.is_nan());
213 assert_relative_eq!(after.support, 4.5, epsilon = 1e-12);
214 let final_out = out[19].expect("ready");
217 assert_relative_eq!(final_out.support, 4.5, epsilon = 1e-12);
218 }
219
220 #[test]
221 fn downtrend_completes_buy_setup_and_sets_resistance() {
222 let candles: Vec<Candle> = (1..=20)
223 .rev()
224 .enumerate()
225 .map(|(i, v)| {
226 c(
227 f64::from(v) + 0.5,
228 f64::from(v) - 0.5,
229 f64::from(v),
230 i64::try_from(i).unwrap(),
231 )
232 })
233 .collect();
234 let mut lines = TdLines::classic();
235 let out = lines.batch(&candles);
236 let after = out[12].expect("ready");
240 assert!(after.support.is_nan());
241 assert_relative_eq!(after.resistance, 16.5, epsilon = 1e-12);
243 }
244
245 #[test]
246 fn flat_series_never_sets_levels() {
247 let candles: Vec<Candle> = (0..30).map(|i| c(10.5, 9.5, 10.0, i64::from(i))).collect();
249 let mut lines = TdLines::classic();
250 for v in lines.batch(&candles).into_iter().flatten() {
251 assert!(v.support.is_nan());
252 assert!(v.resistance.is_nan());
253 }
254 }
255
256 #[test]
257 fn batch_equals_streaming() {
258 let candles: Vec<Candle> = (0..80)
259 .map(|i| {
260 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
261 c(m + 1.0, m - 1.0, m, i64::from(i))
262 })
263 .collect();
264 let mut a = TdLines::classic();
265 let mut b = TdLines::classic();
266 let av = a.batch(&candles);
267 let bv: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
268 assert_eq!(av.len(), bv.len());
269 for (i, (x, y)) in av.iter().zip(bv.iter()).enumerate() {
270 assert_eq!(x.is_some(), y.is_some(), "row {i} option mismatch");
271 if let (Some(a), Some(b)) = (x, y) {
272 assert_eq!(
273 a.support.is_nan(),
274 b.support.is_nan(),
275 "row {i} support nan flag"
276 );
277 assert_eq!(
278 a.resistance.is_nan(),
279 b.resistance.is_nan(),
280 "row {i} resistance nan flag"
281 );
282 if !a.support.is_nan() {
283 assert_relative_eq!(a.support, b.support, epsilon = 1e-12);
284 }
285 if !a.resistance.is_nan() {
286 assert_relative_eq!(a.resistance, b.resistance, epsilon = 1e-12);
287 }
288 }
289 }
290 }
291
292 #[test]
293 fn rejects_invalid_params() {
294 assert!(matches!(TdLines::new(0, 9), Err(Error::PeriodZero)));
295 assert!(matches!(TdLines::new(4, 0), Err(Error::PeriodZero)));
296 }
297
298 #[test]
299 fn reset_clears_state() {
300 let candles: Vec<Candle> = (1..=20)
301 .map(|i| {
302 c(
303 f64::from(i) + 0.5,
304 f64::from(i) - 0.5,
305 f64::from(i),
306 i64::from(i),
307 )
308 })
309 .collect();
310 let mut lines = TdLines::classic();
311 lines.batch(&candles);
312 assert!(lines.is_ready());
313 lines.reset();
314 assert!(!lines.is_ready());
315 assert_eq!(lines.update(candles[0]), None);
316 }
317
318 #[test]
319 fn accessors_and_metadata() {
320 let lines = TdLines::classic();
321 assert_eq!(lines.params(), (4, 9));
322 assert_eq!(lines.warmup_period(), 5);
323 assert_eq!(lines.name(), "TDLines");
324 }
325}