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