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