wickra_core/indicators/
td_trap.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
19use crate::traits::Indicator;
20
21#[derive(Debug, Clone, Default)]
38pub struct TdTrap {
39 prev1: Option<Candle>,
40 prev2: Option<Candle>,
41 last_value: Option<f64>,
42}
43
44impl TdTrap {
45 #[must_use]
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub const fn value(&self) -> Option<f64> {
53 self.last_value
54 }
55}
56
57impl Indicator for TdTrap {
58 type Input = Candle;
59 type Output = f64;
60
61 #[inline]
62 fn update(&mut self, candle: Candle) -> Option<f64> {
63 let (Some(trap), Some(before)) = (self.prev1, self.prev2) else {
64 self.prev2 = self.prev1;
66 self.prev1 = Some(candle);
67 self.last_value = None;
68 return None;
69 };
70 let is_inside = trap.high < before.high && trap.low > before.low;
71 let v = if is_inside && candle.close > trap.high {
72 1.0
73 } else if is_inside && candle.close < trap.low {
74 -1.0
75 } else {
76 0.0
77 };
78 self.prev2 = self.prev1;
79 self.prev1 = Some(candle);
80 self.last_value = Some(v);
81 Some(v)
82 }
83
84 fn reset(&mut self) {
85 self.prev1 = None;
86 self.prev2 = None;
87 self.last_value = None;
88 }
89
90 #[inline]
91 fn warmup_period(&self) -> usize {
92 3
93 }
94
95 #[inline]
96 fn is_ready(&self) -> bool {
97 self.last_value.is_some()
98 }
99
100 #[inline]
101 fn name(&self) -> &'static str {
102 "TDTrap"
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::BatchExt;
110
111 fn c(high: f64, low: f64, close: f64) -> Candle {
112 Candle::new_unchecked(f64::midpoint(high, low), high, low, close, 0.0, 0)
113 }
114
115 #[test]
116 fn accessors_and_metadata() {
117 let td = TdTrap::new();
118 assert_eq!(td.warmup_period(), 3);
119 assert_eq!(td.name(), "TDTrap");
120 assert!(!td.is_ready());
121 assert_eq!(td.value(), None);
122 }
123
124 #[test]
125 fn first_two_bars_seed_without_signal() {
126 let mut td = TdTrap::new();
127 assert_eq!(td.update(c(110.0, 90.0, 100.0)), None);
128 assert_eq!(td.update(c(108.0, 95.0, 102.0)), None);
129 assert!(td.update(c(112.0, 100.0, 110.0)).is_some());
130 }
131
132 #[test]
133 fn inside_then_breakout_up_buys() {
134 let mut td = TdTrap::new();
136 td.update(c(110.0, 90.0, 100.0));
137 td.update(c(108.0, 95.0, 102.0)); assert_eq!(td.update(c(112.0, 100.0, 109.0)), Some(1.0));
139 }
140
141 #[test]
142 fn inside_then_breakdown_sells() {
143 let mut td = TdTrap::new();
144 td.update(c(110.0, 90.0, 100.0));
145 td.update(c(108.0, 95.0, 102.0)); assert_eq!(td.update(c(100.0, 92.0, 94.0)), Some(-1.0)); }
148
149 #[test]
150 fn no_inside_bar_is_zero() {
151 let mut td = TdTrap::new();
152 td.update(c(110.0, 90.0, 100.0));
153 td.update(c(115.0, 85.0, 100.0)); assert_eq!(td.update(c(120.0, 110.0, 118.0)), Some(0.0));
155 }
156
157 #[test]
158 fn inside_but_no_breakout_is_zero() {
159 let mut td = TdTrap::new();
160 td.update(c(110.0, 90.0, 100.0));
161 td.update(c(108.0, 95.0, 102.0)); assert_eq!(td.update(c(107.0, 96.0, 103.0)), Some(0.0)); }
164
165 #[test]
166 fn reset_clears_state() {
167 let mut td = TdTrap::new();
168 td.update(c(110.0, 90.0, 100.0));
169 td.update(c(108.0, 95.0, 102.0));
170 td.update(c(112.0, 100.0, 109.0));
171 assert!(td.is_ready());
172 td.reset();
173 assert!(!td.is_ready());
174 assert_eq!(td.update(c(110.0, 90.0, 100.0)), None);
175 }
176
177 #[test]
178 fn batch_equals_streaming() {
179 let candles: Vec<Candle> = (0..40)
180 .map(|i| {
181 let b = 100.0 + (f64::from(i) * 0.4).sin() * 6.0;
182 c(b + 2.0, b - 2.0, b)
183 })
184 .collect();
185 let batch = TdTrap::new().batch(&candles);
186 let mut b = TdTrap::new();
187 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
188 assert_eq!(batch, streamed);
189 }
190}