wickra_core/indicators/
nrtr.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct NrtrOutput {
10 pub value: f64,
12 pub direction: f64,
14}
15
16#[derive(Debug, Clone)]
55pub struct Nrtr {
56 pct: f64,
57 direction: f64,
58 water: f64,
59 last: Option<NrtrOutput>,
60}
61
62impl Nrtr {
63 pub fn new(pct: f64) -> Result<Self> {
70 if !pct.is_finite() || pct <= 0.0 || pct >= 100.0 {
71 return Err(Error::InvalidParameter {
72 message: "NRTR percentage must be in (0, 100)",
73 });
74 }
75 Ok(Self {
76 pct,
77 direction: 0.0,
78 water: 0.0,
79 last: None,
80 })
81 }
82
83 pub const fn pct(&self) -> f64 {
85 self.pct
86 }
87
88 pub const fn value(&self) -> Option<NrtrOutput> {
90 self.last
91 }
92}
93
94impl Indicator for Nrtr {
95 type Input = Candle;
96 type Output = NrtrOutput;
97
98 #[inline]
99 fn update(&mut self, candle: Candle) -> Option<NrtrOutput> {
100 let close = candle.close;
101 let down = self.pct / 100.0;
102 let up = self.pct / 100.0;
103
104 if self.direction == 0.0 {
105 self.direction = 1.0;
106 self.water = close;
107 } else if self.direction > 0.0 {
108 self.water = self.water.max(close);
109 let line = self.water * (1.0 - down);
110 if close < line {
111 self.direction = -1.0;
112 self.water = close;
113 }
114 } else {
115 self.water = self.water.min(close);
116 let line = self.water * (1.0 + up);
117 if close > line {
118 self.direction = 1.0;
119 self.water = close;
120 }
121 }
122
123 let line = if self.direction > 0.0 {
124 self.water * (1.0 - down)
125 } else {
126 self.water * (1.0 + up)
127 };
128 let out = NrtrOutput {
129 value: line,
130 direction: self.direction,
131 };
132 self.last = Some(out);
133 Some(out)
134 }
135
136 fn reset(&mut self) {
137 self.direction = 0.0;
138 self.water = 0.0;
139 self.last = None;
140 }
141
142 #[inline]
143 fn warmup_period(&self) -> usize {
144 1
145 }
146
147 #[inline]
148 fn is_ready(&self) -> bool {
149 self.last.is_some()
150 }
151
152 #[inline]
153 fn name(&self) -> &'static str {
154 "Nrtr"
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::traits::BatchExt;
162
163 fn c(close: f64) -> Candle {
164 Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
165 }
166
167 #[test]
168 fn rejects_invalid_pct() {
169 assert!(matches!(
170 Nrtr::new(0.0),
171 Err(Error::InvalidParameter { .. })
172 ));
173 assert!(matches!(
174 Nrtr::new(100.0),
175 Err(Error::InvalidParameter { .. })
176 ));
177 assert!(matches!(
178 Nrtr::new(f64::NAN),
179 Err(Error::InvalidParameter { .. })
180 ));
181 assert!(Nrtr::new(2.0).is_ok());
182 }
183
184 #[test]
185 fn accessors_and_metadata() {
186 let n = Nrtr::new(2.0).unwrap();
187 assert_eq!(n.pct(), 2.0);
188 assert_eq!(n.warmup_period(), 1);
189 assert_eq!(n.name(), "Nrtr");
190 assert!(!n.is_ready());
191 assert_eq!(n.value(), None);
192 }
193
194 #[test]
195 fn first_bar_emits_up_line() {
196 let mut n = Nrtr::new(10.0).unwrap();
197 let o = n.update(c(100.0)).unwrap();
198 assert_eq!(o.direction, 1.0);
199 assert!((o.value - 90.0).abs() < 1e-9);
201 }
202
203 #[test]
204 fn uptrend_keeps_line_below_price() {
205 let mut n = Nrtr::new(5.0).unwrap();
206 let candles: Vec<Candle> = (0..40).map(|i| c(100.0 + f64::from(i))).collect();
207 for (o, candle) in n.batch(&candles).into_iter().zip(candles.iter()) {
208 let o = o.unwrap();
209 assert_eq!(o.direction, 1.0);
210 assert!(o.value < candle.close);
211 }
212 }
213
214 #[test]
215 fn reverses_on_retracement() {
216 let mut n = Nrtr::new(5.0).unwrap();
217 let mut candles: Vec<Candle> = (0..20).map(|i| c(100.0 + f64::from(i))).collect();
219 candles.extend((0..10).map(|i| c(119.0 - 3.0 * f64::from(i))));
220 let dirs: Vec<f64> = n
221 .batch(&candles)
222 .into_iter()
223 .flatten()
224 .map(|o| o.direction)
225 .collect();
226 assert!(dirs.iter().any(|&d| d > 0.0));
227 assert!(dirs.iter().any(|&d| d < 0.0));
228 }
229
230 #[test]
231 fn downtrend_keeps_line_above_price() {
232 let mut n = Nrtr::new(5.0).unwrap();
233 let mut candles = vec![c(100.0)];
235 candles.extend((0..30).map(|i| c(80.0 - f64::from(i))));
236 let out = n.batch(&candles);
237 let o = out.last().unwrap().unwrap();
238 let candle = candles.last().unwrap();
239 assert_eq!(o.direction, -1.0);
240 assert!(o.value > candle.close);
241 }
242
243 #[test]
244 fn reset_clears_state() {
245 let mut n = Nrtr::new(2.0).unwrap();
246 n.batch(&(0..20).map(|i| c(100.0 + f64::from(i))).collect::<Vec<_>>());
247 assert!(n.is_ready());
248 n.reset();
249 assert!(!n.is_ready());
250 assert_eq!(n.value(), None);
251 }
252
253 #[test]
254 fn batch_equals_streaming() {
255 let candles: Vec<Candle> = (0..120)
256 .map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 15.0))
257 .collect();
258 let batch = Nrtr::new(3.0).unwrap().batch(&candles);
259 let mut b = Nrtr::new(3.0).unwrap();
260 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
261 assert_eq!(batch, streamed);
262 }
263}