wickra_core/indicators/
closing_marubozu.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
43pub struct ClosingMarubozu {
44 has_emitted: bool,
45}
46
47impl ClosingMarubozu {
48 pub const fn new() -> Self {
50 Self { has_emitted: false }
51 }
52}
53
54impl Indicator for ClosingMarubozu {
55 type Input = Candle;
56 type Output = f64;
57
58 fn update(&mut self, candle: Candle) -> Option<f64> {
59 self.has_emitted = true;
60 let range = candle.high - candle.low;
61 if range <= 0.0 {
62 return Some(0.0);
63 }
64 let body = candle.close - candle.open;
65 if body.abs() < 0.7 * range {
66 return Some(0.0);
67 }
68 let tol = 0.05 * range;
69 if body > 0.0 && candle.high - candle.close <= tol {
70 return Some(1.0);
71 }
72 if body < 0.0 && candle.close - candle.low <= tol {
73 return Some(-1.0);
74 }
75 Some(0.0)
76 }
77
78 fn reset(&mut self) {
79 self.has_emitted = false;
80 }
81
82 fn warmup_period(&self) -> usize {
83 1
84 }
85
86 fn is_ready(&self) -> bool {
87 self.has_emitted
88 }
89
90 fn name(&self) -> &'static str {
91 "ClosingMarubozu"
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::traits::BatchExt;
99
100 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
101 Candle::new(open, high, low, close, 1.0, ts).unwrap()
102 }
103
104 #[test]
105 fn accessors_and_metadata() {
106 let t = ClosingMarubozu::new();
107 assert_eq!(t.name(), "ClosingMarubozu");
108 assert_eq!(t.warmup_period(), 1);
109 assert!(!t.is_ready());
110 }
111
112 #[test]
113 fn white_closing_marubozu_is_plus_one() {
114 let mut t = ClosingMarubozu::new();
115 assert_eq!(t.update(c(10.5, 15.0, 10.0, 15.0, 0)), Some(1.0));
117 }
118
119 #[test]
120 fn black_closing_marubozu_is_minus_one() {
121 let mut t = ClosingMarubozu::new();
122 assert_eq!(t.update(c(14.5, 15.0, 10.0, 10.0, 0)), Some(-1.0));
124 }
125
126 #[test]
127 fn white_with_upper_shadow_yields_zero() {
128 let mut t = ClosingMarubozu::new();
129 assert_eq!(t.update(c(10.5, 16.0, 10.0, 15.0, 0)), Some(0.0));
131 }
132
133 #[test]
134 fn black_with_lower_shadow_yields_zero() {
135 let mut t = ClosingMarubozu::new();
136 assert_eq!(t.update(c(14.5, 15.0, 9.0, 10.0, 0)), Some(0.0));
138 }
139
140 #[test]
141 fn short_body_yields_zero() {
142 let mut t = ClosingMarubozu::new();
143 assert_eq!(t.update(c(12.0, 15.0, 10.0, 12.5, 0)), Some(0.0));
145 }
146
147 #[test]
148 fn zero_range_yields_zero() {
149 let mut t = ClosingMarubozu::new();
150 assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
151 }
152
153 #[test]
154 fn batch_equals_streaming() {
155 let candles: Vec<Candle> = (0..40)
156 .map(|i| {
157 let base = 100.0 + i as f64;
158 c(base + 0.5, base + 5.0, base, base + 5.0, i)
159 })
160 .collect();
161 let mut a = ClosingMarubozu::new();
162 let mut b = ClosingMarubozu::new();
163 assert_eq!(
164 a.batch(&candles),
165 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
166 );
167 }
168
169 #[test]
170 fn reset_clears_state() {
171 let mut t = ClosingMarubozu::new();
172 t.update(c(10.5, 15.0, 10.0, 15.0, 0));
173 assert!(t.is_ready());
174 t.reset();
175 assert!(!t.is_ready());
176 }
177}