wickra_core/indicators/
new_price_lines.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
43pub struct NewPriceLines {
44 count: usize,
45 prev_close: Option<f64>,
46 consec_up: usize,
47 consec_down: usize,
48 last: Option<f64>,
49}
50
51impl NewPriceLines {
52 pub fn new(count: usize) -> Result<Self> {
59 if count < 2 {
60 return Err(Error::InvalidPeriod {
61 message: "new price lines count must be >= 2",
62 });
63 }
64 if count > crate::error::MAX_PERIOD {
65 return Err(Error::InvalidPeriod {
66 message: crate::error::PERIOD_ABOVE_MAX,
67 });
68 }
69 Ok(Self {
70 count,
71 prev_close: None,
72 consec_up: 0,
73 consec_down: 0,
74 last: None,
75 })
76 }
77
78 pub const fn count(&self) -> usize {
80 self.count
81 }
82
83 pub const fn streak(&self) -> (usize, usize) {
85 (self.consec_up, self.consec_down)
86 }
87
88 pub const fn value(&self) -> Option<f64> {
90 self.last
91 }
92}
93
94impl Indicator for NewPriceLines {
95 type Input = Candle;
96 type Output = f64;
97
98 #[inline]
99 fn update(&mut self, candle: Candle) -> Option<f64> {
100 let close = candle.close;
101 let Some(prev) = self.prev_close else {
102 self.prev_close = Some(close);
103 return None;
104 };
105 if close > prev {
106 self.consec_up += 1;
107 self.consec_down = 0;
108 } else if close < prev {
109 self.consec_down += 1;
110 self.consec_up = 0;
111 } else {
112 self.consec_up = 0;
113 self.consec_down = 0;
114 }
115 self.prev_close = Some(close);
116
117 let v = if self.consec_up >= self.count {
118 -1.0
119 } else if self.consec_down >= self.count {
120 1.0
121 } else {
122 0.0
123 };
124 self.last = Some(v);
125 Some(v)
126 }
127
128 fn reset(&mut self) {
129 self.prev_close = None;
130 self.consec_up = 0;
131 self.consec_down = 0;
132 self.last = None;
133 }
134
135 #[inline]
136 fn warmup_period(&self) -> usize {
137 2
138 }
139
140 #[inline]
141 fn is_ready(&self) -> bool {
142 self.last.is_some()
143 }
144
145 #[inline]
146 fn name(&self) -> &'static str {
147 "NewPriceLines"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::BatchExt;
155
156 fn c(close: f64) -> Candle {
157 Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
158 }
159
160 #[test]
161 fn rejects_small_count() {
162 assert!(matches!(
163 NewPriceLines::new(1),
164 Err(Error::InvalidPeriod { .. })
165 ));
166 assert!(NewPriceLines::new(2).is_ok());
167 }
168
169 #[test]
170 fn accessors_and_metadata() {
171 let n = NewPriceLines::new(8).unwrap();
172 assert_eq!(n.count(), 8);
173 assert_eq!(n.streak(), (0, 0));
174 assert_eq!(n.warmup_period(), 2);
175 assert_eq!(n.name(), "NewPriceLines");
176 assert!(!n.is_ready());
177 assert_eq!(n.value(), None);
178 }
179
180 #[test]
181 fn first_bar_seeds_without_signal() {
182 let mut n = NewPriceLines::new(3).unwrap();
183 assert_eq!(n.update(c(100.0)), None);
184 assert!(n.update(c(101.0)).is_some());
185 }
186
187 #[test]
188 fn eight_higher_closes_signal_sell() {
189 let mut n = NewPriceLines::new(8).unwrap();
190 let candles: Vec<Candle> = (0..12).map(|i| c(100.0 + f64::from(i))).collect();
192 let last = n.batch(&candles).into_iter().flatten().last().unwrap();
193 assert_eq!(last, -1.0);
194 }
195
196 #[test]
197 fn eight_lower_closes_signal_buy() {
198 let mut n = NewPriceLines::new(8).unwrap();
199 let candles: Vec<Candle> = (0..12).map(|i| c(200.0 - f64::from(i))).collect();
200 let last = n.batch(&candles).into_iter().flatten().last().unwrap();
201 assert_eq!(last, 1.0);
202 }
203
204 #[test]
205 fn break_in_streak_clears_signal() {
206 let mut n = NewPriceLines::new(3).unwrap();
207 n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); assert_eq!(n.value(), Some(-1.0));
209 assert_eq!(n.update(c(102.0)), Some(0.0));
211 assert_eq!(n.streak(), (0, 1));
212 }
213
214 #[test]
215 fn unchanged_close_resets_streak() {
216 let mut n = NewPriceLines::new(3).unwrap();
217 n.batch(&[c(100.0), c(101.0), c(102.0)]);
218 assert_eq!(n.update(c(102.0)), Some(0.0)); assert_eq!(n.streak(), (0, 0));
220 }
221
222 #[test]
223 fn reset_clears_state() {
224 let mut n = NewPriceLines::new(3).unwrap();
225 n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]);
226 assert!(n.is_ready());
227 n.reset();
228 assert!(!n.is_ready());
229 assert_eq!(n.value(), None);
230 assert_eq!(n.streak(), (0, 0));
231 }
232
233 #[test]
234 fn batch_equals_streaming() {
235 let candles: Vec<Candle> = (0..80)
236 .map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
237 .collect();
238 let batch = NewPriceLines::new(8).unwrap().batch(&candles);
239 let mut b = NewPriceLines::new(8).unwrap();
240 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
241 assert_eq!(batch, streamed);
242 }
243}