wickra_core/indicators/
balance_of_power.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
34pub struct BalanceOfPower {
35 has_emitted: bool,
36}
37
38impl BalanceOfPower {
39 pub const fn new() -> Self {
41 Self { has_emitted: false }
42 }
43}
44
45impl Indicator for BalanceOfPower {
46 type Input = Candle;
47 type Output = f64;
48
49 #[inline]
50 fn update(&mut self, candle: Candle) -> Option<f64> {
51 self.has_emitted = true;
52 let range = candle.high - candle.low;
53 let bop = if range == 0.0 {
54 0.0
56 } else {
57 (candle.close - candle.open) / range
58 };
59 Some(bop)
60 }
61
62 fn reset(&mut self) {
63 self.has_emitted = false;
64 }
65
66 #[inline]
67 fn warmup_period(&self) -> usize {
68 1
69 }
70
71 #[inline]
72 fn is_ready(&self) -> bool {
73 self.has_emitted
74 }
75
76 #[inline]
77 fn name(&self) -> &'static str {
78 "BalanceOfPower"
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::traits::BatchExt;
86 use approx::assert_relative_eq;
87
88 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
89 Candle::new(open, high, low, close, 1.0, ts).unwrap()
90 }
91
92 #[test]
93 fn reference_value() {
94 let mut bop = BalanceOfPower::new();
96 assert_relative_eq!(
97 bop.update(candle(10.0, 14.0, 10.0, 12.0, 0)).unwrap(),
98 0.5,
99 epsilon = 1e-12
100 );
101 }
102
103 #[test]
104 fn close_on_high_after_open_on_low_is_plus_one() {
105 let mut bop = BalanceOfPower::new();
106 assert_relative_eq!(
108 bop.update(candle(9.0, 11.0, 9.0, 11.0, 0)).unwrap(),
109 1.0,
110 epsilon = 1e-12
111 );
112 }
113
114 #[test]
115 fn stays_within_unit_range() {
116 let candles: Vec<Candle> = (0..100)
117 .map(|i| {
118 let mid = 100.0 + (i as f64 * 0.2).sin() * 8.0;
119 let close = mid + (i as f64 * 0.5).cos() * 2.0;
120 candle(mid, mid + 3.0, mid - 3.0, close, i)
121 })
122 .collect();
123 let mut bop = BalanceOfPower::new();
124 for v in bop.batch(&candles).into_iter().flatten() {
125 assert!((-1.0..=1.0).contains(&v), "BOP {v} outside [-1, 1]");
126 }
127 }
128
129 #[test]
130 fn zero_range_bar_yields_zero() {
131 let mut bop = BalanceOfPower::new();
132 assert_relative_eq!(
133 bop.update(candle(10.0, 10.0, 10.0, 10.0, 0)).unwrap(),
134 0.0,
135 epsilon = 1e-12
136 );
137 }
138
139 #[test]
141 fn name_metadata() {
142 let bop = BalanceOfPower::new();
143 assert_eq!(bop.name(), "BalanceOfPower");
144 }
145
146 #[test]
147 fn emits_from_first_candle() {
148 let mut bop = BalanceOfPower::new();
149 assert_eq!(bop.warmup_period(), 1);
150 assert!(!bop.is_ready());
151 assert!(bop.update(candle(10.0, 11.0, 9.0, 10.0, 0)).is_some());
152 assert!(bop.is_ready());
153 }
154
155 #[test]
156 fn reset_clears_state() {
157 let mut bop = BalanceOfPower::new();
158 bop.update(candle(10.0, 11.0, 9.0, 10.0, 0));
159 assert!(bop.is_ready());
160 bop.reset();
161 assert!(!bop.is_ready());
162 }
163
164 #[test]
165 fn batch_equals_streaming() {
166 let candles: Vec<Candle> = (0..40)
167 .map(|i| {
168 let base = 100.0 + i as f64;
169 candle(base, base + 2.0, base - 2.0, base + 1.0, i)
170 })
171 .collect();
172 let mut a = BalanceOfPower::new();
173 let mut b = BalanceOfPower::new();
174 assert_eq!(
175 a.batch(&candles),
176 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
177 );
178 }
179}