wickra_core/indicators/
natr.rs1use crate::error::Result;
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7use super::Atr;
8
9#[derive(Debug, Clone)]
40pub struct Natr {
41 atr: Atr,
42 last: Option<f64>,
43}
44
45impl Natr {
46 pub fn new(period: usize) -> Result<Self> {
52 Ok(Self {
53 atr: Atr::new(period)?,
54 last: None,
55 })
56 }
57
58 pub const fn period(&self) -> usize {
60 self.atr.period()
61 }
62
63 pub const fn value(&self) -> Option<f64> {
65 self.last
66 }
67}
68
69impl Indicator for Natr {
70 type Input = Candle;
71 type Output = f64;
72
73 #[inline]
74 fn update(&mut self, candle: Candle) -> Option<f64> {
75 let atr = self.atr.update(candle)?;
76 let natr = if candle.close == 0.0 {
77 0.0
79 } else {
80 100.0 * atr / candle.close
81 };
82 self.last = Some(natr);
83 Some(natr)
84 }
85
86 fn reset(&mut self) {
87 self.atr.reset();
88 self.last = None;
89 }
90
91 #[inline]
92 fn warmup_period(&self) -> usize {
93 self.atr.warmup_period()
94 }
95
96 #[inline]
97 fn is_ready(&self) -> bool {
98 self.last.is_some()
99 }
100
101 #[inline]
102 fn name(&self) -> &'static str {
103 "NATR"
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::traits::BatchExt;
111 use approx::assert_relative_eq;
112
113 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
114 Candle::new(open, high, low, close, 1.0, ts).unwrap()
115 }
116
117 #[test]
118 fn new_rejects_zero_period() {
119 assert!(Natr::new(0).is_err());
120 }
121
122 #[test]
123 fn warmup_period_matches_atr() {
124 let natr = Natr::new(14).unwrap();
125 assert_eq!(natr.warmup_period(), 14);
126 }
127
128 #[test]
132 fn accessors_and_metadata() {
133 let mut natr = Natr::new(14).unwrap();
134 assert_eq!(natr.period(), 14);
135 assert_eq!(natr.name(), "NATR");
136 assert_eq!(natr.value(), None);
137 let candles: Vec<Candle> = (0..14)
138 .map(|i| candle(100.0, 102.0, 98.0, 101.0, i))
139 .collect();
140 for c in &candles {
141 natr.update(*c);
142 }
143 assert!(natr.value().is_some());
144 }
145
146 #[test]
153 fn zero_close_yields_zero_natr() {
154 let candles: Vec<Candle> = (0..15).map(|i| candle(0.0, 0.0, 0.0, 0.0, i)).collect();
155 let mut natr = Natr::new(5).unwrap();
156 let out = natr.batch(&candles);
157 let last = out.into_iter().flatten().last().expect("emits");
158 assert_eq!(last, 0.0);
159 }
160
161 #[test]
162 fn natr_is_atr_over_close_as_percent() {
163 let candles: Vec<Candle> = (0..60)
165 .map(|i| {
166 let mid = 100.0 + (i as f64 * 0.3).sin() * 10.0;
167 candle(mid, mid + 3.0, mid - 3.0, mid + 1.0, i)
168 })
169 .collect();
170 let natr_out = Natr::new(14).unwrap().batch(&candles);
171 let atr_out = Atr::new(14).unwrap().batch(&candles);
172 for (i, (n, a)) in natr_out.iter().zip(atr_out.iter()).enumerate() {
173 assert_eq!(n.is_some(), a.is_some(), "warmup mismatch at index {i}");
175 if let (Some(nv), Some(av)) = (n, a) {
176 let want = 100.0 * av / candles[i].close;
177 assert_relative_eq!(*nv, want, epsilon = 1e-9);
178 }
179 }
180 }
181
182 #[test]
183 fn flat_market_yields_zero() {
184 let mut natr = Natr::new(5).unwrap();
186 let candles: Vec<Candle> = (0..30)
187 .map(|i| candle(100.0, 100.0, 100.0, 100.0, i))
188 .collect();
189 for v in natr.batch(&candles).into_iter().flatten() {
190 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
191 }
192 }
193
194 #[test]
195 fn reset_clears_state() {
196 let mut natr = Natr::new(5).unwrap();
197 let candles: Vec<Candle> = (0..20)
198 .map(|i| candle(100.0, 102.0, 98.0, 101.0, i))
199 .collect();
200 natr.batch(&candles);
201 assert!(natr.is_ready());
202 natr.reset();
203 assert!(!natr.is_ready());
204 assert_eq!(natr.update(candles[0]), None);
205 }
206
207 #[test]
208 fn batch_equals_streaming() {
209 let candles: Vec<Candle> = (0..80)
210 .map(|i| {
211 let mid = 100.0 + (i as f64 * 0.35).sin() * 9.0;
212 candle(mid, mid + 2.5, mid - 2.5, mid + 0.5, i)
213 })
214 .collect();
215 let batch = Natr::new(14).unwrap().batch(&candles);
216 let mut b = Natr::new(14).unwrap();
217 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
218 assert_eq!(batch, streamed);
219 }
220}