wickra_core/indicators/
td_open.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
21use crate::traits::Indicator;
22
23#[derive(Debug, Clone, Default)]
40pub struct TdOpen {
41 prev: Option<Candle>,
42 last_value: Option<f64>,
43}
44
45impl TdOpen {
46 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 TdOpen {
58 type Input = Candle;
59 type Output = f64;
60
61 #[inline]
62 fn update(&mut self, candle: Candle) -> Option<f64> {
63 let Some(prev) = self.prev else {
64 self.prev = Some(candle);
65 return None;
66 };
67 let v = if candle.open < prev.low && candle.high > prev.low {
68 1.0
69 } else if candle.open > prev.high && candle.low < prev.high {
70 -1.0
71 } else {
72 0.0
73 };
74 self.prev = Some(candle);
75 self.last_value = Some(v);
76 Some(v)
77 }
78
79 fn reset(&mut self) {
80 self.prev = None;
81 self.last_value = None;
82 }
83
84 #[inline]
85 fn warmup_period(&self) -> usize {
86 2
87 }
88
89 #[inline]
90 fn is_ready(&self) -> bool {
91 self.last_value.is_some()
92 }
93
94 #[inline]
95 fn name(&self) -> &'static str {
96 "TDOpen"
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::traits::BatchExt;
104
105 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
106 Candle::new_unchecked(open, high, low, close, 0.0, ts)
107 }
108
109 #[test]
110 fn buy_signal_on_gap_down_with_recovery() {
111 let mut td = TdOpen::new();
113 assert_eq!(td.update(c(10.0, 11.0, 10.0, 10.5, 0)), None);
114 assert_eq!(td.update(c(9.0, 11.0, 8.5, 9.5, 1)), Some(1.0));
115 }
116
117 #[test]
118 fn sell_signal_on_gap_up_with_fade() {
119 let mut td = TdOpen::new();
121 assert_eq!(td.update(c(10.0, 12.0, 9.0, 11.0, 0)), None);
122 assert_eq!(td.update(c(13.0, 13.5, 11.0, 11.5, 1)), Some(-1.0));
123 }
124
125 #[test]
126 fn no_signal_on_normal_open_within_range() {
127 let mut td = TdOpen::new();
129 assert_eq!(td.update(c(10.0, 12.0, 9.0, 11.0, 0)), None);
130 assert_eq!(td.update(c(10.5, 11.5, 9.5, 11.0, 1)), Some(0.0));
131 }
132
133 #[test]
134 fn gap_down_without_recovery_is_zero() {
135 let mut td = TdOpen::new();
137 assert_eq!(td.update(c(10.0, 12.0, 10.0, 11.0, 0)), None);
138 assert_eq!(td.update(c(9.0, 9.5, 8.5, 9.0, 1)), Some(0.0));
140 }
141
142 #[test]
143 fn batch_equals_streaming() {
144 let candles: Vec<Candle> = (0..40)
145 .map(|i| {
146 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
147 c(m, m + 1.0, m - 1.0, m + 0.3, i64::from(i))
148 })
149 .collect();
150 let mut a = TdOpen::new();
151 let mut b = TdOpen::new();
152 assert_eq!(
153 a.batch(&candles),
154 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
155 );
156 }
157
158 #[test]
159 fn output_only_in_canonical_set() {
160 let candles: Vec<Candle> = (0..120)
161 .map(|i| {
162 let m = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
163 c(m, m + 1.0, m - 1.0, m + 0.3, i64::from(i))
164 })
165 .collect();
166 let mut td = TdOpen::new();
167 for v in td.batch(&candles).into_iter().flatten() {
168 assert!(v == -1.0 || v == 0.0 || v == 1.0, "unexpected value {v}");
169 }
170 }
171
172 #[test]
173 fn reset_clears_state() {
174 let mut td = TdOpen::new();
175 td.update(c(10.0, 11.0, 9.0, 10.0, 0));
176 td.update(c(10.5, 11.5, 9.5, 10.5, 1));
177 assert!(td.is_ready());
178 td.reset();
179 assert!(!td.is_ready());
180 assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0, 2)), None);
181 assert_eq!(td.value(), None);
182 }
183
184 #[test]
185 fn accessors_and_metadata() {
186 let td = TdOpen::new();
187 assert_eq!(td.warmup_period(), 2);
188 assert_eq!(td.name(), "TDOpen");
189 assert_eq!(td.value(), None);
190 }
191}