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)]
23pub struct TdTrap {
24 prev1: Option<Candle>,
25 prev2: Option<Candle>,
26 last_value: Option<f64>,
27}
28
29impl TdTrap {
30 #[must_use]
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub const fn value(&self) -> Option<f64> {
38 self.last_value
39 }
40}
41
42impl Indicator for TdTrap {
43 type Input = Candle;
44 type Output = f64;
45
46 fn update(&mut self, candle: Candle) -> Option<f64> {
47 let (Some(trap), Some(before)) = (self.prev1, self.prev2) else {
48 self.prev2 = self.prev1;
50 self.prev1 = Some(candle);
51 self.last_value = Some(0.0);
52 return Some(0.0);
53 };
54 let is_inside = trap.high < before.high && trap.low > before.low;
55 let v = if is_inside && candle.close > trap.high {
56 1.0
57 } else if is_inside && candle.close < trap.low {
58 -1.0
59 } else {
60 0.0
61 };
62 self.prev2 = self.prev1;
63 self.prev1 = Some(candle);
64 self.last_value = Some(v);
65 Some(v)
66 }
67
68 fn reset(&mut self) {
69 self.prev1 = None;
70 self.prev2 = None;
71 self.last_value = None;
72 }
73
74 fn warmup_period(&self) -> usize {
75 3
76 }
77
78 fn is_ready(&self) -> bool {
79 self.last_value.is_some()
80 }
81
82 fn name(&self) -> &'static str {
83 "TDTrap"
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::traits::BatchExt;
91
92 fn c(high: f64, low: f64, close: f64) -> Candle {
93 Candle::new_unchecked(f64::midpoint(high, low), high, low, close, 0.0, 0)
94 }
95
96 #[test]
97 fn accessors_and_metadata() {
98 let td = TdTrap::new();
99 assert_eq!(td.warmup_period(), 3);
100 assert_eq!(td.name(), "TDTrap");
101 assert!(!td.is_ready());
102 assert_eq!(td.value(), None);
103 }
104
105 #[test]
106 fn first_two_bars_seed_without_signal() {
107 let mut td = TdTrap::new();
108 assert_eq!(td.update(c(110.0, 90.0, 100.0)), Some(0.0));
109 assert_eq!(td.update(c(108.0, 95.0, 102.0)), Some(0.0));
110 assert!(td.update(c(112.0, 100.0, 110.0)).is_some());
111 }
112
113 #[test]
114 fn inside_then_breakout_up_buys() {
115 let mut td = TdTrap::new();
117 td.update(c(110.0, 90.0, 100.0));
118 td.update(c(108.0, 95.0, 102.0)); assert_eq!(td.update(c(112.0, 100.0, 109.0)), Some(1.0));
120 }
121
122 #[test]
123 fn inside_then_breakdown_sells() {
124 let mut td = TdTrap::new();
125 td.update(c(110.0, 90.0, 100.0));
126 td.update(c(108.0, 95.0, 102.0)); assert_eq!(td.update(c(100.0, 92.0, 94.0)), Some(-1.0)); }
129
130 #[test]
131 fn no_inside_bar_is_zero() {
132 let mut td = TdTrap::new();
133 td.update(c(110.0, 90.0, 100.0));
134 td.update(c(115.0, 85.0, 100.0)); assert_eq!(td.update(c(120.0, 110.0, 118.0)), Some(0.0));
136 }
137
138 #[test]
139 fn inside_but_no_breakout_is_zero() {
140 let mut td = TdTrap::new();
141 td.update(c(110.0, 90.0, 100.0));
142 td.update(c(108.0, 95.0, 102.0)); assert_eq!(td.update(c(107.0, 96.0, 103.0)), Some(0.0)); }
145
146 #[test]
147 fn reset_clears_state() {
148 let mut td = TdTrap::new();
149 td.update(c(110.0, 90.0, 100.0));
150 td.update(c(108.0, 95.0, 102.0));
151 td.update(c(112.0, 100.0, 109.0));
152 assert!(td.is_ready());
153 td.reset();
154 assert!(!td.is_ready());
155 assert_eq!(td.update(c(110.0, 90.0, 100.0)), Some(0.0));
156 }
157
158 #[test]
159 fn batch_equals_streaming() {
160 let candles: Vec<Candle> = (0..40)
161 .map(|i| {
162 let b = 100.0 + (f64::from(i) * 0.4).sin() * 6.0;
163 c(b + 2.0, b - 2.0, b)
164 })
165 .collect();
166 let batch = TdTrap::new().batch(&candles);
167 let mut b = TdTrap::new();
168 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
169 assert_eq!(batch, streamed);
170 }
171}