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)]
23pub struct TdClopwin {
24 prev: Option<Candle>,
25 last_value: Option<f64>,
26}
27
28impl TdClopwin {
29 #[must_use]
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub const fn value(&self) -> Option<f64> {
37 self.last_value
38 }
39}
40
41impl Indicator for TdClopwin {
42 type Input = Candle;
43 type Output = f64;
44
45 fn update(&mut self, candle: Candle) -> Option<f64> {
46 let Some(prev) = self.prev else {
47 self.prev = Some(candle);
48 self.last_value = Some(0.0);
49 return Some(0.0);
50 };
51 let body_low = prev.open.min(prev.close);
52 let body_high = prev.open.max(prev.close);
53 let open_in = candle.open >= body_low && candle.open <= body_high;
54 let close_in = candle.close >= body_low && candle.close <= body_high;
55 let v = if open_in && close_in {
56 if candle.close >= candle.open {
57 1.0
58 } else {
59 -1.0
60 }
61 } else {
62 0.0
63 };
64 self.prev = Some(candle);
65 self.last_value = Some(v);
66 Some(v)
67 }
68
69 fn reset(&mut self) {
70 self.prev = None;
71 self.last_value = None;
72 }
73
74 fn warmup_period(&self) -> usize {
75 2
76 }
77
78 fn is_ready(&self) -> bool {
79 self.last_value.is_some()
80 }
81
82 fn name(&self) -> &'static str {
83 "TDClopwin"
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::traits::BatchExt;
91
92 fn c(open: f64, close: f64) -> Candle {
93 let high = open.max(close) + 1.0;
94 let low = open.min(close) - 1.0;
95 Candle::new_unchecked(open, high, low, close, 0.0, 0)
96 }
97
98 #[test]
99 fn accessors_and_metadata() {
100 let td = TdClopwin::new();
101 assert_eq!(td.warmup_period(), 2);
102 assert_eq!(td.name(), "TDClopwin");
103 assert!(!td.is_ready());
104 assert_eq!(td.value(), None);
105 }
106
107 #[test]
108 fn first_bar_seeds_without_signal() {
109 let mut td = TdClopwin::new();
110 assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
111 assert!(td.update(c(11.0, 13.0)).is_some());
112 }
113
114 #[test]
115 fn bullish_inside_body_buy() {
116 let mut td = TdClopwin::new();
118 td.update(c(10.0, 14.0));
119 assert_eq!(td.update(c(11.0, 13.0)), Some(1.0));
120 }
121
122 #[test]
123 fn bearish_inside_body_sell() {
124 let mut td = TdClopwin::new();
126 td.update(c(10.0, 14.0));
127 assert_eq!(td.update(c(13.0, 11.0)), Some(-1.0));
128 }
129
130 #[test]
131 fn outside_body_is_zero() {
132 let mut td = TdClopwin::new();
133 td.update(c(10.0, 14.0));
134 assert_eq!(td.update(c(11.0, 16.0)), Some(0.0));
136 }
137
138 #[test]
139 fn reset_clears_state() {
140 let mut td = TdClopwin::new();
141 td.update(c(10.0, 14.0));
142 td.update(c(11.0, 13.0));
143 assert!(td.is_ready());
144 td.reset();
145 assert!(!td.is_ready());
146 assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
147 }
148
149 #[test]
150 fn batch_equals_streaming() {
151 let candles: Vec<Candle> = (0..40)
152 .map(|i| {
153 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
154 c(b, b + 0.3)
155 })
156 .collect();
157 let batch = TdClopwin::new().batch(&candles);
158 let mut b = TdClopwin::new();
159 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
160 assert_eq!(batch, streamed);
161 }
162}