wickra_core/indicators/
td_setup.rs1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
29
30use crate::error::{Error, Result};
31use crate::ohlcv::Candle;
32use crate::traits::Indicator;
33
34#[derive(Debug, Clone)]
37pub struct TdSetup {
38 lookback: usize,
39 target: usize,
40 closes: VecDeque<f64>,
41 buy_count: usize,
42 sell_count: usize,
43 last_value: Option<f64>,
44}
45
46impl TdSetup {
47 pub fn new(lookback: usize, target: usize) -> Result<Self> {
55 if lookback == 0 || target == 0 {
56 return Err(Error::PeriodZero);
57 }
58 Ok(Self {
59 lookback,
60 target,
61 closes: VecDeque::with_capacity(lookback + 1),
62 buy_count: 0,
63 sell_count: 0,
64 last_value: None,
65 })
66 }
67
68 pub fn classic() -> Self {
70 Self::new(4, 9).expect("classic TD Setup parameters are valid")
71 }
72
73 pub const fn params(&self) -> (usize, usize) {
75 (self.lookback, self.target)
76 }
77
78 pub const fn value(&self) -> Option<f64> {
80 self.last_value
81 }
82}
83
84impl Indicator for TdSetup {
85 type Input = Candle;
86 type Output = f64;
87
88 fn update(&mut self, candle: Candle) -> Option<f64> {
89 if self.closes.len() > self.lookback {
92 self.closes.pop_front();
93 }
94 if self.closes.len() < self.lookback {
95 self.closes.push_back(candle.close);
96 return None;
97 }
98 let reference = *self.closes.front().expect("non-empty after the guard");
101 self.closes.push_back(candle.close);
102
103 if candle.close < reference {
104 self.buy_count = (self.buy_count + 1).min(self.target);
105 self.sell_count = 0;
106 let v = self.buy_count as f64;
107 self.last_value = Some(v);
108 Some(v)
109 } else if candle.close > reference {
110 self.sell_count = (self.sell_count + 1).min(self.target);
111 self.buy_count = 0;
112 let v = -(self.sell_count as f64);
113 self.last_value = Some(v);
114 Some(v)
115 } else {
116 self.buy_count = 0;
118 self.sell_count = 0;
119 self.last_value = Some(0.0);
120 Some(0.0)
121 }
122 }
123
124 fn reset(&mut self) {
125 self.closes.clear();
126 self.buy_count = 0;
127 self.sell_count = 0;
128 self.last_value = None;
129 }
130
131 fn warmup_period(&self) -> usize {
132 self.lookback + 1
133 }
134
135 fn is_ready(&self) -> bool {
136 self.last_value.is_some()
137 }
138
139 fn name(&self) -> &'static str {
140 "TDSetup"
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::traits::BatchExt;
148
149 fn c(close: f64, ts: i64) -> Candle {
150 Candle::new_unchecked(close, close, close, close, 0.0, ts)
151 }
152
153 #[test]
154 fn pure_uptrend_reaches_sell_setup_9() {
155 let candles: Vec<Candle> = (1..=20).map(|i| c(f64::from(i), i64::from(i))).collect();
158 let mut setup = TdSetup::classic();
159 let out = setup.batch(&candles);
160 for (i, v) in out.iter().enumerate().take(4) {
164 assert!(v.is_none(), "index {i} must be None during warmup");
165 }
166 assert_eq!(out[4], Some(-1.0));
167 assert_eq!(out[5], Some(-2.0));
168 assert_eq!(out[12], Some(-9.0));
169 assert_eq!(out[13], Some(-9.0));
170 assert_eq!(out[19], Some(-9.0));
171 }
172
173 #[test]
174 fn pure_downtrend_reaches_buy_setup_9() {
175 let candles: Vec<Candle> = (1..=20)
176 .rev()
177 .enumerate()
178 .map(|(i, v)| c(f64::from(v), i64::try_from(i).unwrap()))
179 .collect();
180 let mut setup = TdSetup::classic();
181 let out = setup.batch(&candles);
182 assert_eq!(out[4], Some(1.0));
184 assert_eq!(out[12], Some(9.0));
185 assert_eq!(out[19], Some(9.0));
186 }
187
188 #[test]
189 fn flat_series_emits_zero_after_warmup() {
190 let candles: Vec<Candle> = (0..20).map(|i| c(42.0, i)).collect();
193 let mut setup = TdSetup::classic();
194 let out = setup.batch(&candles);
195 for v in out.iter().skip(4) {
196 assert_eq!(*v, Some(0.0));
197 }
198 }
199
200 #[test]
201 fn streak_resets_on_direction_flip() {
202 let candles = [
206 c(10.0, 0),
207 c(10.0, 1),
208 c(10.0, 2),
209 c(10.0, 3),
210 c(9.0, 4),
211 c(8.0, 5),
212 c(7.0, 6),
213 c(6.0, 7),
214 c(11.0, 8),
215 ];
216 let mut setup = TdSetup::classic();
217 let out = setup.batch(&candles);
218 assert_eq!(out[4], Some(1.0));
219 assert_eq!(out[7], Some(4.0));
220 assert_eq!(out[8], Some(-1.0));
221 }
222
223 #[test]
224 fn rejects_zero_arguments() {
225 assert!(matches!(TdSetup::new(0, 9), Err(Error::PeriodZero)));
226 assert!(matches!(TdSetup::new(4, 0), Err(Error::PeriodZero)));
227 }
228
229 #[test]
230 fn batch_equals_streaming() {
231 let candles: Vec<Candle> = (0..80)
232 .map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0, i64::from(i)))
233 .collect();
234 let mut a = TdSetup::classic();
235 let mut b = TdSetup::classic();
236 assert_eq!(
237 a.batch(&candles),
238 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
239 );
240 }
241
242 #[test]
243 fn reset_clears_state() {
244 let candles: Vec<Candle> = (1..=20).map(|i| c(f64::from(i), i64::from(i))).collect();
245 let mut setup = TdSetup::classic();
246 setup.batch(&candles);
247 assert!(setup.is_ready());
248 setup.reset();
249 assert!(!setup.is_ready());
250 assert_eq!(setup.update(candles[0]), None);
251 assert_eq!(setup.value(), None);
252 }
253
254 #[test]
255 fn accessors_and_metadata() {
256 let setup = TdSetup::new(4, 9).unwrap();
257 assert_eq!(setup.params(), (4, 9));
258 assert_eq!(setup.warmup_period(), 5);
259 assert_eq!(setup.name(), "TDSetup");
260 assert_eq!(setup.value(), None);
261 }
262}