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 fn update(&mut self, candle: Candle) -> Option<f64> {
74 let atr = self.atr.update(candle)?;
75 let natr = if candle.close == 0.0 {
76 0.0
78 } else {
79 100.0 * atr / candle.close
80 };
81 self.last = Some(natr);
82 Some(natr)
83 }
84
85 fn reset(&mut self) {
86 self.atr.reset();
87 self.last = None;
88 }
89
90 fn warmup_period(&self) -> usize {
91 self.atr.warmup_period()
92 }
93
94 fn is_ready(&self) -> bool {
95 self.last.is_some()
96 }
97
98 fn name(&self) -> &'static str {
99 "NATR"
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::traits::BatchExt;
107 use approx::assert_relative_eq;
108
109 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
110 Candle::new(open, high, low, close, 1.0, ts).unwrap()
111 }
112
113 #[test]
114 fn new_rejects_zero_period() {
115 assert!(Natr::new(0).is_err());
116 }
117
118 #[test]
119 fn warmup_period_matches_atr() {
120 let natr = Natr::new(14).unwrap();
121 assert_eq!(natr.warmup_period(), 14);
122 }
123
124 #[test]
128 fn accessors_and_metadata() {
129 let mut natr = Natr::new(14).unwrap();
130 assert_eq!(natr.period(), 14);
131 assert_eq!(natr.name(), "NATR");
132 assert_eq!(natr.value(), None);
133 let candles: Vec<Candle> = (0..14)
134 .map(|i| candle(100.0, 102.0, 98.0, 101.0, i))
135 .collect();
136 for c in &candles {
137 natr.update(*c);
138 }
139 assert!(natr.value().is_some());
140 }
141
142 #[test]
149 fn zero_close_yields_zero_natr() {
150 let candles: Vec<Candle> = (0..15).map(|i| candle(0.0, 0.0, 0.0, 0.0, i)).collect();
151 let mut natr = Natr::new(5).unwrap();
152 let out = natr.batch(&candles);
153 let last = out.into_iter().flatten().last().expect("emits");
154 assert_eq!(last, 0.0);
155 }
156
157 #[test]
158 fn natr_is_atr_over_close_as_percent() {
159 let candles: Vec<Candle> = (0..60)
161 .map(|i| {
162 let mid = 100.0 + (i as f64 * 0.3).sin() * 10.0;
163 candle(mid, mid + 3.0, mid - 3.0, mid + 1.0, i)
164 })
165 .collect();
166 let natr_out = Natr::new(14).unwrap().batch(&candles);
167 let atr_out = Atr::new(14).unwrap().batch(&candles);
168 for (i, (n, a)) in natr_out.iter().zip(atr_out.iter()).enumerate() {
169 assert_eq!(n.is_some(), a.is_some(), "warmup mismatch at index {i}");
171 if let (Some(nv), Some(av)) = (n, a) {
172 let want = 100.0 * av / candles[i].close;
173 assert_relative_eq!(*nv, want, epsilon = 1e-9);
174 }
175 }
176 }
177
178 #[test]
179 fn flat_market_yields_zero() {
180 let mut natr = Natr::new(5).unwrap();
182 let candles: Vec<Candle> = (0..30)
183 .map(|i| candle(100.0, 100.0, 100.0, 100.0, i))
184 .collect();
185 for v in natr.batch(&candles).into_iter().flatten() {
186 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
187 }
188 }
189
190 #[test]
191 fn reset_clears_state() {
192 let mut natr = Natr::new(5).unwrap();
193 let candles: Vec<Candle> = (0..20)
194 .map(|i| candle(100.0, 102.0, 98.0, 101.0, i))
195 .collect();
196 natr.batch(&candles);
197 assert!(natr.is_ready());
198 natr.reset();
199 assert!(!natr.is_ready());
200 assert_eq!(natr.update(candles[0]), None);
201 }
202
203 #[test]
204 fn batch_equals_streaming() {
205 let candles: Vec<Candle> = (0..80)
206 .map(|i| {
207 let mid = 100.0 + (i as f64 * 0.35).sin() * 9.0;
208 candle(mid, mid + 2.5, mid - 2.5, mid + 0.5, i)
209 })
210 .collect();
211 let batch = Natr::new(14).unwrap().batch(&candles);
212 let mut b = Natr::new(14).unwrap();
213 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
214 assert_eq!(batch, streamed);
215 }
216}