1use crate::error::{Error, Result};
4use crate::indicators::atr::Atr;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct AtrRatchetOutput {
11 pub value: f64,
13 pub direction: f64,
15}
16
17#[derive(Debug, Clone)]
51pub struct AtrRatchet {
52 atr: Atr,
53 atr_period: usize,
54 start_mult: f64,
55 increment: f64,
56 direction: f64,
57 stop: f64,
58 last: Option<AtrRatchetOutput>,
59}
60
61impl AtrRatchet {
62 pub fn new(atr_period: usize, start_mult: f64, increment: f64) -> Result<Self> {
70 if !start_mult.is_finite()
71 || start_mult <= 0.0
72 || !increment.is_finite()
73 || increment <= 0.0
74 {
75 return Err(Error::NonPositiveMultiplier);
76 }
77 Ok(Self {
78 atr: Atr::new(atr_period)?,
79 atr_period,
80 start_mult,
81 increment,
82 direction: 0.0,
83 stop: 0.0,
84 last: None,
85 })
86 }
87
88 pub const fn params(&self) -> (usize, f64, f64) {
90 (self.atr_period, self.start_mult, self.increment)
91 }
92
93 pub const fn value(&self) -> Option<AtrRatchetOutput> {
95 self.last
96 }
97}
98
99impl Indicator for AtrRatchet {
100 type Input = Candle;
101 type Output = AtrRatchetOutput;
102
103 #[inline]
104 fn update(&mut self, candle: Candle) -> Option<AtrRatchetOutput> {
105 let atr = self.atr.update(candle)?;
106 let close = candle.close;
107
108 if self.direction == 0.0 {
109 self.direction = 1.0;
110 self.stop = close - self.start_mult * atr;
111 } else if self.direction > 0.0 {
112 self.stop += self.increment * atr;
113 if close < self.stop {
114 self.direction = -1.0;
115 self.stop = close + self.start_mult * atr;
116 }
117 } else {
118 self.stop -= self.increment * atr;
119 if close > self.stop {
120 self.direction = 1.0;
121 self.stop = close - self.start_mult * atr;
122 }
123 }
124
125 let out = AtrRatchetOutput {
126 value: self.stop,
127 direction: self.direction,
128 };
129 self.last = Some(out);
130 Some(out)
131 }
132
133 fn reset(&mut self) {
134 self.atr.reset();
135 self.direction = 0.0;
136 self.stop = 0.0;
137 self.last = None;
138 }
139
140 #[inline]
141 fn warmup_period(&self) -> usize {
142 self.atr_period
143 }
144
145 #[inline]
146 fn is_ready(&self) -> bool {
147 self.last.is_some()
148 }
149
150 #[inline]
151 fn name(&self) -> &'static str {
152 "AtrRatchet"
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::traits::BatchExt;
160
161 fn c(high: f64, low: f64, close: f64) -> Candle {
162 Candle::new_unchecked(f64::midpoint(high, low), high, low, close, 1_000.0, 0)
163 }
164
165 #[test]
166 fn rejects_invalid_params() {
167 assert!(matches!(
168 AtrRatchet::new(0, 4.0, 0.1),
169 Err(Error::PeriodZero)
170 ));
171 assert!(matches!(
172 AtrRatchet::new(14, 0.0, 0.1),
173 Err(Error::NonPositiveMultiplier)
174 ));
175 assert!(matches!(
176 AtrRatchet::new(14, 4.0, 0.0),
177 Err(Error::NonPositiveMultiplier)
178 ));
179 assert!(matches!(
180 AtrRatchet::new(14, 4.0, f64::NAN),
181 Err(Error::NonPositiveMultiplier)
182 ));
183 }
184
185 #[test]
186 fn accessors_and_metadata() {
187 let r = AtrRatchet::new(14, 4.0, 0.1).unwrap();
188 assert_eq!(r.params(), (14, 4.0, 0.1));
189 assert_eq!(r.warmup_period(), 14);
190 assert_eq!(r.name(), "AtrRatchet");
191 assert!(!r.is_ready());
192 assert_eq!(r.value(), None);
193 }
194
195 #[test]
196 fn first_emission_at_warmup_period() {
197 let mut r = AtrRatchet::new(5, 4.0, 0.1).unwrap();
198 let candles: Vec<Candle> = (0..12)
199 .map(|i| {
200 let base = 100.0 + f64::from(i);
201 c(base + 1.0, base - 1.0, base)
202 })
203 .collect();
204 let out = r.batch(&candles);
205 for v in out.iter().take(4) {
206 assert!(v.is_none());
207 }
208 assert!(out[4].is_some());
209 }
210
211 #[test]
212 fn uptrend_keeps_stop_below_price() {
213 let mut r = AtrRatchet::new(5, 4.0, 0.05).unwrap();
214 let candles: Vec<Candle> = (0..60)
215 .map(|i| {
216 let base = 100.0 + 2.0 * f64::from(i);
217 c(base + 1.0, base - 1.0, base + 0.5)
218 })
219 .collect();
220 for (o, candle) in r.batch(&candles).into_iter().zip(candles.iter()) {
221 if let Some(o) = o {
222 assert_eq!(o.direction, 1.0);
223 assert!(o.value < candle.close);
224 }
225 }
226 }
227
228 #[test]
229 fn stall_eventually_triggers_flip() {
230 let mut r = AtrRatchet::new(5, 2.0, 0.5).unwrap();
233 let mut candles: Vec<Candle> = (0..20)
234 .map(|i| {
235 let base = 100.0 + f64::from(i);
236 c(base + 1.0, base - 1.0, base + 0.5)
237 })
238 .collect();
239 candles.extend((0..40).map(|_| c(120.6, 118.6, 119.5)));
241 let dirs: Vec<f64> = r
242 .batch(&candles)
243 .into_iter()
244 .flatten()
245 .map(|o| o.direction)
246 .collect();
247 assert!(
248 dirs.iter().any(|&d| d < 0.0),
249 "the ratchet should eventually flip short"
250 );
251 }
252
253 #[test]
254 fn reset_clears_state() {
255 let mut r = AtrRatchet::new(5, 4.0, 0.1).unwrap();
256 let candles: Vec<Candle> = (0..40)
257 .map(|i| {
258 let base = 100.0 + f64::from(i);
259 c(base + 1.0, base - 1.0, base + 0.5)
260 })
261 .collect();
262 r.batch(&candles);
263 assert!(r.is_ready());
264 r.reset();
265 assert!(!r.is_ready());
266 assert_eq!(r.value(), None);
267 assert_eq!(r.update(candles[0]), None);
268 }
269
270 #[test]
271 fn batch_equals_streaming() {
272 let candles: Vec<Candle> = (0..120)
273 .map(|i| {
274 let base = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
275 c(base + 2.0, base - 1.5, base + 0.5)
276 })
277 .collect();
278 let batch = AtrRatchet::new(14, 4.0, 0.1).unwrap().batch(&candles);
279 let mut b = AtrRatchet::new(14, 4.0, 0.1).unwrap();
280 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
281 assert_eq!(batch, streamed);
282 }
283}