Skip to main content

finance_query/indicators/
bull_bear_power.rs

1//! Bull Bear Power indicator.
2
3use super::{IndicatorError, Result, ema::ema};
4use serde::{Deserialize, Serialize};
5
6/// Result of Bull Bear Power calculation
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct BullBearPowerResult {
9    /// Bull Power
10    pub bull_power: Vec<Option<f64>>,
11    /// Bear Power
12    pub bear_power: Vec<Option<f64>>,
13}
14
15/// Calculate Bull Bear Power.
16///
17/// Bull Power = High - EMA(period)
18/// Bear Power = Low - EMA(period)
19///
20/// # Arguments
21///
22/// * `highs` - High prices
23/// * `lows` - Low prices
24/// * `closes` - Close prices
25/// * `period` - EMA period (default: 13)
26///
27/// # Example
28///
29/// ```
30/// use finance_query::indicators::bull_bear_power;
31///
32/// let highs = vec![10.0; 20];
33/// let lows = vec![8.0; 20];
34/// let closes = vec![9.0; 20];
35/// let result = bull_bear_power(&highs, &lows, &closes, 13).unwrap();
36/// ```
37pub fn bull_bear_power(
38    highs: &[f64],
39    lows: &[f64],
40    closes: &[f64],
41    period: usize,
42) -> Result<BullBearPowerResult> {
43    if period == 0 {
44        return Err(IndicatorError::InvalidPeriod(
45            "Period must be greater than 0".to_string(),
46        ));
47    }
48    let len = highs.len();
49    if lows.len() != len || closes.len() != len {
50        return Err(IndicatorError::InvalidPeriod(
51            "Data lengths must match".to_string(),
52        ));
53    }
54    if len < period {
55        return Err(IndicatorError::InsufficientData {
56            need: period,
57            got: len,
58        });
59    }
60
61    let ema_values = ema(closes, period);
62
63    let mut bull_power = vec![None; len];
64    let mut bear_power = vec![None; len];
65
66    for i in 0..len {
67        if let Some(ema_val) = ema_values[i] {
68            bull_power[i] = Some(highs[i] - ema_val);
69            bear_power[i] = Some(lows[i] - ema_val);
70        }
71    }
72
73    Ok(BullBearPowerResult {
74        bull_power,
75        bear_power,
76    })
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_bull_bear_power_default_period() {
85        let highs = vec![10.0; 20];
86        let lows = vec![8.0; 20];
87        let closes = vec![9.0; 20];
88        let result = bull_bear_power(&highs, &lows, &closes, 13).unwrap();
89
90        assert_eq!(result.bull_power.len(), 20);
91        assert!(result.bull_power[11].is_none());
92        assert!(result.bull_power[12].is_some());
93    }
94
95    #[test]
96    fn test_bull_bear_power_custom_period() {
97        let highs = vec![10.0; 20];
98        let lows = vec![8.0; 20];
99        let closes = vec![9.0; 20];
100        let result = bull_bear_power(&highs, &lows, &closes, 5).unwrap();
101
102        assert_eq!(result.bull_power.len(), 20);
103        assert!(result.bull_power[3].is_none());
104        assert!(result.bull_power[4].is_some());
105    }
106
107    #[test]
108    fn test_bull_bear_power_custom_produces_different_output() {
109        let highs: Vec<f64> = (1..=30).map(|i| i as f64 + 1.0).collect();
110        let lows: Vec<f64> = (1..=30).map(|i| i as f64 - 1.0).collect();
111        let closes: Vec<f64> = (1..=30).map(|i| i as f64).collect();
112        let default = bull_bear_power(&highs, &lows, &closes, 13).unwrap();
113        let custom = bull_bear_power(&highs, &lows, &closes, 5).unwrap();
114        let idx = 14;
115        assert!(default.bull_power[idx].is_some());
116        assert!(custom.bull_power[idx].is_some());
117        assert_ne!(default.bull_power[idx], custom.bull_power[idx]);
118    }
119}