wickra_core/indicators/
td_clopwin.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
19use crate::traits::Indicator;
20
21#[derive(Debug, Clone, Default)]
38pub struct TdClopwin {
39 prev: Option<Candle>,
40 last_value: Option<f64>,
41}
42
43impl TdClopwin {
44 #[must_use]
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub const fn value(&self) -> Option<f64> {
52 self.last_value
53 }
54}
55
56impl Indicator for TdClopwin {
57 type Input = Candle;
58 type Output = f64;
59
60 #[inline]
61 fn update(&mut self, candle: Candle) -> Option<f64> {
62 let Some(prev) = self.prev else {
63 self.prev = Some(candle);
64 self.last_value = None;
65 return None;
66 };
67 let body_low = prev.open.min(prev.close);
68 let body_high = prev.open.max(prev.close);
69 let open_in = candle.open >= body_low && candle.open <= body_high;
70 let close_in = candle.close >= body_low && candle.close <= body_high;
71 let v = if open_in && close_in {
72 if candle.close >= candle.open {
73 1.0
74 } else {
75 -1.0
76 }
77 } else {
78 0.0
79 };
80 self.prev = Some(candle);
81 self.last_value = Some(v);
82 Some(v)
83 }
84
85 fn reset(&mut self) {
86 self.prev = None;
87 self.last_value = None;
88 }
89
90 #[inline]
91 fn warmup_period(&self) -> usize {
92 2
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 "TDClopwin"
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::BatchExt;
110
111 fn c(open: f64, close: f64) -> Candle {
112 let high = open.max(close) + 1.0;
113 let low = open.min(close) - 1.0;
114 Candle::new_unchecked(open, high, low, close, 0.0, 0)
115 }
116
117 #[test]
118 fn accessors_and_metadata() {
119 let td = TdClopwin::new();
120 assert_eq!(td.warmup_period(), 2);
121 assert_eq!(td.name(), "TDClopwin");
122 assert!(!td.is_ready());
123 assert_eq!(td.value(), None);
124 }
125
126 #[test]
127 fn first_bar_seeds_without_signal() {
128 let mut td = TdClopwin::new();
129 assert_eq!(td.update(c(10.0, 14.0)), None);
130 assert!(td.update(c(11.0, 13.0)).is_some());
131 }
132
133 #[test]
134 fn bullish_inside_body_buy() {
135 let mut td = TdClopwin::new();
137 td.update(c(10.0, 14.0));
138 assert_eq!(td.update(c(11.0, 13.0)), Some(1.0));
139 }
140
141 #[test]
142 fn bearish_inside_body_sell() {
143 let mut td = TdClopwin::new();
145 td.update(c(10.0, 14.0));
146 assert_eq!(td.update(c(13.0, 11.0)), Some(-1.0));
147 }
148
149 #[test]
150 fn outside_body_is_zero() {
151 let mut td = TdClopwin::new();
152 td.update(c(10.0, 14.0));
153 assert_eq!(td.update(c(11.0, 16.0)), Some(0.0));
155 }
156
157 #[test]
158 fn reset_clears_state() {
159 let mut td = TdClopwin::new();
160 td.update(c(10.0, 14.0));
161 td.update(c(11.0, 13.0));
162 assert!(td.is_ready());
163 td.reset();
164 assert!(!td.is_ready());
165 assert_eq!(td.update(c(10.0, 14.0)), None);
166 }
167
168 #[test]
169 fn batch_equals_streaming() {
170 let candles: Vec<Candle> = (0..40)
171 .map(|i| {
172 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
173 c(b, b + 0.3)
174 })
175 .collect();
176 let batch = TdClopwin::new().batch(&candles);
177 let mut b = TdClopwin::new();
178 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
179 assert_eq!(batch, streamed);
180 }
181}