179 lines
5.3 KiB
Rust
179 lines
5.3 KiB
Rust
//! Volatility indicators: ATR, Bollinger Bands.
|
|
|
|
use crate::core::error::RaptorError;
|
|
use crate::core::Result;
|
|
|
|
/// Average True Range (ATR).
|
|
///
|
|
/// # Arguments
|
|
/// * `high` - High prices
|
|
/// * `low` - Low prices
|
|
/// * `close` - Close prices
|
|
/// * `period` - Lookback period (default: 14)
|
|
///
|
|
/// # Returns
|
|
/// Vector of ATR values (NaN for warmup period)
|
|
pub fn atr(high: &[f64], low: &[f64], close: &[f64], period: usize) -> Result<Vec<f64>> {
|
|
let n = close.len();
|
|
if n != high.len() || n != low.len() {
|
|
return Err(RaptorError::length_mismatch(n, high.len()));
|
|
}
|
|
if period == 0 {
|
|
return Err(RaptorError::invalid_parameter("ATR period must be > 0"));
|
|
}
|
|
Ok(ferro_ta_core::volatility::atr(high, low, close, period))
|
|
}
|
|
|
|
/// True Range calculation (single bar).
|
|
#[inline]
|
|
pub fn true_range(high: f64, low: f64, prev_close: f64) -> f64 {
|
|
let hl = high - low;
|
|
let hc = (high - prev_close).abs();
|
|
let lc = (low - prev_close).abs();
|
|
hl.max(hc).max(lc)
|
|
}
|
|
|
|
/// Bollinger Bands result.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BollingerBandsResult {
|
|
/// Middle band (SMA).
|
|
pub middle: Vec<f64>,
|
|
/// Upper band (SMA + std_dev * multiplier).
|
|
pub upper: Vec<f64>,
|
|
/// Lower band (SMA - std_dev * multiplier).
|
|
pub lower: Vec<f64>,
|
|
/// Bandwidth: (upper - lower) / middle.
|
|
pub bandwidth: Vec<f64>,
|
|
/// %B: (price - lower) / (upper - lower).
|
|
pub percent_b: Vec<f64>,
|
|
}
|
|
|
|
/// Bollinger Bands.
|
|
///
|
|
/// # Arguments
|
|
/// * `data` - Price data (typically close prices)
|
|
/// * `period` - Lookback period (default: 20)
|
|
/// * `std_dev` - Standard deviation multiplier (default: 2.0)
|
|
///
|
|
/// # Returns
|
|
/// BollingerBandsResult with middle, upper, lower bands, bandwidth, and %B
|
|
pub fn bollinger_bands(data: &[f64], period: usize, std_dev: f64) -> Result<BollingerBandsResult> {
|
|
if period == 0 {
|
|
return Err(RaptorError::invalid_parameter("Bollinger Bands period must be > 0"));
|
|
}
|
|
if std_dev <= 0.0 {
|
|
return Err(RaptorError::invalid_parameter("Bollinger Bands std_dev must be > 0"));
|
|
}
|
|
|
|
let n = data.len();
|
|
let (upper, middle, lower) = ferro_ta_core::overlap::bbands(data, period, std_dev, std_dev);
|
|
|
|
let mut bandwidth = vec![f64::NAN; n];
|
|
let mut percent_b = vec![f64::NAN; n];
|
|
|
|
for i in 0..n {
|
|
if !middle[i].is_nan() && middle[i].abs() > f64::EPSILON {
|
|
bandwidth[i] = (upper[i] - lower[i]) / middle[i].abs();
|
|
}
|
|
let band_width = upper[i] - lower[i];
|
|
if band_width > f64::EPSILON {
|
|
percent_b[i] = (data[i] - lower[i]) / band_width;
|
|
}
|
|
}
|
|
|
|
Ok(BollingerBandsResult { middle, upper, lower, bandwidth, percent_b })
|
|
}
|
|
|
|
/// Keltner Channels (ATR-based bands).
|
|
///
|
|
/// # Arguments
|
|
/// * `high` - High prices
|
|
/// * `low` - Low prices
|
|
/// * `close` - Close prices
|
|
/// * `ema_period` - EMA period for middle band
|
|
/// * `atr_period` - ATR period
|
|
/// * `multiplier` - ATR multiplier
|
|
///
|
|
/// # Returns
|
|
/// Tuple of (middle, upper, lower) bands
|
|
pub fn keltner_channels(
|
|
high: &[f64],
|
|
low: &[f64],
|
|
close: &[f64],
|
|
ema_period: usize,
|
|
atr_period: usize,
|
|
multiplier: f64,
|
|
) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>)> {
|
|
let n = close.len();
|
|
if n != high.len() || n != low.len() {
|
|
return Err(RaptorError::length_mismatch(n, high.len()));
|
|
}
|
|
|
|
// Calculate EMA for middle band
|
|
let middle = super::trend::ema(close, ema_period)?;
|
|
|
|
// Calculate ATR
|
|
let atr_values = atr(high, low, close, atr_period)?;
|
|
|
|
// Calculate bands
|
|
let mut upper = vec![f64::NAN; n];
|
|
let mut lower = vec![f64::NAN; n];
|
|
|
|
for i in 0..n {
|
|
if !middle[i].is_nan() && !atr_values[i].is_nan() {
|
|
upper[i] = middle[i] + multiplier * atr_values[i];
|
|
lower[i] = middle[i] - multiplier * atr_values[i];
|
|
}
|
|
}
|
|
|
|
Ok((middle, upper, lower))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_atr() {
|
|
let high = vec![50.0, 51.0, 52.0, 51.5, 50.5, 51.0, 52.0, 53.0, 52.5, 51.5];
|
|
let low = vec![48.0, 49.0, 50.0, 49.5, 48.5, 49.0, 50.0, 51.0, 50.5, 49.5];
|
|
let close = vec![49.0, 50.0, 51.0, 50.0, 49.0, 50.0, 51.0, 52.0, 51.0, 50.0];
|
|
|
|
let result = atr(&high, &low, &close, 5).unwrap();
|
|
|
|
// ATR should be valid from index 4
|
|
assert!(result[3].is_nan());
|
|
assert!(!result[4].is_nan());
|
|
assert!(result[4] > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bollinger_bands() {
|
|
let data: Vec<f64> = (1..=30).map(|x| x as f64 + (x as f64 * 0.1).sin()).collect();
|
|
|
|
let result = bollinger_bands(&data, 20, 2.0).unwrap();
|
|
|
|
// Bands should be valid from index 19
|
|
assert!(result.middle[18].is_nan());
|
|
assert!(!result.middle[19].is_nan());
|
|
|
|
// Upper > Middle > Lower
|
|
assert!(result.upper[19] > result.middle[19]);
|
|
assert!(result.middle[19] > result.lower[19]);
|
|
|
|
// %B should be between 0 and 1 for data within bands
|
|
assert!(result.percent_b[19] >= -0.5 && result.percent_b[19] <= 1.5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_true_range() {
|
|
// Simple case
|
|
assert!((true_range(52.0, 48.0, 50.0) - 4.0).abs() < 1e-10);
|
|
|
|
// Gap up case
|
|
assert!((true_range(55.0, 53.0, 50.0) - 5.0).abs() < 1e-10);
|
|
|
|
// Gap down case
|
|
assert!((true_range(48.0, 45.0, 50.0) - 5.0).abs() < 1e-10);
|
|
}
|
|
} |