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 fn update(&mut self, candle: Candle) -> Option<NrtrOutput> {
99 let close = candle.close;
100 let down = self.pct / 100.0;
101 let up = self.pct / 100.0;
102
103 if self.direction == 0.0 {
104 self.direction = 1.0;
105 self.water = close;
106 } else if self.direction > 0.0 {
107 self.water = self.water.max(close);
108 let line = self.water * (1.0 - down);
109 if close < line {
110 self.direction = -1.0;
111 self.water = close;
112 }
113 } else {
114 self.water = self.water.min(close);
115 let line = self.water * (1.0 + up);
116 if close > line {
117 self.direction = 1.0;
118 self.water = close;
119 }
120 }
121
122 let line = if self.direction > 0.0 {
123 self.water * (1.0 - down)
124 } else {
125 self.water * (1.0 + up)
126 };
127 let out = NrtrOutput {
128 value: line,
129 direction: self.direction,
130 };
131 self.last = Some(out);
132 Some(out)
133 }
134
135 fn reset(&mut self) {
136 self.direction = 0.0;
137 self.water = 0.0;
138 self.last = None;
139 }
140
141 fn warmup_period(&self) -> usize {
142 1
143 }
144
145 fn is_ready(&self) -> bool {
146 self.last.is_some()
147 }
148
149 fn name(&self) -> &'static str {
150 "Nrtr"
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::traits::BatchExt;
158
159 fn c(close: f64) -> Candle {
160 Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
161 }
162
163 #[test]
164 fn rejects_invalid_pct() {
165 assert!(matches!(
166 Nrtr::new(0.0),
167 Err(Error::InvalidParameter { .. })
168 ));
169 assert!(matches!(
170 Nrtr::new(100.0),
171 Err(Error::InvalidParameter { .. })
172 ));
173 assert!(matches!(
174 Nrtr::new(f64::NAN),
175 Err(Error::InvalidParameter { .. })
176 ));
177 assert!(Nrtr::new(2.0).is_ok());
178 }
179
180 #[test]
181 fn accessors_and_metadata() {
182 let n = Nrtr::new(2.0).unwrap();
183 assert_eq!(n.pct(), 2.0);
184 assert_eq!(n.warmup_period(), 1);
185 assert_eq!(n.name(), "Nrtr");
186 assert!(!n.is_ready());
187 assert_eq!(n.value(), None);
188 }
189
190 #[test]
191 fn first_bar_emits_up_line() {
192 let mut n = Nrtr::new(10.0).unwrap();
193 let o = n.update(c(100.0)).unwrap();
194 assert_eq!(o.direction, 1.0);
195 assert!((o.value - 90.0).abs() < 1e-9);
197 }
198
199 #[test]
200 fn uptrend_keeps_line_below_price() {
201 let mut n = Nrtr::new(5.0).unwrap();
202 let candles: Vec<Candle> = (0..40).map(|i| c(100.0 + f64::from(i))).collect();
203 for (o, candle) in n.batch(&candles).into_iter().zip(candles.iter()) {
204 let o = o.unwrap();
205 assert_eq!(o.direction, 1.0);
206 assert!(o.value < candle.close);
207 }
208 }
209
210 #[test]
211 fn reverses_on_retracement() {
212 let mut n = Nrtr::new(5.0).unwrap();
213 let mut candles: Vec<Candle> = (0..20).map(|i| c(100.0 + f64::from(i))).collect();
215 candles.extend((0..10).map(|i| c(119.0 - 3.0 * f64::from(i))));
216 let dirs: Vec<f64> = n
217 .batch(&candles)
218 .into_iter()
219 .flatten()
220 .map(|o| o.direction)
221 .collect();
222 assert!(dirs.iter().any(|&d| d > 0.0));
223 assert!(dirs.iter().any(|&d| d < 0.0));
224 }
225
226 #[test]
227 fn downtrend_keeps_line_above_price() {
228 let mut n = Nrtr::new(5.0).unwrap();
229 let mut candles = vec![c(100.0)];
231 candles.extend((0..30).map(|i| c(80.0 - f64::from(i))));
232 let out = n.batch(&candles);
233 let o = out.last().unwrap().unwrap();
234 let candle = candles.last().unwrap();
235 assert_eq!(o.direction, -1.0);
236 assert!(o.value > candle.close);
237 }
238
239 #[test]
240 fn reset_clears_state() {
241 let mut n = Nrtr::new(2.0).unwrap();
242 n.batch(&(0..20).map(|i| c(100.0 + f64::from(i))).collect::<Vec<_>>());
243 assert!(n.is_ready());
244 n.reset();
245 assert!(!n.is_ready());
246 assert_eq!(n.value(), None);
247 }
248
249 #[test]
250 fn batch_equals_streaming() {
251 let candles: Vec<Candle> = (0..120)
252 .map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 15.0))
253 .collect();
254 let batch = Nrtr::new(3.0).unwrap().batch(&candles);
255 let mut b = Nrtr::new(3.0).unwrap();
256 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
257 assert_eq!(batch, streamed);
258 }
259}