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