Skip to main content

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/// Fast batch for scalar `f64 -> f64` indicators.
94///
95/// The generic [`BatchExt::batch`] returns `Vec<Option<f64>>` — 16 bytes per
96/// element (no niche fits an arbitrary `f64`), which a caller wanting a dense
97/// `f64` series then has to walk a second time to map warmup `None`s to `NaN`.
98/// This skips both the wide intermediate and the second pass: one allocation,
99/// one pass, warmup encoded as `NaN`. The default body is bit-identical to
100/// replaying `update`; indicators with a vectorizable closed form override it
101/// with an inherent `batch_nan` of the same name, which wins method resolution
102/// over this trait default.
103pub trait BatchNanExt: Indicator<Input = f64, Output = f64> {
104    /// One `f64` per input, warmup positions filled with `NaN`.
105    fn batch_nan(&mut self, inputs: &[f64]) -> Vec<f64> {
106        let mut out = Vec::with_capacity(inputs.len());
107        for &x in inputs {
108            out.push(self.update(x).unwrap_or(f64::NAN));
109        }
110        out
111    }
112}
113
114impl<T: Indicator<Input = f64, Output = f64>> BatchNanExt for T {}
115
116/// A streaming *bar builder* — an alternative-chart constructor (Renko, Kagi,
117/// Point-and-Figure) that turns a candle stream into a stream of price-driven
118/// bars.
119///
120/// Bar builders are deliberately **not** [`Indicator`]s: a single input candle
121/// may complete zero, one, or many bars (a large move can print several Renko
122/// bricks at once), which breaks the `update -> Option<Output>` one-in-one-out
123/// contract and the `batch == repeated update` length invariant. They get their
124/// own trait instead, returning a `Vec` of freshly completed bars per candle.
125///
126/// The contract is:
127///
128/// - [`update`](BarBuilder::update) ingests one candle and returns every bar it
129///   *completed* on that candle, in chronological order. An empty vector means
130///   the move was not large enough to finish a bar yet.
131/// - [`reset`](BarBuilder::reset) clears all state, returning the builder to the
132///   configuration it had immediately after construction.
133/// - [`batch`](BarBuilder::batch) concatenates the bars from replaying `update`
134///   over a slice; the flattened length is data-dependent, not the input length.
135///
136/// Bar builders cannot participate in [`Chain`] (which requires
137/// `Indicator<Input = f64, Output = f64>`); feed a downstream indicator from the
138/// bars' close prices manually if you need to chain off them.
139///
140/// ```text
141/// let mut renko = RenkoBars::new(1.0).unwrap();
142/// let bricks = renko.update(candle); // Vec<RenkoBrick>: 0..n completed bricks
143/// ```
144pub trait BarBuilder {
145    /// Type of one completed bar.
146    type Bar;
147
148    /// Feed one candle and return every bar completed on it (possibly none).
149    fn update(&mut self, candle: Candle) -> Vec<Self::Bar>;
150
151    /// Reset all internal state to the freshly-constructed configuration.
152    fn reset(&mut self);
153
154    /// Stable, human-readable builder name.
155    fn name(&self) -> &'static str;
156
157    /// Replay `update` over a slice, concatenating all completed bars. The
158    /// result length is data-dependent (not the input length).
159    fn batch(&mut self, candles: &[Candle]) -> Vec<Self::Bar> {
160        let mut out = Vec::new();
161        for candle in candles {
162            out.extend(self.update(*candle));
163        }
164        out
165    }
166}
167
168/// Chain two indicators so the output of the first becomes the input of the second.
169///
170/// Both indicators must agree on `f64` as the bridging type, which is the common
171/// case for price-in/value-out indicators. The chain itself is an indicator, so
172/// chains can be nested arbitrarily.
173///
174/// # Example
175///
176/// ```
177/// use wickra_core::{Chain, Ema, Indicator, Rsi};
178///
179/// // RSI(7) on top of EMA(14). EMA seeds at input 14, then RSI needs 7+1 more
180/// // valid inputs to emit, so the chain becomes ready at input 21.
181/// let mut chain = Chain::new(Ema::new(14).unwrap(), Rsi::new(7).unwrap());
182/// for i in 1..=21 {
183///     chain.update(f64::from(i));
184/// }
185/// assert!(chain.is_ready());
186/// ```
187#[derive(Debug, Clone)]
188pub struct Chain<A, B>
189where
190    A: Indicator<Input = f64, Output = f64>,
191    B: Indicator<Input = f64>,
192{
193    first: A,
194    second: B,
195}
196
197impl<A, B> Chain<A, B>
198where
199    A: Indicator<Input = f64, Output = f64>,
200    B: Indicator<Input = f64>,
201{
202    /// Construct a chain whose inputs flow through `first` and then `second`.
203    pub const fn new(first: A, second: B) -> Self {
204        Self { first, second }
205    }
206
207    /// Add a third stage on top.
208    pub fn then<C>(self, third: C) -> Chain<Self, C>
209    where
210        C: Indicator<Input = f64>,
211        Self: Indicator<Input = f64, Output = f64>,
212    {
213        Chain::new(self, third)
214    }
215
216    /// Borrow the upstream indicator.
217    pub const fn first(&self) -> &A {
218        &self.first
219    }
220
221    /// Borrow the downstream indicator.
222    pub const fn second(&self) -> &B {
223        &self.second
224    }
225}
226
227impl<A, B> Indicator for Chain<A, B>
228where
229    A: Indicator<Input = f64, Output = f64>,
230    B: Indicator<Input = f64>,
231{
232    type Input = f64;
233    type Output = B::Output;
234
235    fn update(&mut self, input: f64) -> Option<Self::Output> {
236        self.first.update(input).and_then(|v| self.second.update(v))
237    }
238
239    fn reset(&mut self) {
240        self.first.reset();
241        self.second.reset();
242    }
243
244    fn warmup_period(&self) -> usize {
245        // Conservative upper bound: both stages must warm up.
246        self.first.warmup_period() + self.second.warmup_period()
247    }
248
249    fn is_ready(&self) -> bool {
250        self.first.is_ready() && self.second.is_ready()
251    }
252
253    fn name(&self) -> &'static str {
254        "Chain"
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    /// A trivial test indicator: identity (passes input through).
263    #[derive(Debug, Default)]
264    struct Identity {
265        seen: bool,
266    }
267
268    impl Indicator for Identity {
269        type Input = f64;
270        type Output = f64;
271        fn update(&mut self, input: f64) -> Option<f64> {
272            self.seen = true;
273            Some(input)
274        }
275        fn reset(&mut self) {
276            self.seen = false;
277        }
278        fn warmup_period(&self) -> usize {
279            0
280        }
281        fn is_ready(&self) -> bool {
282            self.seen
283        }
284        fn name(&self) -> &'static str {
285            "Identity"
286        }
287    }
288
289    /// Another trivial test indicator: scales input by 2.
290    #[derive(Debug, Default)]
291    struct Doubler {
292        seen: bool,
293    }
294
295    impl Indicator for Doubler {
296        type Input = f64;
297        type Output = f64;
298        fn update(&mut self, input: f64) -> Option<f64> {
299            self.seen = true;
300            Some(input * 2.0)
301        }
302        fn reset(&mut self) {
303            self.seen = false;
304        }
305        fn warmup_period(&self) -> usize {
306            0
307        }
308        fn is_ready(&self) -> bool {
309            self.seen
310        }
311        fn name(&self) -> &'static str {
312            "Doubler"
313        }
314    }
315
316    #[test]
317    fn batch_replays_update() {
318        let mut id = Identity::default();
319        let out = id.batch(&[1.0, 2.0, 3.0]);
320        assert_eq!(out, vec![Some(1.0), Some(2.0), Some(3.0)]);
321    }
322
323    /// The blanket [`BatchNanExt::batch_nan`] default (used by every scalar
324    /// indicator without an inherent fast path) maps `update` outputs to a dense
325    /// `f64` series, warmup `None` becoming `NaN`. `Identity` is always ready, so
326    /// the result is just the inputs back.
327    #[test]
328    fn batch_nan_default_maps_none_to_nan() {
329        let mut id = Identity::default();
330        let out = id.batch_nan(&[1.0, 2.0, 3.0]);
331        assert_eq!(out, vec![1.0, 2.0, 3.0]);
332    }
333
334    #[test]
335    fn chain_pipes_first_into_second() {
336        let mut c = Chain::new(Doubler::default(), Doubler::default());
337        // 5 -> 10 -> 20
338        assert_eq!(c.update(5.0), Some(20.0));
339    }
340
341    #[test]
342    fn chain_is_ready_only_after_both_stages_emit() {
343        let mut c = Chain::new(Doubler::default(), Doubler::default());
344        assert!(!c.is_ready());
345        c.update(1.0);
346        assert!(c.is_ready());
347    }
348
349    #[test]
350    fn chain_reset_propagates() {
351        let mut c = Chain::new(Doubler::default(), Doubler::default());
352        c.update(1.0);
353        assert!(c.is_ready());
354        c.reset();
355        assert!(!c.is_ready());
356    }
357
358    #[test]
359    fn chain_three_levels_via_then() {
360        let c = Chain::new(Doubler::default(), Doubler::default()).then(Doubler::default());
361        let mut c = c;
362        // 1 -> 2 -> 4 -> 8
363        assert_eq!(c.update(1.0), Some(8.0));
364    }
365
366    /// Cover the `Chain::first` / `Chain::second` borrow accessors and the
367    /// `Chain::warmup_period` + `Chain::name` Indicator-impl bodies.
368    ///
369    /// Existing chain tests only invoked the Indicator surface (`update`,
370    /// `reset`, `is_ready`) on the wrapped `Chain`. The const borrow accessors
371    /// and the `warmup_period` / `name` impls were never traversed, so Codecov
372    /// flagged traits.rs lines 140-142, 145-147, 167-170, 176-178 as missed.
373    /// `chain.warmup_period()` also reaches `Doubler::warmup_period`
374    /// (228-230), and `chain.first().name()` reaches `Doubler::name`
375    /// (234-236) — both helper methods were uncovered for the same reason.
376    #[test]
377    fn chain_accessors_and_metadata() {
378        let chain = Chain::new(Doubler::default(), Doubler::default());
379        // Borrow accessors return the wrapped stages; query each via .name()
380        // so Doubler::name (lines 234-236) is also exercised.
381        assert_eq!(chain.first().name(), "Doubler");
382        assert_eq!(chain.second().name(), "Doubler");
383        // Doubler::warmup_period (lines 228-230) is 0; Chain::warmup_period
384        // sums the two, so the result must also be 0.
385        assert_eq!(chain.first().warmup_period(), 0);
386        assert_eq!(chain.second().warmup_period(), 0);
387        assert_eq!(chain.warmup_period(), 0);
388        // Chain::name returns the literal "Chain" (line 177).
389        assert_eq!(chain.name(), "Chain");
390    }
391
392    /// Cover the full Indicator surface of the `Identity` test helper:
393    /// `reset` (198-200), `warmup_period` (201-203), `is_ready` (204-206),
394    /// and `name` (207-209). The only other test using `Identity`
395    /// (`batch_replays_update`) calls `batch`, which exercises `update`
396    /// alone, leaving the remaining four trait methods uncovered.
397    #[test]
398    fn identity_helper_full_indicator_surface() {
399        let mut id = Identity::default();
400        // warmup_period is the literal 0; name is the literal "Identity".
401        assert_eq!(id.warmup_period(), 0);
402        assert_eq!(id.name(), "Identity");
403        // is_ready exercises the `self.seen` return with seen=false first…
404        assert!(!id.is_ready());
405        // …then with seen=true after a single update.
406        let out = id.update(42.0);
407        assert_eq!(out, Some(42.0));
408        assert!(id.is_ready());
409        // reset() flips seen back to false; is_ready reflects it.
410        id.reset();
411        assert!(!id.is_ready());
412    }
413
414    #[cfg(feature = "parallel")]
415    #[test]
416    fn batch_parallel_runs_independent_instances() {
417        let series: Vec<Vec<f64>> = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
418        let out = Doubler::batch_parallel(&series, Doubler::default);
419        assert_eq!(out.len(), 2);
420        assert_eq!(out[0], vec![Some(2.0), Some(4.0), Some(6.0)]);
421        assert_eq!(out[1], vec![Some(8.0), Some(10.0), Some(12.0)]);
422    }
423}