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