Skip to main content

finance_query/indicators/
keltner_channels.rs

1//! Keltner Channels indicator.
2
3use super::{IndicatorError, Result, atr::atr_raw, ema::ema_raw};
4use serde::{Deserialize, Serialize};
5
6/// Result of Keltner Channels calculation
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct KeltnerChannelsResult {
9    /// Upper channel
10    pub upper: Vec<Option<f64>>,
11    /// Middle channel (EMA)
12    pub middle: Vec<Option<f64>>,
13    /// Lower channel
14    pub lower: Vec<Option<f64>>,
15}
16
17/// Calculate Keltner Channels.
18///
19/// Middle Line = EMA(period)
20/// Upper Channel = EMA + (multiplier * ATR)
21/// Lower Channel = EMA - (multiplier * ATR)
22///
23/// # Arguments
24///
25/// * `highs` - High prices
26/// * `lows` - Low prices
27/// * `closes` - Close prices
28/// * `period` - EMA period
29/// * `atr_period` - ATR period
30/// * `multiplier` - ATR multiplier
31///
32/// # Example
33///
34/// ```
35/// use finance_query::indicators::keltner_channels;
36///
37/// let highs = vec![10.0; 20];
38/// let lows = vec![8.0; 20];
39/// let closes = vec![9.0; 20];
40/// let result = keltner_channels(&highs, &lows, &closes, 10, 10, 2.0).unwrap();
41/// ```
42pub fn keltner_channels(
43    highs: &[f64],
44    lows: &[f64],
45    closes: &[f64],
46    period: usize,
47    atr_period: usize,
48    multiplier: f64,
49) -> Result<KeltnerChannelsResult> {
50    if period == 0 || atr_period == 0 {
51        return Err(IndicatorError::InvalidPeriod(
52            "Periods must be greater than 0".to_string(),
53        ));
54    }
55    let len = highs.len();
56    if lows.len() != len || closes.len() != len {
57        return Err(IndicatorError::InvalidPeriod(
58            "Data lengths must match".to_string(),
59        ));
60    }
61    if len < period {
62        return Err(IndicatorError::InsufficientData {
63            need: period,
64            got: len,
65        });
66    }
67
68    let atr_dense = atr_raw(highs, lows, closes, atr_period)?;
69    keltner_with_atr_dense(closes, period, &atr_dense, atr_period, multiplier)
70}
71
72/// Internal variant accepting pre-computed dense ATR values (avoids redundant ATR computation
73/// when caller already has atr_raw output for the same period).
74/// `atr_dense[k]` corresponds to original index `k + atr_period - 1`.
75pub(crate) fn keltner_with_atr_dense(
76    closes: &[f64],
77    period: usize,
78    atr_dense: &[f64],
79    atr_period: usize,
80    multiplier: f64,
81) -> Result<KeltnerChannelsResult> {
82    if period == 0 || atr_period == 0 {
83        return Err(IndicatorError::InvalidPeriod(
84            "Periods must be greater than 0".to_string(),
85        ));
86    }
87    let len = closes.len();
88    if len < period {
89        return Err(IndicatorError::InsufficientData {
90            need: period,
91            got: len,
92        });
93    }
94    let ema_vals = ema_raw(closes, period);
95    let ema_off = period - 1;
96    let atr_off = atr_period - 1;
97    let mut upper = vec![None; len];
98    let mut middle = vec![None; len];
99    let mut lower = vec![None; len];
100    for (k, &ev) in ema_vals.iter().enumerate() {
101        let i = k + ema_off;
102        middle[i] = Some(ev);
103        if i >= atr_off {
104            let av = atr_dense[i - atr_off];
105            upper[i] = Some(ev + multiplier * av);
106            lower[i] = Some(ev - multiplier * av);
107        }
108    }
109    Ok(KeltnerChannelsResult {
110        upper,
111        middle,
112        lower,
113    })
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_keltner_channels() {
122        let highs = vec![10.0; 20];
123        let lows = vec![8.0; 20];
124        let closes = vec![9.0; 20];
125        let result = keltner_channels(&highs, &lows, &closes, 10, 10, 2.0).unwrap();
126
127        assert_eq!(result.upper.len(), 20);
128        assert!(result.upper[8].is_none());
129        assert!(result.upper[9].is_some());
130    }
131}