wickra_core/indicators/
td_camouflage.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
18use crate::traits::Indicator;
19
20#[derive(Debug, Clone, Default)]
37pub struct TdCamouflage {
38 prev: Option<Candle>,
39 last_value: Option<f64>,
40}
41
42impl TdCamouflage {
43 #[must_use]
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub const fn value(&self) -> Option<f64> {
51 self.last_value
52 }
53}
54
55impl Indicator for TdCamouflage {
56 type Input = Candle;
57 type Output = f64;
58
59 fn update(&mut self, candle: Candle) -> Option<f64> {
60 let Some(prev) = self.prev else {
61 self.prev = Some(candle);
62 self.last_value = Some(0.0);
63 return Some(0.0);
64 };
65 let v = if candle.close < prev.close && candle.close > candle.open && candle.low < prev.low
66 {
67 1.0
68 } else if candle.close > prev.close && candle.close < candle.open && candle.high > prev.high
69 {
70 -1.0
71 } else {
72 0.0
73 };
74 self.prev = Some(candle);
75 self.last_value = Some(v);
76 Some(v)
77 }
78
79 fn reset(&mut self) {
80 self.prev = None;
81 self.last_value = None;
82 }
83
84 fn warmup_period(&self) -> usize {
85 2
86 }
87
88 fn is_ready(&self) -> bool {
89 self.last_value.is_some()
90 }
91
92 fn name(&self) -> &'static str {
93 "TDCamouflage"
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::traits::BatchExt;
101
102 fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
103 Candle::new_unchecked(open, high, low, close, 0.0, 0)
104 }
105
106 #[test]
107 fn accessors_and_metadata() {
108 let td = TdCamouflage::new();
109 assert_eq!(td.warmup_period(), 2);
110 assert_eq!(td.name(), "TDCamouflage");
111 assert!(!td.is_ready());
112 assert_eq!(td.value(), None);
113 }
114
115 #[test]
116 fn first_bar_seeds_without_signal() {
117 let mut td = TdCamouflage::new();
118 assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
119 assert!(td.update(c(10.0, 11.0, 8.0, 9.5)).is_some());
120 }
121
122 #[test]
123 fn bullish_camouflage_buy() {
124 let mut td = TdCamouflage::new();
127 td.update(c(10.0, 11.0, 8.0, 10.0));
128 assert_eq!(td.update(c(9.0, 10.0, 7.0, 9.5)), Some(1.0));
129 }
130
131 #[test]
132 fn bearish_camouflage_sell() {
133 let mut td = TdCamouflage::new();
136 td.update(c(10.0, 11.0, 8.0, 10.0));
137 assert_eq!(td.update(c(11.0, 12.0, 10.0, 10.5)), Some(-1.0));
138 }
139
140 #[test]
141 fn no_pattern_is_zero() {
142 let mut td = TdCamouflage::new();
143 td.update(c(10.0, 11.0, 9.0, 10.0));
144 assert_eq!(td.update(c(10.0, 11.5, 9.5, 11.0)), Some(0.0));
145 }
146
147 #[test]
148 fn reset_clears_state() {
149 let mut td = TdCamouflage::new();
150 td.update(c(10.0, 11.0, 9.0, 10.0));
151 td.update(c(9.0, 10.0, 7.0, 9.5));
152 assert!(td.is_ready());
153 td.reset();
154 assert!(!td.is_ready());
155 assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
156 }
157
158 #[test]
159 fn batch_equals_streaming() {
160 let candles: Vec<Candle> = (0..40)
161 .map(|i| {
162 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
163 c(b, b + 1.0, b - 1.0, b + 0.2)
164 })
165 .collect();
166 let batch = TdCamouflage::new().batch(&candles);
167 let mut b = TdCamouflage::new();
168 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
169 assert_eq!(batch, streamed);
170 }
171}