wickra_core/indicators/
initial_balance.rs1use crate::error::{Error, Result};
12use crate::ohlcv::Candle;
13use crate::traits::Indicator;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct InitialBalanceOutput {
18 pub high: f64,
20 pub low: f64,
22}
23
24#[derive(Debug, Clone)]
52pub struct InitialBalance {
53 period: usize,
54 bars_seen: usize,
55 high: f64,
56 low: f64,
57 locked: bool,
58}
59
60impl InitialBalance {
61 pub fn new(period: usize) -> Result<Self> {
67 if period == 0 {
68 return Err(Error::PeriodZero);
69 }
70 Ok(Self {
71 period,
72 bars_seen: 0,
73 high: f64::NEG_INFINITY,
74 low: f64::INFINITY,
75 locked: false,
76 })
77 }
78
79 pub fn classic() -> Self {
81 Self::new(12).expect("classic IB period is valid")
82 }
83
84 pub const fn period(&self) -> usize {
86 self.period
87 }
88
89 pub fn value(&self) -> Option<InitialBalanceOutput> {
91 if self.bars_seen == 0 {
92 None
93 } else {
94 Some(InitialBalanceOutput {
95 high: self.high,
96 low: self.low,
97 })
98 }
99 }
100
101 pub const fn is_locked(&self) -> bool {
103 self.locked
104 }
105}
106
107impl Indicator for InitialBalance {
108 type Input = Candle;
109 type Output = InitialBalanceOutput;
110
111 fn update(&mut self, candle: Candle) -> Option<InitialBalanceOutput> {
112 if self.locked {
113 return Some(InitialBalanceOutput {
114 high: self.high,
115 low: self.low,
116 });
117 }
118 if candle.high > self.high {
119 self.high = candle.high;
120 }
121 if candle.low < self.low {
122 self.low = candle.low;
123 }
124 self.bars_seen += 1;
125 if self.bars_seen >= self.period {
126 self.locked = true;
127 }
128 Some(InitialBalanceOutput {
129 high: self.high,
130 low: self.low,
131 })
132 }
133
134 fn reset(&mut self) {
135 self.bars_seen = 0;
136 self.high = f64::NEG_INFINITY;
137 self.low = f64::INFINITY;
138 self.locked = false;
139 }
140
141 fn warmup_period(&self) -> usize {
142 1
143 }
144
145 fn is_ready(&self) -> bool {
146 self.bars_seen > 0
147 }
148
149 fn name(&self) -> &'static str {
150 "InitialBalance"
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::traits::BatchExt;
158 use approx::assert_relative_eq;
159
160 fn c(high: f64, low: f64, ts: i64) -> Candle {
161 let mid = f64::midpoint(high, low);
163 Candle::new(mid, high, low, mid, 10.0, ts).unwrap()
164 }
165
166 #[test]
167 fn rejects_zero_period() {
168 assert!(matches!(InitialBalance::new(0), Err(Error::PeriodZero)));
169 }
170
171 #[test]
172 fn accessors_and_metadata() {
173 let mut ib = InitialBalance::new(12).unwrap();
174 assert_eq!(ib.period(), 12);
175 assert_eq!(ib.name(), "InitialBalance");
176 assert_eq!(ib.warmup_period(), 1);
177 assert!(ib.value().is_none());
178 assert!(!ib.is_locked());
179 ib.update(c(102.0, 100.0, 0));
181 let v = ib.value().unwrap();
182 assert_relative_eq!(v.high, 102.0);
183 assert_relative_eq!(v.low, 100.0);
184 }
185
186 #[test]
187 fn classic_is_constructible() {
188 let ib = InitialBalance::classic();
189 assert_eq!(ib.period(), 12);
190 }
191
192 #[test]
193 fn tracks_high_low_during_window() {
194 let mut ib = InitialBalance::new(3).unwrap();
195 let o1 = ib.update(c(102.0, 100.0, 0)).unwrap();
196 assert_relative_eq!(o1.high, 102.0);
197 assert_relative_eq!(o1.low, 100.0);
198 let o2 = ib.update(c(105.0, 99.0, 1)).unwrap();
199 assert_relative_eq!(o2.high, 105.0);
200 assert_relative_eq!(o2.low, 99.0);
201 let o3 = ib.update(c(103.0, 99.5, 2)).unwrap();
202 assert_relative_eq!(o3.high, 105.0);
203 assert_relative_eq!(o3.low, 99.0);
204 assert!(ib.is_locked());
205 }
206
207 #[test]
208 fn locks_after_period_and_ignores_subsequent_bars() {
209 let mut ib = InitialBalance::new(2).unwrap();
210 ib.update(c(102.0, 100.0, 0));
211 ib.update(c(103.0, 101.0, 1));
212 assert!(ib.is_locked());
213 let after = ib.update(c(200.0, 50.0, 2)).unwrap();
215 assert_relative_eq!(after.high, 103.0);
216 assert_relative_eq!(after.low, 100.0);
217 }
218
219 #[test]
220 fn reset_unlocks_and_clears_state() {
221 let mut ib = InitialBalance::new(2).unwrap();
222 ib.update(c(102.0, 100.0, 0));
223 ib.update(c(103.0, 101.0, 1));
224 assert!(ib.is_locked());
225 ib.reset();
226 assert!(!ib.is_locked());
227 assert!(!ib.is_ready());
228 let o = ib.update(c(50.0, 49.0, 2)).unwrap();
230 assert_relative_eq!(o.high, 50.0);
231 assert_relative_eq!(o.low, 49.0);
232 }
233
234 #[test]
235 fn batch_equals_streaming() {
236 let candles: Vec<Candle> = (0..20)
237 .map(|i| c(100.0 + i as f64, 99.0 + i as f64 * 0.5, i))
238 .collect();
239 let mut a = InitialBalance::new(5).unwrap();
240 let mut b = InitialBalance::new(5).unwrap();
241 assert_eq!(
242 a.batch(&candles),
243 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
244 );
245 }
246
247 #[test]
248 fn is_ready_after_first_bar() {
249 let mut ib = InitialBalance::new(5).unwrap();
250 assert!(!ib.is_ready());
251 ib.update(c(101.0, 99.0, 0));
252 assert!(ib.is_ready());
253 }
254}