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