wickra_core/indicators/
single_prints.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
45pub struct SinglePrints {
46 period: usize,
47 bins: usize,
48 window: VecDeque<Candle>,
49 last: Option<f64>,
50}
51
52impl SinglePrints {
53 pub fn new(period: usize, bins: usize) -> Result<Self> {
59 if period == 0 || bins == 0 {
60 return Err(Error::PeriodZero);
61 }
62 Ok(Self {
63 period,
64 bins,
65 window: VecDeque::with_capacity(period),
66 last: None,
67 })
68 }
69
70 pub const fn params(&self) -> (usize, usize) {
72 (self.period, self.bins)
73 }
74
75 pub const fn value(&self) -> Option<f64> {
77 self.last
78 }
79
80 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
81 fn count_single_prints(&self) -> usize {
82 let mut low = f64::INFINITY;
83 let mut high = f64::NEG_INFINITY;
84 for c in &self.window {
85 low = low.min(c.low);
86 high = high.max(c.high);
87 }
88 let span = high - low;
89 if span <= 0.0 {
90 return 0;
91 }
92 let width = span / self.bins as f64;
93 let mut touches = vec![0u32; self.bins];
94 for c in &self.window {
95 let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1);
96 let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1);
97 for t in touches.iter_mut().take(hi_idx + 1).skip(lo_idx) {
98 *t += 1;
99 }
100 }
101 touches.iter().filter(|&&t| t == 1).count()
102 }
103}
104
105impl Indicator for SinglePrints {
106 type Input = Candle;
107 type Output = f64;
108
109 #[inline]
110 fn update(&mut self, candle: Candle) -> Option<f64> {
111 if self.window.len() == self.period {
112 self.window.pop_front();
113 }
114 self.window.push_back(candle);
115 if self.window.len() < self.period {
116 return None;
117 }
118 let count = self.count_single_prints() as f64;
119 self.last = Some(count);
120 Some(count)
121 }
122
123 fn reset(&mut self) {
124 self.window.clear();
125 self.last = None;
126 }
127
128 #[inline]
129 fn warmup_period(&self) -> usize {
130 self.period
131 }
132
133 #[inline]
134 fn is_ready(&self) -> bool {
135 self.last.is_some()
136 }
137
138 #[inline]
139 fn name(&self) -> &'static str {
140 "SinglePrints"
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::traits::BatchExt;
148
149 fn c(high: f64, low: f64) -> Candle {
150 Candle::new_unchecked(
151 f64::midpoint(high, low),
152 high,
153 low,
154 f64::midpoint(high, low),
155 1_000.0,
156 0,
157 )
158 }
159
160 #[test]
161 fn rejects_zero_params() {
162 assert!(matches!(SinglePrints::new(0, 24), Err(Error::PeriodZero)));
163 assert!(matches!(SinglePrints::new(20, 0), Err(Error::PeriodZero)));
164 }
165
166 #[test]
167 fn accessors_and_metadata() {
168 let s = SinglePrints::new(20, 24).unwrap();
169 assert_eq!(s.params(), (20, 24));
170 assert_eq!(s.warmup_period(), 20);
171 assert_eq!(s.name(), "SinglePrints");
172 assert!(!s.is_ready());
173 assert_eq!(s.value(), None);
174 }
175
176 #[test]
177 fn first_emission_at_warmup_period() {
178 let mut s = SinglePrints::new(4, 8).unwrap();
179 let candles: Vec<Candle> = (0..6)
180 .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
181 .collect();
182 let out = s.batch(&candles);
183 for v in out.iter().take(3) {
184 assert!(v.is_none());
185 }
186 assert!(out[3].is_some());
187 }
188
189 #[test]
190 fn flat_range_has_no_single_prints() {
191 let mut s = SinglePrints::new(4, 8).unwrap();
193 let last = s
194 .batch(&[c(100.0, 100.0); 6])
195 .into_iter()
196 .flatten()
197 .last()
198 .unwrap();
199 assert_eq!(last, 0.0);
200 }
201
202 #[test]
203 fn ramp_has_many_single_prints() {
204 let mut s = SinglePrints::new(10, 24).unwrap();
206 let candles: Vec<Candle> = (0..10)
207 .map(|i| c(100.5 + f64::from(i), 99.5 + f64::from(i)))
208 .collect();
209 let last = s.batch(&candles).into_iter().flatten().last().unwrap();
210 assert!(
211 last > 0.0,
212 "a ramp should produce single prints, got {last}"
213 );
214 }
215
216 #[test]
217 fn output_non_negative() {
218 let mut s = SinglePrints::new(14, 24).unwrap();
219 for v in s
220 .batch(
221 &(0..60)
222 .map(|i| c(110.0 + (f64::from(i) * 0.3).sin() * 8.0, 90.0))
223 .collect::<Vec<_>>(),
224 )
225 .into_iter()
226 .flatten()
227 {
228 assert!(v >= 0.0);
229 }
230 }
231
232 #[test]
233 fn reset_clears_state() {
234 let mut s = SinglePrints::new(4, 8).unwrap();
235 s.batch(
236 &(0..6)
237 .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
238 .collect::<Vec<_>>(),
239 );
240 assert!(s.is_ready());
241 s.reset();
242 assert!(!s.is_ready());
243 assert_eq!(s.value(), None);
244 assert_eq!(s.update(c(101.0, 99.0)), None);
245 }
246
247 #[test]
248 fn batch_equals_streaming() {
249 let candles: Vec<Candle> = (0..80)
250 .map(|i| c(110.0 + (f64::from(i) * 0.25).sin() * 9.0, 90.0))
251 .collect();
252 let batch = SinglePrints::new(20, 24).unwrap().batch(&candles);
253 let mut b = SinglePrints::new(20, 24).unwrap();
254 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
255 assert_eq!(batch, streamed);
256 }
257}