wickra_core/traits.rs
1//! Core traits: the [`Indicator`] state machine and the [`BatchExt`] blanket extension.
2
3use crate::ohlcv::Candle;
4
5/// A streaming technical indicator.
6///
7/// Every indicator in Wickra implements this trait. The contract is:
8///
9/// - [`update`](Indicator::update) is called once per input point and must be O(1) in
10/// the input length. Pre-existing buffered state may be touched, but no full
11/// recomputation over the entire series is permitted.
12/// - The returned `Option<Output>` is `None` while the indicator is still in its
13/// *warmup* phase (insufficient inputs to produce a defined value), and `Some`
14/// once it is ready.
15/// - [`reset`](Indicator::reset) clears all state, returning the indicator to the
16/// exact configuration it had immediately after construction.
17///
18/// Implementors that consume scalar prices use `Input = f64` so they automatically
19/// gain access to chaining via [`Chain`].
20pub trait Indicator {
21 /// Type of one input data point (typically `f64` for a price, or `Candle` / `Tick`).
22 type Input;
23 /// Type of one output value.
24 type Output;
25
26 /// Feed one new data point into the indicator and return the freshly computed
27 /// output, or `None` if the indicator is still warming up.
28 fn update(&mut self, input: Self::Input) -> Option<Self::Output>;
29
30 /// Reset all internal state, leaving the indicator equivalent to a freshly
31 /// constructed instance with the same parameters.
32 fn reset(&mut self);
33
34 /// Number of inputs required before the first non-`None` output can be produced.
35 fn warmup_period(&self) -> usize;
36
37 /// Whether the indicator has emitted at least one value since the last reset.
38 fn is_ready(&self) -> bool;
39
40 /// Stable, human-readable indicator name. Used by chaining and diagnostics.
41 fn name(&self) -> &'static str;
42}
43
44/// Blanket extension that adds batch evaluation to every [`Indicator`].
45///
46/// The naive `batch` simply replays `update` over a slice, which is always correct
47/// because `update` is the only state transition. Concrete indicators may override
48/// `batch` if they have a faster vectorized path; the default keeps the contract
49/// `batch == repeated update`.
50pub trait BatchExt: Indicator {
51 /// Run the indicator over a slice of inputs in order, returning one output (or
52 /// `None` during warmup) per input.
53 fn batch(&mut self, inputs: &[Self::Input]) -> Vec<Option<Self::Output>>
54 where
55 Self::Input: Clone,
56 {
57 let mut out = Vec::with_capacity(inputs.len());
58 for x in inputs {
59 out.push(self.update(x.clone()));
60 }
61 out
62 }
63
64 /// Run an independent copy of the indicator over each input series in parallel.
65 ///
66 /// Each asset is processed by its own fresh instance built via `make`, so state
67 /// never leaks across assets. Requires the `parallel` feature (enabled by
68 /// default), which pulls in `rayon`.
69 #[cfg(feature = "parallel")]
70 fn batch_parallel<F>(
71 inputs_per_asset: &[Vec<Self::Input>],
72 make: F,
73 ) -> Vec<Vec<Option<Self::Output>>>
74 where
75 Self: Sized + Send,
76 Self::Input: Sync + Clone,
77 Self::Output: Send,
78 F: Fn() -> Self + Sync + Send,
79 {
80 use rayon::prelude::*;
81 inputs_per_asset
82 .par_iter()
83 .map(|series| {
84 let mut ind = make();
85 ind.batch(series)
86 })
87 .collect()
88 }
89}
90
91impl<T: Indicator> BatchExt for T {}
92
93/// A streaming *bar builder* — an alternative-chart constructor (Renko, Kagi,
94/// Point-and-Figure) that turns a candle stream into a stream of price-driven
95/// bars.
96///
97/// Bar builders are deliberately **not** [`Indicator`]s: a single input candle
98/// may complete zero, one, or many bars (a large move can print several Renko
99/// bricks at once), which breaks the `update -> Option<Output>` one-in-one-out
100/// contract and the `batch == repeated update` length invariant. They get their
101/// own trait instead, returning a `Vec` of freshly completed bars per candle.
102///
103/// The contract is:
104///
105/// - [`update`](BarBuilder::update) ingests one candle and returns every bar it
106/// *completed* on that candle, in chronological order. An empty vector means
107/// the move was not large enough to finish a bar yet.
108/// - [`reset`](BarBuilder::reset) clears all state, returning the builder to the
109/// configuration it had immediately after construction.
110/// - [`batch`](BarBuilder::batch) concatenates the bars from replaying `update`
111/// over a slice; the flattened length is data-dependent, not the input length.
112///
113/// Bar builders cannot participate in [`Chain`] (which requires
114/// `Indicator<Input = f64, Output = f64>`); feed a downstream indicator from the
115/// bars' close prices manually if you need to chain off them.
116///
117/// ```text
118/// let mut renko = RenkoBars::new(1.0).unwrap();
119/// let bricks = renko.update(candle); // Vec<RenkoBrick>: 0..n completed bricks
120/// ```
121pub trait BarBuilder {
122 /// Type of one completed bar.
123 type Bar;
124
125 /// Feed one candle and return every bar completed on it (possibly none).
126 fn update(&mut self, candle: Candle) -> Vec<Self::Bar>;
127
128 /// Reset all internal state to the freshly-constructed configuration.
129 fn reset(&mut self);
130
131 /// Stable, human-readable builder name.
132 fn name(&self) -> &'static str;
133
134 /// Replay `update` over a slice, concatenating all completed bars. The
135 /// result length is data-dependent (not the input length).
136 fn batch(&mut self, candles: &[Candle]) -> Vec<Self::Bar> {
137 let mut out = Vec::new();
138 for candle in candles {
139 out.extend(self.update(*candle));
140 }
141 out
142 }
143}
144
145/// Chain two indicators so the output of the first becomes the input of the second.
146///
147/// Both indicators must agree on `f64` as the bridging type, which is the common
148/// case for price-in/value-out indicators. The chain itself is an indicator, so
149/// chains can be nested arbitrarily.
150///
151/// # Example
152///
153/// ```
154/// use wickra_core::{Chain, Ema, Indicator, Rsi};
155///
156/// // RSI(7) on top of EMA(14). EMA seeds at input 14, then RSI needs 7+1 more
157/// // valid inputs to emit, so the chain becomes ready at input 21.
158/// let mut chain = Chain::new(Ema::new(14).unwrap(), Rsi::new(7).unwrap());
159/// for i in 1..=21 {
160/// chain.update(f64::from(i));
161/// }
162/// assert!(chain.is_ready());
163/// ```
164#[derive(Debug, Clone)]
165pub struct Chain<A, B>
166where
167 A: Indicator<Input = f64, Output = f64>,
168 B: Indicator<Input = f64>,
169{
170 first: A,
171 second: B,
172}
173
174impl<A, B> Chain<A, B>
175where
176 A: Indicator<Input = f64, Output = f64>,
177 B: Indicator<Input = f64>,
178{
179 /// Construct a chain whose inputs flow through `first` and then `second`.
180 pub const fn new(first: A, second: B) -> Self {
181 Self { first, second }
182 }
183
184 /// Add a third stage on top.
185 pub fn then<C>(self, third: C) -> Chain<Self, C>
186 where
187 C: Indicator<Input = f64>,
188 Self: Indicator<Input = f64, Output = f64>,
189 {
190 Chain::new(self, third)
191 }
192
193 /// Borrow the upstream indicator.
194 pub const fn first(&self) -> &A {
195 &self.first
196 }
197
198 /// Borrow the downstream indicator.
199 pub const fn second(&self) -> &B {
200 &self.second
201 }
202}
203
204impl<A, B> Indicator for Chain<A, B>
205where
206 A: Indicator<Input = f64, Output = f64>,
207 B: Indicator<Input = f64>,
208{
209 type Input = f64;
210 type Output = B::Output;
211
212 fn update(&mut self, input: f64) -> Option<Self::Output> {
213 self.first.update(input).and_then(|v| self.second.update(v))
214 }
215
216 fn reset(&mut self) {
217 self.first.reset();
218 self.second.reset();
219 }
220
221 fn warmup_period(&self) -> usize {
222 // Conservative upper bound: both stages must warm up.
223 self.first.warmup_period() + self.second.warmup_period()
224 }
225
226 fn is_ready(&self) -> bool {
227 self.first.is_ready() && self.second.is_ready()
228 }
229
230 fn name(&self) -> &'static str {
231 "Chain"
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 /// A trivial test indicator: identity (passes input through).
240 #[derive(Debug, Default)]
241 struct Identity {
242 seen: bool,
243 }
244
245 impl Indicator for Identity {
246 type Input = f64;
247 type Output = f64;
248 fn update(&mut self, input: f64) -> Option<f64> {
249 self.seen = true;
250 Some(input)
251 }
252 fn reset(&mut self) {
253 self.seen = false;
254 }
255 fn warmup_period(&self) -> usize {
256 0
257 }
258 fn is_ready(&self) -> bool {
259 self.seen
260 }
261 fn name(&self) -> &'static str {
262 "Identity"
263 }
264 }
265
266 /// Another trivial test indicator: scales input by 2.
267 #[derive(Debug, Default)]
268 struct Doubler {
269 seen: bool,
270 }
271
272 impl Indicator for Doubler {
273 type Input = f64;
274 type Output = f64;
275 fn update(&mut self, input: f64) -> Option<f64> {
276 self.seen = true;
277 Some(input * 2.0)
278 }
279 fn reset(&mut self) {
280 self.seen = false;
281 }
282 fn warmup_period(&self) -> usize {
283 0
284 }
285 fn is_ready(&self) -> bool {
286 self.seen
287 }
288 fn name(&self) -> &'static str {
289 "Doubler"
290 }
291 }
292
293 #[test]
294 fn batch_replays_update() {
295 let mut id = Identity::default();
296 let out = id.batch(&[1.0, 2.0, 3.0]);
297 assert_eq!(out, vec![Some(1.0), Some(2.0), Some(3.0)]);
298 }
299
300 #[test]
301 fn chain_pipes_first_into_second() {
302 let mut c = Chain::new(Doubler::default(), Doubler::default());
303 // 5 -> 10 -> 20
304 assert_eq!(c.update(5.0), Some(20.0));
305 }
306
307 #[test]
308 fn chain_is_ready_only_after_both_stages_emit() {
309 let mut c = Chain::new(Doubler::default(), Doubler::default());
310 assert!(!c.is_ready());
311 c.update(1.0);
312 assert!(c.is_ready());
313 }
314
315 #[test]
316 fn chain_reset_propagates() {
317 let mut c = Chain::new(Doubler::default(), Doubler::default());
318 c.update(1.0);
319 assert!(c.is_ready());
320 c.reset();
321 assert!(!c.is_ready());
322 }
323
324 #[test]
325 fn chain_three_levels_via_then() {
326 let c = Chain::new(Doubler::default(), Doubler::default()).then(Doubler::default());
327 let mut c = c;
328 // 1 -> 2 -> 4 -> 8
329 assert_eq!(c.update(1.0), Some(8.0));
330 }
331
332 /// Cover the `Chain::first` / `Chain::second` borrow accessors and the
333 /// `Chain::warmup_period` + `Chain::name` Indicator-impl bodies.
334 ///
335 /// Existing chain tests only invoked the Indicator surface (`update`,
336 /// `reset`, `is_ready`) on the wrapped `Chain`. The const borrow accessors
337 /// and the `warmup_period` / `name` impls were never traversed, so Codecov
338 /// flagged traits.rs lines 140-142, 145-147, 167-170, 176-178 as missed.
339 /// `chain.warmup_period()` also reaches `Doubler::warmup_period`
340 /// (228-230), and `chain.first().name()` reaches `Doubler::name`
341 /// (234-236) — both helper methods were uncovered for the same reason.
342 #[test]
343 fn chain_accessors_and_metadata() {
344 let chain = Chain::new(Doubler::default(), Doubler::default());
345 // Borrow accessors return the wrapped stages; query each via .name()
346 // so Doubler::name (lines 234-236) is also exercised.
347 assert_eq!(chain.first().name(), "Doubler");
348 assert_eq!(chain.second().name(), "Doubler");
349 // Doubler::warmup_period (lines 228-230) is 0; Chain::warmup_period
350 // sums the two, so the result must also be 0.
351 assert_eq!(chain.first().warmup_period(), 0);
352 assert_eq!(chain.second().warmup_period(), 0);
353 assert_eq!(chain.warmup_period(), 0);
354 // Chain::name returns the literal "Chain" (line 177).
355 assert_eq!(chain.name(), "Chain");
356 }
357
358 /// Cover the full Indicator surface of the `Identity` test helper:
359 /// `reset` (198-200), `warmup_period` (201-203), `is_ready` (204-206),
360 /// and `name` (207-209). The only other test using `Identity`
361 /// (`batch_replays_update`) calls `batch`, which exercises `update`
362 /// alone, leaving the remaining four trait methods uncovered.
363 #[test]
364 fn identity_helper_full_indicator_surface() {
365 let mut id = Identity::default();
366 // warmup_period is the literal 0; name is the literal "Identity".
367 assert_eq!(id.warmup_period(), 0);
368 assert_eq!(id.name(), "Identity");
369 // is_ready exercises the `self.seen` return with seen=false first…
370 assert!(!id.is_ready());
371 // …then with seen=true after a single update.
372 let out = id.update(42.0);
373 assert_eq!(out, Some(42.0));
374 assert!(id.is_ready());
375 // reset() flips seen back to false; is_ready reflects it.
376 id.reset();
377 assert!(!id.is_ready());
378 }
379
380 #[cfg(feature = "parallel")]
381 #[test]
382 fn batch_parallel_runs_independent_instances() {
383 let series: Vec<Vec<f64>> = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
384 let out = Doubler::batch_parallel(&series, Doubler::default);
385 assert_eq!(out.len(), 2);
386 assert_eq!(out[0], vec![Some(2.0), Some(4.0), Some(6.0)]);
387 assert_eq!(out[1], vec![Some(8.0), Some(10.0), Some(12.0)]);
388 }
389}