Skip to main content

kestrel_chartkit/indicator/
buy_sell_pressure.rs

1use super::smoothing::Ema;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::HashMap;
5
6/// Buy/Sell Pressure Estimator Engine (-100..+100).
7/// Pressure = Location * Range * Volume * Wick Structure * Direction
8#[derive(Debug, Clone)]
9pub struct BuySellPressureEstimator {
10    period: usize,
11    ema: Ema,
12}
13
14impl BuySellPressureEstimator {
15    pub fn new(period: usize) -> Self {
16        Self {
17            period: period.max(1),
18            ema: Ema::new(period),
19        }
20    }
21
22    pub fn with_defaults() -> Self {
23        Self::new(14)
24    }
25}
26
27impl Indicator for BuySellPressureEstimator {
28    fn name(&self) -> &str {
29        "buy_sell_pressure"
30    }
31
32    fn warmup_period(&self) -> usize {
33        self.period
34    }
35
36    fn reset(&mut self) {
37        self.ema.reset();
38    }
39
40    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
41        let range = (bar.high - bar.low).max(1e-8);
42
43        // Location of close within range (-1.0 .. +1.0)
44        let location = (2.0 * (bar.close - bar.low) / range) - 1.0;
45
46        // Upper wick vs Lower wick ratio
47        let upper_wick = bar.high - bar.high.min(bar.open.max(bar.close));
48        let lower_wick = bar.low.max(bar.open.min(bar.close)) - bar.low;
49        let wick_balance = (lower_wick - upper_wick) / range;
50
51        // Raw pressure per bar
52        let raw_pressure = (location * 0.6 + wick_balance * 0.4) * 100.0;
53        let smoothed_pressure = self.ema.update(raw_pressure).clamp(-100.0, 100.0);
54
55        let mut extra = HashMap::new();
56        extra.insert("location".to_string(), location);
57        extra.insert("wick_balance".to_string(), wick_balance);
58
59        Some(IndicatorOutput::with_extra(smoothed_pressure, extra))
60    }
61
62    fn alerts(&self) -> Vec<IndicatorAlert> {
63        Vec::new()
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_buy_sell_pressure() {
73        let mut bsp = BuySellPressureEstimator::with_defaults();
74        let mut out = None;
75        for i in 0..20 {
76            let b = Bar::new(i, 100.0, 105.0, 95.0, 104.0, 1000.0);
77            out = bsp.on_bar(&b);
78        }
79        assert!(out.is_some());
80        let val = out.unwrap().value;
81        assert!((-100.0..=100.0).contains(&val));
82    }
83}