Skip to main content

indicators/volatility/
elder_ray_index.rs

1//! Elder Ray Index.
2//!
3//! Python source: `indicators/other/elder_ray_index.py :: class ElderRayIndexIndicator`
4//!
5//! # Python algorithm (to port)
6//! ```python
7//! ema        = data["Close"].ewm(span=self.fast_period, adjust=False).mean()
8//! bull_power = data["High"] - ema
9//! bear_power = data["Low"]  - ema
10//! ```
11//!
12//! Output columns: `"ElderRay_bull"`, `"ElderRay_bear"`.
13
14use std::collections::HashMap;
15
16use crate::functions::{self};
17use crate::error::IndicatorError;
18use crate::indicator::{Indicator, IndicatorOutput};
19use crate::registry::param_usize;
20use crate::types::Candle;
21
22#[derive(Debug, Clone)]
23pub struct ElderRayParams {
24    /// EMA period for the base line.  Python default: 14.
25    pub fast_period: usize,
26}
27impl Default for ElderRayParams {
28    fn default() -> Self {
29        Self { fast_period: 14 }
30    }
31}
32
33#[derive(Debug, Clone)]
34pub struct ElderRayIndex {
35    pub params: ElderRayParams,
36}
37
38impl ElderRayIndex {
39    pub fn new(params: ElderRayParams) -> Self {
40        Self { params }
41    }
42    pub fn with_period(period: usize) -> Self {
43        Self::new(ElderRayParams {
44            fast_period: period,
45        })
46    }
47}
48
49impl Indicator for ElderRayIndex {
50    fn name(&self) -> &str {
51        "ElderRayIndex"
52    }
53    fn required_len(&self) -> usize {
54        self.params.fast_period
55    }
56    fn required_columns(&self) -> &[&'static str] {
57        &["high", "low", "close"]
58    }
59
60    /// TODO: port Python EMA-based bull/bear power.
61    fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
62        self.check_len(candles)?;
63
64        let close: Vec<f64> = candles.iter().map(|c| c.close).collect();
65        let high: Vec<f64> = candles.iter().map(|c| c.high).collect();
66        let low: Vec<f64> = candles.iter().map(|c| c.low).collect();
67
68        let ema = functions::ema(&close, self.params.fast_period)?;
69
70        let bull: Vec<f64> = high.iter().zip(&ema).map(|(&h, &e)| h - e).collect();
71        let bear: Vec<f64> = low.iter().zip(&ema).map(|(&l, &e)| l - e).collect();
72
73        Ok(IndicatorOutput::from_pairs([
74            ("ElderRay_bull".to_string(), bull),
75            ("ElderRay_bear".to_string(), bear),
76        ]))
77    }
78}
79
80pub fn factory(params: &HashMap<String, String>) -> Result<Box<dyn Indicator>, IndicatorError> {
81    Ok(Box::new(ElderRayIndex::new(ElderRayParams {
82        fast_period: param_usize(params, "fast_period", 14)?,
83    })))
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    fn candles(n: usize) -> Vec<Candle> {
91        (0..n).map(|i| Candle {
92            time: i as i64, open: 10.0, high: 12.0, low: 8.0,
93            close: 10.0 + i as f64 * 0.1, volume: 100.0,
94        }).collect()
95    }
96
97    #[test]
98    fn elder_ray_two_columns() {
99        let out = ElderRayIndex::with_period(14).calculate(&candles(20)).unwrap();
100        assert!(out.get("ElderRay_bull").is_some());
101        assert!(out.get("ElderRay_bear").is_some());
102    }
103
104    #[test]
105    fn bull_power_is_high_minus_ema() {
106        // Bull power must always be >= bear power (high >= low).
107        let out = ElderRayIndex::with_period(5).calculate(&candles(20)).unwrap();
108        let bull = out.get("ElderRay_bull").unwrap();
109        let bear = out.get("ElderRay_bear").unwrap();
110        for i in 5..20 {
111            if !bull[i].is_nan() {
112                assert!(bull[i] >= bear[i], "bull < bear at {i}");
113            }
114        }
115    }
116
117    #[test]
118    fn factory_creates_elder_ray() {
119        assert_eq!(factory(&HashMap::new()).unwrap().name(), "ElderRayIndex");
120    }
121}