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 fn update(&mut self, candle: Candle) -> Option<f64> {
61 let Some(prev) = self.prev else {
62 self.prev = Some(candle);
63 self.last_value = Some(0.0);
64 return Some(0.0);
65 };
66 let body_low = prev.open.min(prev.close);
67 let body_high = prev.open.max(prev.close);
68 let open_in = candle.open >= body_low && candle.open <= body_high;
69 let close_in = candle.close >= body_low && candle.close <= body_high;
70 let v = if open_in && close_in {
71 if candle.close >= candle.open {
72 1.0
73 } else {
74 -1.0
75 }
76 } else {
77 0.0
78 };
79 self.prev = Some(candle);
80 self.last_value = Some(v);
81 Some(v)
82 }
83
84 fn reset(&mut self) {
85 self.prev = None;
86 self.last_value = None;
87 }
88
89 fn warmup_period(&self) -> usize {
90 2
91 }
92
93 fn is_ready(&self) -> bool {
94 self.last_value.is_some()
95 }
96
97 fn name(&self) -> &'static str {
98 "TDClopwin"
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::traits::BatchExt;
106
107 fn c(open: f64, close: f64) -> Candle {
108 let high = open.max(close) + 1.0;
109 let low = open.min(close) - 1.0;
110 Candle::new_unchecked(open, high, low, close, 0.0, 0)
111 }
112
113 #[test]
114 fn accessors_and_metadata() {
115 let td = TdClopwin::new();
116 assert_eq!(td.warmup_period(), 2);
117 assert_eq!(td.name(), "TDClopwin");
118 assert!(!td.is_ready());
119 assert_eq!(td.value(), None);
120 }
121
122 #[test]
123 fn first_bar_seeds_without_signal() {
124 let mut td = TdClopwin::new();
125 assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
126 assert!(td.update(c(11.0, 13.0)).is_some());
127 }
128
129 #[test]
130 fn bullish_inside_body_buy() {
131 let mut td = TdClopwin::new();
133 td.update(c(10.0, 14.0));
134 assert_eq!(td.update(c(11.0, 13.0)), Some(1.0));
135 }
136
137 #[test]
138 fn bearish_inside_body_sell() {
139 let mut td = TdClopwin::new();
141 td.update(c(10.0, 14.0));
142 assert_eq!(td.update(c(13.0, 11.0)), Some(-1.0));
143 }
144
145 #[test]
146 fn outside_body_is_zero() {
147 let mut td = TdClopwin::new();
148 td.update(c(10.0, 14.0));
149 assert_eq!(td.update(c(11.0, 16.0)), Some(0.0));
151 }
152
153 #[test]
154 fn reset_clears_state() {
155 let mut td = TdClopwin::new();
156 td.update(c(10.0, 14.0));
157 td.update(c(11.0, 13.0));
158 assert!(td.is_ready());
159 td.reset();
160 assert!(!td.is_ready());
161 assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
162 }
163
164 #[test]
165 fn batch_equals_streaming() {
166 let candles: Vec<Candle> = (0..40)
167 .map(|i| {
168 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
169 c(b, b + 0.3)
170 })
171 .collect();
172 let batch = TdClopwin::new().batch(&candles);
173 let mut b = TdClopwin::new();
174 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
175 assert_eq!(batch, streamed);
176 }
177}