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 fn update(&mut self, candle: Candle) -> Option<f64> {
110 if self.window.len() == self.period {
111 self.window.pop_front();
112 }
113 self.window.push_back(candle);
114 if self.window.len() < self.period {
115 return None;
116 }
117 let count = self.count_single_prints() as f64;
118 self.last = Some(count);
119 Some(count)
120 }
121
122 fn reset(&mut self) {
123 self.window.clear();
124 self.last = None;
125 }
126
127 fn warmup_period(&self) -> usize {
128 self.period
129 }
130
131 fn is_ready(&self) -> bool {
132 self.last.is_some()
133 }
134
135 fn name(&self) -> &'static str {
136 "SinglePrints"
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::traits::BatchExt;
144
145 fn c(high: f64, low: f64) -> Candle {
146 Candle::new_unchecked(
147 f64::midpoint(high, low),
148 high,
149 low,
150 f64::midpoint(high, low),
151 1_000.0,
152 0,
153 )
154 }
155
156 #[test]
157 fn rejects_zero_params() {
158 assert!(matches!(SinglePrints::new(0, 24), Err(Error::PeriodZero)));
159 assert!(matches!(SinglePrints::new(20, 0), Err(Error::PeriodZero)));
160 }
161
162 #[test]
163 fn accessors_and_metadata() {
164 let s = SinglePrints::new(20, 24).unwrap();
165 assert_eq!(s.params(), (20, 24));
166 assert_eq!(s.warmup_period(), 20);
167 assert_eq!(s.name(), "SinglePrints");
168 assert!(!s.is_ready());
169 assert_eq!(s.value(), None);
170 }
171
172 #[test]
173 fn first_emission_at_warmup_period() {
174 let mut s = SinglePrints::new(4, 8).unwrap();
175 let candles: Vec<Candle> = (0..6)
176 .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
177 .collect();
178 let out = s.batch(&candles);
179 for v in out.iter().take(3) {
180 assert!(v.is_none());
181 }
182 assert!(out[3].is_some());
183 }
184
185 #[test]
186 fn flat_range_has_no_single_prints() {
187 let mut s = SinglePrints::new(4, 8).unwrap();
189 let last = s
190 .batch(&[c(100.0, 100.0); 6])
191 .into_iter()
192 .flatten()
193 .last()
194 .unwrap();
195 assert_eq!(last, 0.0);
196 }
197
198 #[test]
199 fn ramp_has_many_single_prints() {
200 let mut s = SinglePrints::new(10, 24).unwrap();
202 let candles: Vec<Candle> = (0..10)
203 .map(|i| c(100.5 + f64::from(i), 99.5 + f64::from(i)))
204 .collect();
205 let last = s.batch(&candles).into_iter().flatten().last().unwrap();
206 assert!(
207 last > 0.0,
208 "a ramp should produce single prints, got {last}"
209 );
210 }
211
212 #[test]
213 fn output_non_negative() {
214 let mut s = SinglePrints::new(14, 24).unwrap();
215 for v in s
216 .batch(
217 &(0..60)
218 .map(|i| c(110.0 + (f64::from(i) * 0.3).sin() * 8.0, 90.0))
219 .collect::<Vec<_>>(),
220 )
221 .into_iter()
222 .flatten()
223 {
224 assert!(v >= 0.0);
225 }
226 }
227
228 #[test]
229 fn reset_clears_state() {
230 let mut s = SinglePrints::new(4, 8).unwrap();
231 s.batch(
232 &(0..6)
233 .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
234 .collect::<Vec<_>>(),
235 );
236 assert!(s.is_ready());
237 s.reset();
238 assert!(!s.is_ready());
239 assert_eq!(s.value(), None);
240 assert_eq!(s.update(c(101.0, 99.0)), None);
241 }
242
243 #[test]
244 fn batch_equals_streaming() {
245 let candles: Vec<Candle> = (0..80)
246 .map(|i| c(110.0 + (f64::from(i) * 0.25).sin() * 9.0, 90.0))
247 .collect();
248 let batch = SinglePrints::new(20, 24).unwrap().batch(&candles);
249 let mut b = SinglePrints::new(20, 24).unwrap();
250 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
251 assert_eq!(batch, streamed);
252 }
253}