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