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