Skip to main content

kestrel_chartkit/indicator/
bop.rs

1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4
5/// Balance of Power (BOP) Engine.
6/// BOP = SMA((Close - Open) / (High - Low), period)
7#[derive(Debug, Clone)]
8pub struct BalanceOfPowerEngine {
9    period: usize,
10    sma: Sma,
11}
12
13impl BalanceOfPowerEngine {
14    pub fn new(period: usize) -> Self {
15        Self {
16            period: period.max(1),
17            sma: Sma::new(period),
18        }
19    }
20}
21
22impl Indicator for BalanceOfPowerEngine {
23    fn name(&self) -> &str {
24        "bop"
25    }
26
27    fn warmup_period(&self) -> usize {
28        self.period
29    }
30
31    fn reset(&mut self) {
32        self.sma.reset();
33    }
34
35    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
36        let range = (bar.high - bar.low).max(1e-8);
37        let raw_bop = (bar.close - bar.open) / range;
38        let bop_val = self.sma.update(raw_bop)?;
39        Some(IndicatorOutput::new(bop_val.clamp(-1.0, 1.0)))
40    }
41
42    fn alerts(&self) -> Vec<IndicatorAlert> {
43        Vec::new()
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn test_bop_bounds() {
53        let mut bop = BalanceOfPowerEngine::new(14);
54        let mut out = None;
55        for i in 0..20 {
56            let b = Bar::new(i, 100.0, 105.0, 95.0, 104.0, 1000.0);
57            out = bop.on_bar(&b);
58        }
59        assert!(out.is_some());
60        let val = out.unwrap().value;
61        assert!((-1.0..=1.0).contains(&val));
62    }
63}