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)]
23pub struct TdClop {
24 prev: Option<Candle>,
25 last_value: Option<f64>,
26}
27
28impl TdClop {
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 TdClop {
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 below_body = candle.open < prev.open && candle.open < prev.close;
52 let above_body = candle.close > prev.open && candle.close > prev.close;
53 let over_body = candle.open > prev.open && candle.open > prev.close;
54 let under_body = candle.close < prev.open && candle.close < prev.close;
55 let v = if below_body && above_body {
56 1.0
57 } else if over_body && under_body {
58 -1.0
59 } else {
60 0.0
61 };
62 self.prev = Some(candle);
63 self.last_value = Some(v);
64 Some(v)
65 }
66
67 fn reset(&mut self) {
68 self.prev = None;
69 self.last_value = None;
70 }
71
72 fn warmup_period(&self) -> usize {
73 2
74 }
75
76 fn is_ready(&self) -> bool {
77 self.last_value.is_some()
78 }
79
80 fn name(&self) -> &'static str {
81 "TDClop"
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::traits::BatchExt;
89
90 fn c(open: f64, close: f64) -> Candle {
91 let high = open.max(close) + 1.0;
92 let low = open.min(close) - 1.0;
93 Candle::new_unchecked(open, high, low, close, 0.0, 0)
94 }
95
96 #[test]
97 fn accessors_and_metadata() {
98 let td = TdClop::new();
99 assert_eq!(td.warmup_period(), 2);
100 assert_eq!(td.name(), "TDClop");
101 assert!(!td.is_ready());
102 assert_eq!(td.value(), None);
103 }
104
105 #[test]
106 fn first_bar_seeds_without_signal() {
107 let mut td = TdClop::new();
108 assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
109 assert!(td.update(c(9.0, 12.0)).is_some());
110 }
111
112 #[test]
113 fn bullish_clop_buy() {
114 let mut td = TdClop::new();
116 td.update(c(10.0, 11.0));
117 assert_eq!(td.update(c(9.0, 12.0)), Some(1.0));
118 }
119
120 #[test]
121 fn bearish_clop_sell() {
122 let mut td = TdClop::new();
124 td.update(c(10.0, 11.0));
125 assert_eq!(td.update(c(12.0, 9.0)), Some(-1.0));
126 }
127
128 #[test]
129 fn no_pattern_is_zero() {
130 let mut td = TdClop::new();
131 td.update(c(10.0, 11.0));
132 assert_eq!(td.update(c(10.5, 11.5)), Some(0.0));
133 }
134
135 #[test]
136 fn reset_clears_state() {
137 let mut td = TdClop::new();
138 td.update(c(10.0, 11.0));
139 td.update(c(9.0, 12.0));
140 assert!(td.is_ready());
141 td.reset();
142 assert!(!td.is_ready());
143 assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
144 }
145
146 #[test]
147 fn batch_equals_streaming() {
148 let candles: Vec<Candle> = (0..40)
149 .map(|i| {
150 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
151 c(b, b + 0.5)
152 })
153 .collect();
154 let batch = TdClop::new().batch(&candles);
155 let mut b = TdClop::new();
156 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
157 assert_eq!(batch, streamed);
158 }
159}