indicators/volatility/
elder_ray_index.rs1use std::collections::HashMap;
15
16use crate::error::IndicatorError;
17use crate::functions::{self};
18use crate::indicator::{Indicator, IndicatorOutput};
19use crate::registry::param_usize;
20use crate::types::Candle;
21
22#[derive(Debug, Clone)]
23pub struct ElderRayParams {
24 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) -> &'static 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 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
70 self.check_len(candles)?;
71
72 let close: Vec<f64> = candles.iter().map(|c| c.close).collect();
73 let high: Vec<f64> = candles.iter().map(|c| c.high).collect();
74 let low: Vec<f64> = candles.iter().map(|c| c.low).collect();
75
76 let ema = functions::ema_nan_aware(&close, self.params.fast_period)?;
80
81 let bull: Vec<f64> = high.iter().zip(&ema).map(|(&h, &e)| h - e).collect();
82 let bear: Vec<f64> = low.iter().zip(&ema).map(|(&l, &e)| l - e).collect();
83
84 Ok(IndicatorOutput::from_pairs([
85 ("ElderRay_bull".to_string(), bull),
86 ("ElderRay_bear".to_string(), bear),
87 ]))
88 }
89}
90
91pub fn factory<S: ::std::hash::BuildHasher>(
92 params: &HashMap<String, String, S>,
93) -> Result<Box<dyn Indicator>, IndicatorError> {
94 Ok(Box::new(ElderRayIndex::new(ElderRayParams {
95 fast_period: param_usize(params, "fast_period", 14)?,
96 })))
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn candles(n: usize) -> Vec<Candle> {
104 (0..n)
105 .map(|i| Candle {
106 time: i64::try_from(i).expect("time index fits i64"),
107 open: 10.0,
108 high: 12.0,
109 low: 8.0,
110 close: 10.0 + i as f64 * 0.1,
111 volume: 100.0,
112 })
113 .collect()
114 }
115
116 #[test]
117 fn elder_ray_two_columns() {
118 let out = ElderRayIndex::with_period(14)
119 .calculate(&candles(20))
120 .unwrap();
121 assert!(out.get("ElderRay_bull").is_some());
122 assert!(out.get("ElderRay_bear").is_some());
123 }
124
125 #[test]
126 fn bull_power_is_high_minus_ema() {
127 let out = ElderRayIndex::with_period(5)
129 .calculate(&candles(20))
130 .unwrap();
131 let bull = out.get("ElderRay_bull").unwrap();
132 let bear = out.get("ElderRay_bear").unwrap();
133 for i in 5..20 {
134 if !bull[i].is_nan() {
135 assert!(bull[i] >= bear[i], "bull < bear at {i}");
136 }
137 }
138 }
139
140 #[test]
141 fn factory_creates_elder_ray() {
142 assert_eq!(factory(&HashMap::new()).unwrap().name(), "ElderRayIndex");
143 }
144}