wickra_core/indicators/
td_clop.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
19use crate::traits::Indicator;
20
21#[derive(Debug, Clone, Default)]
38pub struct TdClop {
39 prev: Option<Candle>,
40 last_value: Option<f64>,
41}
42
43impl TdClop {
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 TdClop {
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 below_body = candle.open < prev.open && candle.open < prev.close;
67 let above_body = candle.close > prev.open && candle.close > prev.close;
68 let over_body = candle.open > prev.open && candle.open > prev.close;
69 let under_body = candle.close < prev.open && candle.close < prev.close;
70 let v = if below_body && above_body {
71 1.0
72 } else if over_body && under_body {
73 -1.0
74 } else {
75 0.0
76 };
77 self.prev = Some(candle);
78 self.last_value = Some(v);
79 Some(v)
80 }
81
82 fn reset(&mut self) {
83 self.prev = None;
84 self.last_value = None;
85 }
86
87 fn warmup_period(&self) -> usize {
88 2
89 }
90
91 fn is_ready(&self) -> bool {
92 self.last_value.is_some()
93 }
94
95 fn name(&self) -> &'static str {
96 "TDClop"
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::traits::BatchExt;
104
105 fn c(open: f64, close: f64) -> Candle {
106 let high = open.max(close) + 1.0;
107 let low = open.min(close) - 1.0;
108 Candle::new_unchecked(open, high, low, close, 0.0, 0)
109 }
110
111 #[test]
112 fn accessors_and_metadata() {
113 let td = TdClop::new();
114 assert_eq!(td.warmup_period(), 2);
115 assert_eq!(td.name(), "TDClop");
116 assert!(!td.is_ready());
117 assert_eq!(td.value(), None);
118 }
119
120 #[test]
121 fn first_bar_seeds_without_signal() {
122 let mut td = TdClop::new();
123 assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
124 assert!(td.update(c(9.0, 12.0)).is_some());
125 }
126
127 #[test]
128 fn bullish_clop_buy() {
129 let mut td = TdClop::new();
131 td.update(c(10.0, 11.0));
132 assert_eq!(td.update(c(9.0, 12.0)), Some(1.0));
133 }
134
135 #[test]
136 fn bearish_clop_sell() {
137 let mut td = TdClop::new();
139 td.update(c(10.0, 11.0));
140 assert_eq!(td.update(c(12.0, 9.0)), Some(-1.0));
141 }
142
143 #[test]
144 fn no_pattern_is_zero() {
145 let mut td = TdClop::new();
146 td.update(c(10.0, 11.0));
147 assert_eq!(td.update(c(10.5, 11.5)), Some(0.0));
148 }
149
150 #[test]
151 fn reset_clears_state() {
152 let mut td = TdClop::new();
153 td.update(c(10.0, 11.0));
154 td.update(c(9.0, 12.0));
155 assert!(td.is_ready());
156 td.reset();
157 assert!(!td.is_ready());
158 assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
159 }
160
161 #[test]
162 fn batch_equals_streaming() {
163 let candles: Vec<Candle> = (0..40)
164 .map(|i| {
165 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
166 c(b, b + 0.5)
167 })
168 .collect();
169 let batch = TdClop::new().batch(&candles);
170 let mut b = TdClop::new();
171 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
172 assert_eq!(batch, streamed);
173 }
174}