kestrel_chartkit/indicator/
buy_sell_pressure.rs1use super::smoothing::Ema;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
21pub struct BuySellPressureEstimator {
22 period: usize,
23 ema: Ema,
24}
25
26impl BuySellPressureEstimator {
27 pub fn new(period: usize) -> Self {
28 Self {
29 period: period.max(1),
30 ema: Ema::new(period),
31 }
32 }
33
34 pub fn with_defaults() -> Self {
35 Self::new(14)
36 }
37}
38
39impl Indicator for BuySellPressureEstimator {
40 fn name(&self) -> &str {
41 "buy_sell_pressure"
42 }
43
44 fn warmup_period(&self) -> usize {
45 self.period
46 }
47
48 fn reset(&mut self) {
49 self.ema.reset();
50 }
51
52 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
53 let range = (bar.high - bar.low).max(1e-8);
54
55 let location = (2.0 * (bar.close - bar.low) / range) - 1.0;
57
58 let upper_wick = bar.high - bar.high.min(bar.open.max(bar.close));
60 let lower_wick = bar.low.max(bar.open.min(bar.close)) - bar.low;
61 let wick_balance = (lower_wick - upper_wick) / range;
62
63 let raw_pressure = (location * 0.6 + wick_balance * 0.4) * 100.0;
65 let smoothed_pressure = self.ema.update(raw_pressure)?.clamp(-100.0, 100.0);
66
67 let mut extra = HashMap::new();
68 extra.insert("location".to_string(), location);
69 extra.insert("wick_balance".to_string(), wick_balance);
70
71 Some(IndicatorOutput::with_extra(smoothed_pressure, extra))
72 }
73
74 fn alerts(&self) -> Vec<IndicatorAlert> {
75 Vec::new()
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn test_buy_sell_pressure() {
85 let mut bsp = BuySellPressureEstimator::with_defaults();
86 let mut out = None;
87 for i in 0..20 {
88 let b = Bar::new(i, 100.0, 105.0, 95.0, 104.0, 1000.0);
89 out = bsp.on_bar(&b);
90 }
91 assert!(out.is_some());
92 let val = out.unwrap().value;
93 assert!((-100.0..=100.0).contains(&val));
94 }
95}