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