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> {
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<S: ::std::hash::BuildHasher>(params: &HashMap<String, String, S>) -> 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)
92 .map(|i| Candle {
93 time: i64::try_from(i).expect("time index fits i64"),
94 open: 10.0,
95 high: 12.0,
96 low: 8.0,
97 close: 10.0 + i as f64 * 0.1,
98 volume: 100.0,
99 })
100 .collect()
101 }
102
103 #[test]
104 fn elder_ray_two_columns() {
105 let out = ElderRayIndex::with_period(14)
106 .calculate(&candles(20))
107 .unwrap();
108 assert!(out.get("ElderRay_bull").is_some());
109 assert!(out.get("ElderRay_bear").is_some());
110 }
111
112 #[test]
113 fn bull_power_is_high_minus_ema() {
114 let out = ElderRayIndex::with_period(5)
116 .calculate(&candles(20))
117 .unwrap();
118 let bull = out.get("ElderRay_bull").unwrap();
119 let bear = out.get("ElderRay_bear").unwrap();
120 for i in 5..20 {
121 if !bull[i].is_nan() {
122 assert!(bull[i] >= bear[i], "bull < bear at {i}");
123 }
124 }
125 }
126
127 #[test]
128 fn factory_creates_elder_ray() {
129 assert_eq!(factory(&HashMap::new()).unwrap().name(), "ElderRayIndex");
130 }
131}