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