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 #[inline]
60 fn update(&mut self, candle: Candle) -> Option<f64> {
61 self.has_emitted = true;
62 let range = candle.high - candle.low;
63 if range <= 0.0 {
64 return Some(0.0);
65 }
66 let body = candle.close - candle.open;
67 if body.abs() < 0.7 * range {
68 return Some(0.0);
69 }
70 let tol = 0.05 * range;
71 if body > 0.0 && candle.open - candle.low <= tol {
72 return Some(1.0);
73 }
74 if body < 0.0 && candle.high - candle.open <= tol {
75 return Some(-1.0);
76 }
77 Some(0.0)
78 }
79
80 fn reset(&mut self) {
81 self.has_emitted = false;
82 }
83
84 #[inline]
85 fn warmup_period(&self) -> usize {
86 1
87 }
88
89 #[inline]
90 fn is_ready(&self) -> bool {
91 self.has_emitted
92 }
93
94 #[inline]
95 fn name(&self) -> &'static str {
96 "OpeningMarubozu"
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::traits::BatchExt;
104
105 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
106 Candle::new(open, high, low, close, 1.0, ts).unwrap()
107 }
108
109 #[test]
110 fn accessors_and_metadata() {
111 let t = OpeningMarubozu::new();
112 assert_eq!(t.name(), "OpeningMarubozu");
113 assert_eq!(t.warmup_period(), 1);
114 assert!(!t.is_ready());
115 }
116
117 #[test]
118 fn white_opening_marubozu_is_plus_one() {
119 let mut t = OpeningMarubozu::new();
120 assert_eq!(t.update(c(10.0, 15.0, 10.0, 14.5, 0)), Some(1.0));
122 }
123
124 #[test]
125 fn black_opening_marubozu_is_minus_one() {
126 let mut t = OpeningMarubozu::new();
127 assert_eq!(t.update(c(15.0, 15.0, 10.0, 10.5, 0)), Some(-1.0));
129 }
130
131 #[test]
132 fn white_with_lower_shadow_yields_zero() {
133 let mut t = OpeningMarubozu::new();
134 assert_eq!(t.update(c(11.0, 15.0, 10.0, 15.0, 0)), Some(0.0));
136 }
137
138 #[test]
139 fn black_with_upper_shadow_yields_zero() {
140 let mut t = OpeningMarubozu::new();
141 assert_eq!(t.update(c(14.0, 16.0, 10.0, 10.5, 0)), Some(0.0));
143 }
144
145 #[test]
146 fn short_body_yields_zero() {
147 let mut t = OpeningMarubozu::new();
148 assert_eq!(t.update(c(10.0, 15.0, 10.0, 12.5, 0)), Some(0.0));
150 }
151
152 #[test]
153 fn zero_range_yields_zero() {
154 let mut t = OpeningMarubozu::new();
155 assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 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 base = 100.0 + i as f64;
163 c(base, base + 5.0, base, base + 4.5, i)
164 })
165 .collect();
166 let mut a = OpeningMarubozu::new();
167 let mut b = OpeningMarubozu::new();
168 assert_eq!(
169 a.batch(&candles),
170 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
171 );
172 }
173
174 #[test]
175 fn reset_clears_state() {
176 let mut t = OpeningMarubozu::new();
177 t.update(c(10.0, 15.0, 10.0, 14.5, 0));
178 assert!(t.is_ready());
179 t.reset();
180 assert!(!t.is_ready());
181 }
182}