kestrel_chartkit/indicator/
bop.rs1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4
5#[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}