indicators/volatility/
market_cycle.rs1use std::collections::HashMap;
17
18use crate::error::IndicatorError;
19use crate::indicator::{Indicator, IndicatorOutput};
20use crate::registry::param_usize;
21use crate::types::Candle;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum CyclePhase {
26 Markup = 1,
27 Markdown = -1,
28 Plateau = 0,
29 Accumulation = 2, Distribution = -2,
31}
32
33impl CyclePhase {
34 pub fn as_f64(self) -> f64 {
35 self as i32 as f64
36 }
37}
38
39#[derive(Debug, Clone)]
40pub struct MarketCycleParams {
41 pub momentum_period: usize,
43}
44impl Default for MarketCycleParams {
45 fn default() -> Self {
46 Self { momentum_period: 1 }
47 }
48}
49
50#[derive(Debug, Clone)]
51pub struct MarketCycle {
52 pub params: MarketCycleParams,
53}
54
55impl MarketCycle {
56 pub fn new(params: MarketCycleParams) -> Self {
57 Self { params }
58 }
59}
60
61impl Default for MarketCycle {
62 fn default() -> Self {
63 Self::new(MarketCycleParams::default())
64 }
65}
66
67impl Indicator for MarketCycle {
68 fn name(&self) -> &'static str {
69 "MarketCycle"
70 }
71 fn required_len(&self) -> usize {
72 self.params.momentum_period + 1
73 }
74 fn required_columns(&self) -> &[&'static str] {
75 &["close"]
76 }
77
78 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
81 self.check_len(candles)?;
82
83 let close: Vec<f64> = candles.iter().map(|c| c.close).collect();
84 let mp = self.params.momentum_period;
85 let n = close.len();
86
87 let mut phases = vec![CyclePhase::Plateau; n];
89 for i in mp..n {
90 let momentum = close[i] - close[i - mp];
91 phases[i] = if momentum > 0.0 {
92 CyclePhase::Markup
93 } else if momentum < 0.0 {
94 CyclePhase::Markdown
95 } else {
96 CyclePhase::Plateau
97 };
98 }
99
100 let mut result = phases.clone();
110 for i in 1..n {
111 match (result[i - 1], phases[i]) {
112 (CyclePhase::Markdown, p) if p != CyclePhase::Markdown => {
113 result[i] = CyclePhase::Accumulation;
114 }
115 (CyclePhase::Markup, p) if p != CyclePhase::Markup => {
116 result[i] = CyclePhase::Distribution;
117 }
118 _ => {}
119 }
120 }
121
122 let values: Vec<f64> = result.iter().map(|p| p.as_f64()).collect();
123
124 Ok(IndicatorOutput::from_pairs([(
125 "MarketCycle".to_string(),
126 values,
127 )]))
128 }
129}
130
131pub fn factory<S: ::std::hash::BuildHasher>(
132 params: &HashMap<String, String, S>,
133) -> Result<Box<dyn Indicator>, IndicatorError> {
134 Ok(Box::new(MarketCycle::new(MarketCycleParams {
135 momentum_period: param_usize(params, "momentum_period", 1)?,
136 })))
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn candles(closes: &[f64]) -> Vec<Candle> {
144 closes
145 .iter()
146 .enumerate()
147 .map(|(i, &c)| Candle {
148 time: i64::try_from(i).expect("time index fits i64"),
149 open: c,
150 high: c,
151 low: c,
152 close: c,
153 volume: 1.0,
154 })
155 .collect()
156 }
157
158 #[test]
159 fn market_cycle_output_column() {
160 let out = MarketCycle::default()
161 .calculate(&candles(&[1.0, 2.0, 3.0]))
162 .unwrap();
163 assert!(out.get("MarketCycle").is_some());
164 }
165
166 #[test]
167 fn rising_prices_give_markup() {
168 let closes = vec![1.0, 2.0, 3.0, 4.0, 5.0];
169 let out = MarketCycle::default().calculate(&candles(&closes)).unwrap();
170 let vals = out.get("MarketCycle").unwrap();
171 assert_eq!(vals[1], CyclePhase::Markup.as_f64());
173 }
174
175 #[test]
176 fn falling_after_rising_gives_distribution() {
177 let closes = vec![1.0, 2.0, 3.0, 2.0];
179 let out = MarketCycle::default().calculate(&candles(&closes)).unwrap();
180 let vals = out.get("MarketCycle").unwrap();
181 assert_eq!(vals[3], CyclePhase::Distribution.as_f64());
182 }
183
184 #[test]
185 fn factory_creates_market_cycle() {
186 assert_eq!(factory(&HashMap::new()).unwrap().name(), "MarketCycle");
187 }
188}