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