Skip to main content

fin_primitives/signals/
mod.rs

1//! # Module: signals
2//!
3//! ## Responsibility
4//! Provides the `Signal` trait, `SignalValue` enum, `BarInput` thin input type, and a
5//! `SignalPipeline` that applies multiple signals to each OHLCV bar in sequence.
6//!
7//! ## Guarantees
8//! - `SignalValue::Unavailable` is returned until a signal has accumulated `period` bars
9//! - `SignalPipeline::update` always returns a `SignalMap`; per-signal errors are collected
10//!   rather than aborting the whole pipeline
11//!
12//! ## NOT Responsible For
13//! - Persistence
14//! - Real-time streaming (use `OhlcvAggregator` upstream)
15
16pub mod indicators;
17pub mod pipeline;
18
19use crate::error::FinError;
20use crate::ohlcv::OhlcvBar;
21use rust_decimal::Decimal;
22
23/// Thin input type for signal computation, decoupled from `OhlcvBar`.
24///
25/// Carrying all four price fields and volume allows future indicators (e.g. MACD on
26/// high-low, OBV on volume) without forcing a dependency on `OhlcvBar`.
27#[derive(Debug, Clone, Copy)]
28pub struct BarInput {
29    /// Closing price (used by most indicators).
30    pub close: Decimal,
31    /// High price of the bar.
32    pub high: Decimal,
33    /// Low price of the bar.
34    pub low: Decimal,
35    /// Opening price of the bar.
36    pub open: Decimal,
37    /// Total traded volume during the bar.
38    pub volume: Decimal,
39}
40
41impl BarInput {
42    /// Constructs a `BarInput` with all fields explicitly specified.
43    pub fn new(close: Decimal, high: Decimal, low: Decimal, open: Decimal, volume: Decimal) -> Self {
44        Self { close, high, low, open, volume }
45    }
46
47    /// Constructs a `BarInput` from a single close price, setting all OHLC fields to `close`
48    /// and volume to zero. Useful in tests and for close-only indicators (SMA/EMA/RSI).
49    pub fn from_close(close: Decimal) -> Self {
50        Self { close, high: close, low: close, open: close, volume: Decimal::ZERO }
51    }
52
53    /// Returns the typical price of this bar: `(high + low + close) / 3`.
54    pub fn typical_price(&self) -> Decimal {
55        (self.high + self.low + self.close) / Decimal::from(3u32)
56    }
57
58    /// Returns the weighted close price: `(high + low + close + close) / 4`.
59    ///
60    /// Weights the close twice, giving it extra significance compared to the typical price.
61    /// Used by some indicators (e.g. CCI variants) and charting systems as a price reference.
62    pub fn weighted_close(&self) -> Decimal {
63        (self.high + self.low + self.close + self.close) / Decimal::from(4u32)
64    }
65
66    /// Returns the price range: `high - low`.
67    pub fn range(&self) -> Decimal {
68        self.high - self.low
69    }
70
71    /// Returns the midpoint of the bar: `(high + low) / 2`.
72    pub fn midpoint(&self) -> Decimal {
73        (self.high + self.low) / Decimal::from(2u32)
74    }
75
76    /// Close Location Value: `((close - low) - (high - close)) / (high - low)`.
77    ///
78    /// Ranges from -1.0 (close at low) to +1.0 (close at high).
79    /// Returns zero when the range is zero (doji / flat bar).
80    pub fn close_location_value(&self) -> Decimal {
81        let range = self.range();
82        if range.is_zero() {
83            return Decimal::ZERO;
84        }
85        (Decimal::from(2u32) * self.close - self.high - self.low) / range
86    }
87
88    /// Returns the signed intrabar move: `close - open`.
89    ///
90    /// Positive for bullish bars, negative for bearish, zero for doji.
91    /// Unlike [`body_size`](Self::body_size), this preserves direction.
92    pub fn net_move(&self) -> Decimal {
93        self.close - self.open
94    }
95
96    /// Returns the absolute body size: `|close - open|`.
97    pub fn body_size(&self) -> Decimal {
98        (self.close - self.open).abs()
99    }
100
101    /// Returns the higher of open and close: `max(open, close)`.
102    pub fn body_high(&self) -> Decimal {
103        self.open.max(self.close)
104    }
105
106    /// Returns the lower of open and close: `min(open, close)`.
107    pub fn body_low(&self) -> Decimal {
108        self.open.min(self.close)
109    }
110
111    /// Returns the upper wick length: `high - max(open, close)`.
112    pub fn upper_wick(&self) -> Decimal {
113        self.high - self.body_high()
114    }
115
116    /// Returns the lower wick length: `min(open, close) - low`.
117    pub fn lower_wick(&self) -> Decimal {
118        self.body_low() - self.low
119    }
120
121    /// Returns `true` if the bar closed higher than it opened (bullish candle).
122    pub fn is_bullish(&self) -> bool {
123        self.close > self.open
124    }
125
126    /// Returns `true` if the bar closed lower than it opened (bearish candle).
127    pub fn is_bearish(&self) -> bool {
128        self.close < self.open
129    }
130
131    /// Returns the close-to-close price change: `close - prev_close`.
132    ///
133    /// When `prev_close` is `None` (first bar), returns `Decimal::ZERO`.
134    pub fn price_change(&self, prev_close: Option<Decimal>) -> Decimal {
135        match prev_close {
136            None => Decimal::ZERO,
137            Some(pc) => self.close - pc,
138        }
139    }
140
141    /// Returns the log return: `ln(close / prev_close)` via f64.
142    ///
143    /// Returns `None` when `prev_close` is `None`, zero, or negative, or when the
144    /// f64 conversion fails.
145    pub fn log_return(&self, prev_close: Option<Decimal>) -> Option<Decimal> {
146        use rust_decimal::prelude::ToPrimitive;
147        let pc = prev_close?;
148        if pc <= Decimal::ZERO {
149            return None;
150        }
151        let ratio = self.close.to_f64()? / pc.to_f64()?;
152        if ratio <= 0.0 {
153            return None;
154        }
155        Decimal::try_from(ratio.ln()).ok()
156    }
157
158    /// Returns the True Range of this bar given the previous bar's close.
159    ///
160    /// `TR = max(high - low, |high - prev_close|, |low - prev_close|)`
161    ///
162    /// When there is no previous close (first bar), `high - low` is used as the true range.
163    pub fn true_range(&self, prev_close: Option<Decimal>) -> Decimal {
164        let hl = self.high - self.low;
165        match prev_close {
166            None => hl,
167            Some(pc) => {
168                let hc = (self.high - pc).abs();
169                let lc = (self.low - pc).abs();
170                hl.max(hc).max(lc)
171            }
172        }
173    }
174}
175
176impl From<&OhlcvBar> for BarInput {
177    fn from(bar: &OhlcvBar) -> Self {
178        Self {
179            close: bar.close.value(),
180            high: bar.high.value(),
181            low: bar.low.value(),
182            open: bar.open.value(),
183            volume: bar.volume.value(),
184        }
185    }
186}
187
188/// The output value of a signal computation.
189#[derive(Debug, Clone, PartialEq)]
190pub enum SignalValue {
191    /// A computed scalar value.
192    Scalar(Decimal),
193    /// The signal does not yet have enough data to produce a value.
194    Unavailable,
195}
196
197impl SignalValue {
198    /// Returns the inner `Decimal` if this is `Scalar`, or `None` if `Unavailable`.
199    ///
200    /// Eliminates `match` boilerplate at call sites.
201    pub fn as_decimal(&self) -> Option<Decimal> {
202        match self {
203            SignalValue::Scalar(d) => Some(*d),
204            SignalValue::Unavailable => None,
205        }
206    }
207
208    /// Returns `true` if this value is `Scalar`.
209    pub fn is_scalar(&self) -> bool {
210        matches!(self, SignalValue::Scalar(_))
211    }
212
213    /// Returns `true` if this value is `Unavailable`.
214    pub fn is_unavailable(&self) -> bool {
215        matches!(self, SignalValue::Unavailable)
216    }
217
218    /// Returns the inner `Decimal` if `Scalar`, otherwise returns `default`.
219    pub fn scalar_or(&self, default: Decimal) -> Decimal {
220        match self {
221            SignalValue::Scalar(d) => *d,
222            SignalValue::Unavailable => default,
223        }
224    }
225
226    /// Combine two `SignalValue`s with `f`, returning `Unavailable` if either is unavailable.
227    ///
228    /// Mirrors `Option::zip` combined with `map`. Useful for computing derived values
229    /// that require two ready signals (e.g. a spread = signal_a - signal_b).
230    ///
231    /// # Example
232    /// ```rust
233    /// use fin_primitives::signals::SignalValue;
234    /// use rust_decimal_macros::dec;
235    ///
236    /// let a = SignalValue::Scalar(dec!(10));
237    /// let b = SignalValue::Scalar(dec!(3));
238    /// let diff = a.zip_with(b, |x, y| x - y);
239    /// assert_eq!(diff, SignalValue::Scalar(dec!(7)));
240    /// ```
241    pub fn zip_with(
242        self,
243        other: SignalValue,
244        f: impl FnOnce(Decimal, Decimal) -> Decimal,
245    ) -> SignalValue {
246        match (self, other) {
247            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(f(a, b)),
248            _ => SignalValue::Unavailable,
249        }
250    }
251
252    /// Apply `f` to the inner value if `Scalar`, returning a new `SignalValue`.
253    ///
254    /// If `Unavailable`, returns `Unavailable` without calling `f`. This mirrors
255    /// `Option::map` and enables functional chaining without explicit `match`.
256    ///
257    /// # Example
258    /// ```rust
259    /// use fin_primitives::signals::SignalValue;
260    /// use rust_decimal_macros::dec;
261    ///
262    /// let v = SignalValue::Scalar(dec!(100));
263    /// let scaled = v.map(|x| x * dec!(2));
264    /// assert_eq!(scaled, SignalValue::Scalar(dec!(200)));
265    /// ```
266    pub fn map(self, f: impl FnOnce(Decimal) -> Decimal) -> SignalValue {
267        match self {
268            SignalValue::Scalar(d) => SignalValue::Scalar(f(d)),
269            SignalValue::Unavailable => SignalValue::Unavailable,
270        }
271    }
272
273    /// Applies `f` to the inner value if `Scalar`, where `f` returns a `SignalValue`.
274    ///
275    /// If `Unavailable`, returns `Unavailable` without calling `f`. This mirrors
276    /// `Option::and_then` and enables chaining operations that may themselves produce
277    /// `Unavailable` (e.g., clamping, conditional transforms).
278    ///
279    /// # Example
280    /// ```rust
281    /// use fin_primitives::signals::SignalValue;
282    /// use rust_decimal_macros::dec;
283    ///
284    /// let v = SignalValue::Scalar(dec!(50));
285    /// // Only return a value if it's above 30.
286    /// let r = v.and_then(|x| if x > dec!(30) { SignalValue::Scalar(x) } else { SignalValue::Unavailable });
287    /// assert_eq!(r, SignalValue::Scalar(dec!(50)));
288    /// ```
289    pub fn and_then(self, f: impl FnOnce(Decimal) -> SignalValue) -> SignalValue {
290        match self {
291            SignalValue::Scalar(d) => f(d),
292            SignalValue::Unavailable => SignalValue::Unavailable,
293        }
294    }
295
296    /// Negates the scalar value: returns `Scalar(-x)` if `Scalar(x)`, else `Unavailable`.
297    ///
298    /// Useful for inverting oscillator signals (e.g. turning a sell signal into a buy signal
299    /// by negating the output) without requiring an explicit `map(|x| -x)`.
300    pub fn negate(self) -> SignalValue {
301        match self {
302            SignalValue::Scalar(d) => SignalValue::Scalar(-d),
303            SignalValue::Unavailable => SignalValue::Unavailable,
304        }
305    }
306
307    /// Adds `delta` to the scalar value.
308    ///
309    /// Returns [`SignalValue::Unavailable`] unchanged.
310    pub fn offset(self, delta: rust_decimal::Decimal) -> SignalValue {
311        match self {
312            SignalValue::Unavailable => SignalValue::Unavailable,
313            SignalValue::Scalar(v) => SignalValue::Scalar(v + delta),
314        }
315    }
316
317    /// Returns the smaller of `self` and `other`.  `Unavailable` loses to any `Scalar`.
318    pub fn min_with(self, other: SignalValue) -> SignalValue {
319        match (self, other) {
320            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(a.min(b)),
321            (s @ SignalValue::Scalar(_), SignalValue::Unavailable) => s,
322            (SignalValue::Unavailable, s @ SignalValue::Scalar(_)) => s,
323            (SignalValue::Unavailable, SignalValue::Unavailable) => SignalValue::Unavailable,
324        }
325    }
326
327    /// Returns the larger of `self` and `other`.  `Unavailable` loses to any `Scalar`.
328    pub fn max_with(self, other: SignalValue) -> SignalValue {
329        match (self, other) {
330            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(a.max(b)),
331            (s @ SignalValue::Scalar(_), SignalValue::Unavailable) => s,
332            (SignalValue::Unavailable, s @ SignalValue::Scalar(_)) => s,
333            (SignalValue::Unavailable, SignalValue::Unavailable) => SignalValue::Unavailable,
334        }
335    }
336
337    /// Returns the absolute value of the scalar: `Scalar(|x|)` or `Unavailable`.
338    ///
339    /// Useful when you only care about the magnitude of a signal (e.g. absolute momentum).
340    pub fn abs(self) -> SignalValue {
341        match self {
342            SignalValue::Scalar(d) => SignalValue::Scalar(d.abs()),
343            SignalValue::Unavailable => SignalValue::Unavailable,
344        }
345    }
346
347    /// Scales the scalar by `factor`: `Scalar(x) * factor = Scalar(x * factor)`.
348    ///
349    /// Returns `Unavailable` if the signal is `Unavailable`. Useful for weighting
350    /// or inverting signals (e.g. `signal.mul(Decimal::NEGATIVE_ONE)`).
351    pub fn mul(self, factor: Decimal) -> SignalValue {
352        match self {
353            SignalValue::Scalar(d) => SignalValue::Scalar(d * factor),
354            SignalValue::Unavailable => SignalValue::Unavailable,
355        }
356    }
357
358    /// Subtracts two signals: `Scalar(a) - Scalar(b) = Scalar(a - b)`.
359    ///
360    /// Returns `Unavailable` if either operand is `Unavailable`.
361    pub fn sub(self, other: SignalValue) -> SignalValue {
362        match (self, other) {
363            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(a - b),
364            _ => SignalValue::Unavailable,
365        }
366    }
367
368    /// Multiplies two signals: `Scalar(a) * Scalar(b) = Scalar(a * b)`.
369    ///
370    /// Returns `Unavailable` if either operand is `Unavailable`.
371    pub fn mul_signal(self, other: SignalValue) -> SignalValue {
372        match (self, other) {
373            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(a * b),
374            _ => SignalValue::Unavailable,
375        }
376    }
377
378    /// Adds two signals: `Scalar(a) + Scalar(b) = Scalar(a + b)`.
379    ///
380    /// Returns `Unavailable` if either operand is `Unavailable`.
381    /// Useful for combining multiple signal outputs without explicit pattern matching.
382    pub fn add(self, other: SignalValue) -> SignalValue {
383        match (self, other) {
384            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(a + b),
385            _ => SignalValue::Unavailable,
386        }
387    }
388
389    /// Clamps the scalar value to `[lo, hi]`, returning `Unavailable` if `Unavailable`.
390    ///
391    /// If `Scalar(v)`, returns `Scalar(v.clamp(lo, hi))`. Useful for bounding oscillators
392    /// such as RSI to valid ranges after arithmetic transforms.
393    ///
394    /// # Example
395    /// ```rust
396    /// use fin_primitives::signals::SignalValue;
397    /// use rust_decimal_macros::dec;
398    ///
399    /// let v = SignalValue::Scalar(dec!(105));
400    /// assert_eq!(v.clamp(dec!(0), dec!(100)), SignalValue::Scalar(dec!(100)));
401    /// ```
402    pub fn clamp(self, lo: Decimal, hi: Decimal) -> SignalValue {
403        match self {
404            SignalValue::Scalar(d) => SignalValue::Scalar(d.clamp(lo, hi)),
405            SignalValue::Unavailable => SignalValue::Unavailable,
406        }
407    }
408
409    /// Divides two signals: `Scalar(a) / Scalar(b)`.
410    ///
411    /// Returns `Unavailable` if either operand is `Unavailable` or `b` is zero.
412    pub fn div(self, other: SignalValue) -> SignalValue {
413        match (self, other) {
414            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => {
415                if b.is_zero() {
416                    SignalValue::Unavailable
417                } else {
418                    match a.checked_div(b) {
419                        Some(result) => SignalValue::Scalar(result),
420                        None => SignalValue::Unavailable,
421                    }
422                }
423            }
424            _ => SignalValue::Unavailable,
425        }
426    }
427
428    /// Returns `true` if the scalar value is strictly positive. `Unavailable` returns `false`.
429    pub fn is_positive(&self) -> bool {
430        matches!(self, SignalValue::Scalar(d) if *d > Decimal::ZERO)
431    }
432
433    /// Returns `true` if the scalar value is strictly negative. `Unavailable` returns `false`.
434    pub fn is_negative(&self) -> bool {
435        matches!(self, SignalValue::Scalar(d) if *d < Decimal::ZERO)
436    }
437
438    /// Returns `default` if this is `Unavailable`; otherwise returns the scalar value.
439    pub fn if_unavailable(self, default: Decimal) -> Decimal {
440        match self {
441            SignalValue::Scalar(v) => v,
442            SignalValue::Unavailable => default,
443        }
444    }
445
446    /// Returns `true` if the scalar value is strictly above `threshold`.
447    ///
448    /// `Unavailable` always returns `false`.
449    pub fn is_above(&self, threshold: Decimal) -> bool {
450        matches!(self, SignalValue::Scalar(d) if *d > threshold)
451    }
452
453    /// Returns `true` if the scalar value is strictly below `threshold`.
454    ///
455    /// `Unavailable` always returns `false`.
456    pub fn is_below(&self, threshold: Decimal) -> bool {
457        matches!(self, SignalValue::Scalar(d) if *d < threshold)
458    }
459
460    /// Rounds the scalar to `dp` decimal places using banker's rounding.
461    ///
462    /// Returns `Unavailable` unchanged.
463    pub fn round(self, dp: u32) -> SignalValue {
464        match self {
465            SignalValue::Scalar(d) => SignalValue::Scalar(d.round_dp(dp)),
466            SignalValue::Unavailable => SignalValue::Unavailable,
467        }
468    }
469
470    /// Converts to `Option<Decimal>`: `Some(d)` for `Scalar(d)`, `None` for `Unavailable`.
471    pub fn to_option(self) -> Option<Decimal> {
472        match self {
473            SignalValue::Scalar(d) => Some(d),
474            SignalValue::Unavailable => None,
475        }
476    }
477
478    /// Converts to `Option<f64>`: `Some(f64)` for `Scalar`, `None` for `Unavailable`.
479    ///
480    /// Precision may be lost in the `Decimal → f64` conversion.
481    pub fn as_f64(&self) -> Option<f64> {
482        use rust_decimal::prelude::ToPrimitive;
483        match self {
484            SignalValue::Scalar(d) => d.to_f64(),
485            SignalValue::Unavailable => None,
486        }
487    }
488
489    /// Returns the element-wise maximum of two signals.
490    ///
491    /// `Scalar(a).max(Scalar(b)) = Scalar(max(a, b))`.
492    /// Returns `Unavailable` if either operand is `Unavailable`.
493    pub fn max(self, other: SignalValue) -> SignalValue {
494        match (self, other) {
495            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(a.max(b)),
496            _ => SignalValue::Unavailable,
497        }
498    }
499
500    /// Returns the element-wise minimum of two signals.
501    ///
502    /// `Scalar(a).min(Scalar(b)) = Scalar(min(a, b))`.
503    /// Returns `Unavailable` if either operand is `Unavailable`.
504    pub fn min(self, other: SignalValue) -> SignalValue {
505        match (self, other) {
506            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar(a.min(b)),
507            _ => SignalValue::Unavailable,
508        }
509    }
510
511    /// Returns `Scalar(-1)`, `Scalar(0)`, or `Scalar(1)` based on the sign of the value.
512    ///
513    /// Returns `Unavailable` if the value is unavailable.
514    pub fn signum(self) -> SignalValue {
515        match self {
516            SignalValue::Scalar(v) => {
517                let s = if v > Decimal::ZERO {
518                    Decimal::ONE
519                } else if v < Decimal::ZERO {
520                    -Decimal::ONE
521                } else {
522                    Decimal::ZERO
523                };
524                SignalValue::Scalar(s)
525            }
526            SignalValue::Unavailable => SignalValue::Unavailable,
527        }
528    }
529
530    /// Returns the square root of the scalar value.
531    ///
532    /// Uses f64 intermediate computation. Returns `Unavailable` if the value is
533    /// negative or unavailable.
534    ///
535    /// ```rust
536    /// use fin_primitives::signals::SignalValue;
537    /// use rust_decimal_macros::dec;
538    ///
539    /// let v = SignalValue::Scalar(dec!(4));
540    /// if let SignalValue::Scalar(r) = v.sqrt() {
541    ///     assert!((r - dec!(2)).abs() < dec!(0.00001));
542    /// }
543    /// ```
544    pub fn sqrt(self) -> SignalValue {
545        use rust_decimal::prelude::ToPrimitive;
546        match self {
547            SignalValue::Scalar(v) => {
548                if v < Decimal::ZERO {
549                    return SignalValue::Unavailable;
550                }
551                let f = v.to_f64().unwrap_or(0.0).sqrt();
552                Decimal::try_from(f)
553                    .map(SignalValue::Scalar)
554                    .unwrap_or(SignalValue::Unavailable)
555            }
556            SignalValue::Unavailable => SignalValue::Unavailable,
557        }
558    }
559
560    /// Raises the scalar value to an integer power.
561    ///
562    /// Returns `Unavailable` if the value is unavailable.
563    ///
564    /// ```rust
565    /// use fin_primitives::signals::SignalValue;
566    /// use rust_decimal_macros::dec;
567    ///
568    /// assert_eq!(SignalValue::Scalar(dec!(3)).pow(2), SignalValue::Scalar(dec!(9)));
569    /// ```
570    pub fn pow(self, exp: u32) -> SignalValue {
571        match self {
572            SignalValue::Scalar(v) => {
573                let mut result = Decimal::ONE;
574                for _ in 0..exp {
575                    result *= v;
576                }
577                SignalValue::Scalar(result)
578            }
579            SignalValue::Unavailable => SignalValue::Unavailable,
580        }
581    }
582
583    /// Returns the natural logarithm of the scalar value.
584    ///
585    /// Returns `Unavailable` if the value is ≤ 0 or unavailable.
586    ///
587    /// ```rust
588    /// use fin_primitives::signals::SignalValue;
589    /// use rust_decimal_macros::dec;
590    ///
591    /// let v = SignalValue::Scalar(dec!(1));
592    /// assert_eq!(v.ln(), SignalValue::Scalar(dec!(0)));
593    /// assert_eq!(SignalValue::Scalar(dec!(-1)).ln(), SignalValue::Unavailable);
594    /// ```
595    pub fn ln(self) -> SignalValue {
596        use rust_decimal::prelude::ToPrimitive;
597        match self {
598            SignalValue::Scalar(v) => {
599                if v <= Decimal::ZERO {
600                    return SignalValue::Unavailable;
601                }
602                let f = v.to_f64().unwrap_or(0.0).ln();
603                if f.is_finite() {
604                    Decimal::try_from(f)
605                        .map(SignalValue::Scalar)
606                        .unwrap_or(SignalValue::Unavailable)
607                } else {
608                    SignalValue::Unavailable
609                }
610            }
611            SignalValue::Unavailable => SignalValue::Unavailable,
612        }
613    }
614
615    /// Returns `true` if this value is above `threshold` while `prev` was at or below it.
616    ///
617    /// Detects an upward crossing of a threshold level. Both values must be scalar.
618    ///
619    /// ```rust
620    /// use fin_primitives::signals::SignalValue;
621    /// use rust_decimal_macros::dec;
622    ///
623    /// let prev = SignalValue::Scalar(dec!(49));
624    /// let curr = SignalValue::Scalar(dec!(51));
625    /// assert!(curr.cross_above(dec!(50), prev));
626    /// ```
627    pub fn cross_above(self, threshold: Decimal, prev: SignalValue) -> bool {
628        matches!(
629            (self, prev),
630            (SignalValue::Scalar(curr), SignalValue::Scalar(p))
631            if curr > threshold && p <= threshold
632        )
633    }
634
635    /// Returns `true` if this value is below `threshold` while `prev` was at or above it.
636    ///
637    /// Detects a downward crossing of a threshold level. Both values must be scalar.
638    ///
639    /// ```rust
640    /// use fin_primitives::signals::SignalValue;
641    /// use rust_decimal_macros::dec;
642    ///
643    /// let prev = SignalValue::Scalar(dec!(51));
644    /// let curr = SignalValue::Scalar(dec!(49));
645    /// assert!(curr.cross_below(dec!(50), prev));
646    /// ```
647    pub fn cross_below(self, threshold: Decimal, prev: SignalValue) -> bool {
648        matches!(
649            (self, prev),
650            (SignalValue::Scalar(curr), SignalValue::Scalar(p))
651            if curr < threshold && p >= threshold
652        )
653    }
654
655    /// Returns this scalar as a percentage of `other`.
656    ///
657    /// `result = (self / other) × 100`
658    ///
659    /// Returns `Unavailable` if either value is unavailable or `other` is zero.
660    ///
661    /// ```rust
662    /// use fin_primitives::signals::SignalValue;
663    /// use rust_decimal_macros::dec;
664    ///
665    /// let v = SignalValue::Scalar(dec!(50));
666    /// let base = SignalValue::Scalar(dec!(200));
667    /// assert_eq!(v.pct_of(base), SignalValue::Scalar(dec!(25)));
668    /// ```
669    pub fn pct_of(self, other: SignalValue) -> SignalValue {
670        match (self, other) {
671            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => {
672                if b.is_zero() {
673                    return SignalValue::Unavailable;
674                }
675                match a.checked_div(b) {
676                    Some(r) => SignalValue::Scalar(r * Decimal::ONE_HUNDRED),
677                    None => SignalValue::Unavailable,
678                }
679            }
680            _ => SignalValue::Unavailable,
681        }
682    }
683
684    /// Returns `-1`, `0`, or `+1` depending on how this value crosses `threshold` from `prev`.
685    ///
686    /// - `+1` if `prev <= threshold` and `self > threshold` (upward crossing)
687    /// - `-1` if `prev >= threshold` and `self < threshold` (downward crossing)
688    /// - `0` otherwise (no crossing, or either value is unavailable)
689    ///
690    /// ```rust
691    /// use fin_primitives::signals::SignalValue;
692    /// use rust_decimal_macros::dec;
693    ///
694    /// let prev = SignalValue::Scalar(dec!(49));
695    /// let curr = SignalValue::Scalar(dec!(51));
696    /// assert_eq!(curr.threshold_cross(dec!(50), prev), SignalValue::Scalar(dec!(1)));
697    /// ```
698    pub fn threshold_cross(self, threshold: Decimal, prev: SignalValue) -> SignalValue {
699        match (self, prev) {
700            (SignalValue::Scalar(curr), SignalValue::Scalar(p)) => {
701                if curr > threshold && p <= threshold {
702                    SignalValue::Scalar(Decimal::ONE)
703                } else if curr < threshold && p >= threshold {
704                    SignalValue::Scalar(Decimal::NEGATIVE_ONE)
705                } else {
706                    SignalValue::Scalar(Decimal::ZERO)
707                }
708            }
709            _ => SignalValue::Scalar(Decimal::ZERO),
710        }
711    }
712
713    /// Returns `e^x`. Returns `Unavailable` if the value is `Unavailable` or if `x > 700`
714    /// (overflow guard — `e^709 ≈ f64::MAX`).
715    pub fn exp(self) -> SignalValue {
716        match self {
717            SignalValue::Unavailable => SignalValue::Unavailable,
718            SignalValue::Scalar(v) => {
719                if v > Decimal::from(700) {
720                    return SignalValue::Unavailable;
721                }
722                use rust_decimal::prelude::ToPrimitive;
723                let f = v.to_f64().unwrap_or(f64::NAN);
724                if f.is_nan() { return SignalValue::Unavailable; }
725                match Decimal::try_from(f.exp()) {
726                    Ok(d) => SignalValue::Scalar(d),
727                    Err(_) => SignalValue::Unavailable,
728                }
729            }
730        }
731    }
732
733    /// Returns the floor of the value (rounds toward negative infinity).
734    pub fn floor(self) -> SignalValue {
735        self.map(|v| v.floor())
736    }
737
738    /// Returns the ceiling of the value (rounds toward positive infinity).
739    pub fn ceil(self) -> SignalValue {
740        self.map(|v| v.ceil())
741    }
742
743    /// Returns `1 / self`. Returns `Unavailable` if the value is zero or `Unavailable`.
744    pub fn reciprocal(self) -> SignalValue {
745        match self {
746            SignalValue::Unavailable => SignalValue::Unavailable,
747            SignalValue::Scalar(v) => {
748                if v.is_zero() {
749                    SignalValue::Unavailable
750                } else {
751                    SignalValue::Scalar(Decimal::ONE / v)
752                }
753            }
754        }
755    }
756
757    /// Returns `(self / total) * 100`. Returns `Unavailable` if `total` is zero or either
758    /// value is `Unavailable`.
759    pub fn to_percent(self, total: SignalValue) -> SignalValue {
760        match (self, total) {
761            (SignalValue::Scalar(v), SignalValue::Scalar(t)) => {
762                if t.is_zero() {
763                    SignalValue::Unavailable
764                } else {
765                    SignalValue::Scalar(v / t * Decimal::ONE_HUNDRED)
766                }
767            }
768            _ => SignalValue::Unavailable,
769        }
770    }
771
772    /// Returns the arctangent of the value in radians. Returns `Unavailable` if unavailable.
773    pub fn atan(self) -> SignalValue {
774        match self {
775            SignalValue::Unavailable => SignalValue::Unavailable,
776            SignalValue::Scalar(v) => {
777                use rust_decimal::prelude::ToPrimitive;
778                let f: f64 = v.to_f64().unwrap_or(f64::NAN);
779                match Decimal::try_from(f.atan()) {
780                    Ok(d) => SignalValue::Scalar(d),
781                    Err(_) => SignalValue::Unavailable,
782                }
783            }
784        }
785    }
786
787    /// Returns the hyperbolic tangent of the value. Returns `Unavailable` if unavailable.
788    ///
789    /// `tanh` maps any real value to `(-1, 1)` — useful for normalising unbounded signals.
790    pub fn tanh(self) -> SignalValue {
791        match self {
792            SignalValue::Unavailable => SignalValue::Unavailable,
793            SignalValue::Scalar(v) => {
794                use rust_decimal::prelude::ToPrimitive;
795                let f: f64 = v.to_f64().unwrap_or(f64::NAN);
796                match Decimal::try_from(f.tanh()) {
797                    Ok(d) => SignalValue::Scalar(d),
798                    Err(_) => SignalValue::Unavailable,
799                }
800            }
801        }
802    }
803
804    /// Returns the hyperbolic sine of the scalar value.
805    ///
806    /// Returns [`SignalValue::Unavailable`] if the result is non-finite.
807    pub fn sinh(self) -> SignalValue {
808        match self {
809            SignalValue::Unavailable => SignalValue::Unavailable,
810            SignalValue::Scalar(v) => {
811                use rust_decimal::prelude::ToPrimitive;
812                let f: f64 = v.to_f64().unwrap_or(f64::NAN);
813                match Decimal::try_from(f.sinh()) {
814                    Ok(d) => SignalValue::Scalar(d),
815                    Err(_) => SignalValue::Unavailable,
816                }
817            }
818        }
819    }
820
821    /// Returns the hyperbolic cosine of the scalar value.
822    ///
823    /// Returns [`SignalValue::Unavailable`] if the result is non-finite.
824    pub fn cosh(self) -> SignalValue {
825        match self {
826            SignalValue::Unavailable => SignalValue::Unavailable,
827            SignalValue::Scalar(v) => {
828                use rust_decimal::prelude::ToPrimitive;
829                let f: f64 = v.to_f64().unwrap_or(f64::NAN);
830                match Decimal::try_from(f.cosh()) {
831                    Ok(d) => SignalValue::Scalar(d),
832                    Err(_) => SignalValue::Unavailable,
833                }
834            }
835        }
836    }
837
838    /// Rounds the scalar to `dp` decimal places using banker's rounding.
839    ///
840    /// Returns [`SignalValue::Unavailable`] unchanged.
841    pub fn round_to(self, dp: u32) -> SignalValue {
842        match self {
843            SignalValue::Unavailable => SignalValue::Unavailable,
844            SignalValue::Scalar(v) => SignalValue::Scalar(v.round_dp(dp)),
845        }
846    }
847
848    /// Returns `true` if this is a `Scalar` with a non-zero value.
849    pub fn to_bool(&self) -> bool {
850        matches!(self, SignalValue::Scalar(v) if !v.is_zero())
851    }
852
853    /// Multiplies the scalar by `factor`, returning the product as a new `SignalValue`.
854    ///
855    /// Returns [`SignalValue::Unavailable`] unchanged.
856    pub fn scale_by(self, factor: rust_decimal::Decimal) -> SignalValue {
857        match self {
858            SignalValue::Unavailable => SignalValue::Unavailable,
859            SignalValue::Scalar(v) => SignalValue::Scalar(v * factor),
860        }
861    }
862
863    /// Returns `true` if this is `Scalar(0)`.
864    pub fn is_zero(&self) -> bool {
865        matches!(self, SignalValue::Scalar(v) if v.is_zero())
866    }
867
868    /// Absolute difference between two `SignalValue`s.
869    ///
870    /// Returns `Unavailable` if either operand is `Unavailable`.
871    pub fn delta(self, other: SignalValue) -> SignalValue {
872        match (self, other) {
873            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar((a - b).abs()),
874            _ => SignalValue::Unavailable,
875        }
876    }
877
878    /// Linear interpolation: `self * (1 - t) + other * t`.
879    ///
880    /// `t` is clamped to `[0, 1]`. Returns `Unavailable` if either operand is `Unavailable`.
881    pub fn lerp(self, other: SignalValue, t: Decimal) -> SignalValue {
882        match (self, other) {
883            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => {
884                let t_clamped = t.max(Decimal::ZERO).min(Decimal::ONE);
885                SignalValue::Scalar(a * (Decimal::ONE - t_clamped) + b * t_clamped)
886            }
887            _ => SignalValue::Unavailable,
888        }
889    }
890
891    /// Returns `true` if `self` is a scalar strictly greater than `other`.
892    ///
893    /// Returns `false` if either operand is `Unavailable`.
894    pub fn gt(&self, other: &SignalValue) -> bool {
895        match (self, other) {
896            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => a > b,
897            _ => false,
898        }
899    }
900
901    /// Returns `true` if `self` is a scalar strictly less than `other`.
902    ///
903    /// Returns `false` if either operand is `Unavailable`.
904    pub fn lt(&self, other: &SignalValue) -> bool {
905        match (self, other) {
906            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => a < b,
907            _ => false,
908        }
909    }
910
911    /// Returns `true` if both are scalars and `|self - other| <= tolerance`.
912    ///
913    /// Returns `false` if either is `Unavailable`.
914    pub fn eq_approx(&self, other: &SignalValue, tolerance: Decimal) -> bool {
915        match (self, other) {
916            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => (a - b).abs() <= tolerance,
917            _ => false,
918        }
919    }
920
921    /// Two-argument arctangent: `atan2(self, x)` in radians.
922    ///
923    /// Treats `self` as the `y` argument. Returns `Unavailable` if either is `Unavailable`.
924    pub fn atan2(self, x: SignalValue) -> SignalValue {
925        match (self, x) {
926            (SignalValue::Scalar(y), SignalValue::Scalar(xv)) => {
927                use rust_decimal::prelude::ToPrimitive;
928                let yf: f64 = y.to_f64().unwrap_or(f64::NAN);
929                let xf: f64 = xv.to_f64().unwrap_or(f64::NAN);
930                match Decimal::try_from(yf.atan2(xf)) {
931                    Ok(d) => SignalValue::Scalar(d),
932                    Err(_) => SignalValue::Unavailable,
933                }
934            }
935            _ => SignalValue::Unavailable,
936        }
937    }
938
939    /// Returns `true` if both scalars have the same sign (both positive or both negative).
940    ///
941    /// Zero is treated as positive. Returns `false` if either is `Unavailable`.
942    pub fn sign_match(&self, other: &SignalValue) -> bool {
943        match (self, other) {
944            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => {
945                (a >= &Decimal::ZERO) == (b >= &Decimal::ZERO)
946            }
947            _ => false,
948        }
949    }
950
951    /// Adds a raw `Decimal` to this scalar value.
952    ///
953    /// Returns `Unavailable` if `self` is `Unavailable`.
954    pub fn add_scalar(self, delta: Decimal) -> SignalValue {
955        match self {
956            SignalValue::Scalar(v) => SignalValue::Scalar(v + delta),
957            SignalValue::Unavailable => SignalValue::Unavailable,
958        }
959    }
960
961    /// Maps the scalar with `f`, falling back to `default` if `Unavailable`.
962    pub fn map_or(self, default: Decimal, f: impl FnOnce(Decimal) -> Decimal) -> Decimal {
963        match self {
964            SignalValue::Scalar(v) => f(v),
965            SignalValue::Unavailable => default,
966        }
967    }
968
969    /// Returns `true` if `self >= other` (both scalar). Returns `false` if either is `Unavailable`.
970    pub fn gte(&self, other: &SignalValue) -> bool {
971        match (self, other) {
972            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => a >= b,
973            _ => false,
974        }
975    }
976
977    /// Returns `true` if `self <= other` (both scalar). Returns `false` if either is `Unavailable`.
978    pub fn lte(&self, other: &SignalValue) -> bool {
979        match (self, other) {
980            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => a <= b,
981            _ => false,
982        }
983    }
984
985    /// Express this scalar as a percentage of `base`: `self / base * 100`.
986    ///
987    /// Returns `Unavailable` if `self` is `Unavailable` or `base` is zero.
988    pub fn as_percent(self, base: Decimal) -> SignalValue {
989        if base.is_zero() { return SignalValue::Unavailable; }
990        match self {
991            SignalValue::Scalar(v) => SignalValue::Scalar(v / base * Decimal::ONE_HUNDRED),
992            SignalValue::Unavailable => SignalValue::Unavailable,
993        }
994    }
995
996    /// Returns `true` if this scalar is in `[lo, hi]` (inclusive).
997    ///
998    /// Returns `false` if `Unavailable`.
999    pub fn within_range(&self, lo: Decimal, hi: Decimal) -> bool {
1000        match self {
1001            SignalValue::Scalar(v) => v >= &lo && v <= &hi,
1002            SignalValue::Unavailable => false,
1003        }
1004    }
1005
1006    /// Caps the scalar at `max_val`. Returns `Unavailable` if `self` is `Unavailable`.
1007    pub fn cap_at(self, max_val: Decimal) -> SignalValue {
1008        match self {
1009            SignalValue::Scalar(v) => SignalValue::Scalar(v.min(max_val)),
1010            SignalValue::Unavailable => SignalValue::Unavailable,
1011        }
1012    }
1013
1014    /// Floors the scalar at `min_val`. Returns `Unavailable` if `self` is `Unavailable`.
1015    pub fn floor_at(self, min_val: Decimal) -> SignalValue {
1016        match self {
1017            SignalValue::Scalar(v) => SignalValue::Scalar(v.max(min_val)),
1018            SignalValue::Unavailable => SignalValue::Unavailable,
1019        }
1020    }
1021
1022    /// Round the scalar to the nearest multiple of `step`. Returns `Unavailable` if unavailable
1023    /// or `step` is zero.
1024    pub fn quantize(self, step: Decimal) -> SignalValue {
1025        if step.is_zero() {
1026            return SignalValue::Unavailable;
1027        }
1028        match self {
1029            SignalValue::Scalar(v) => SignalValue::Scalar((v / step).round() * step),
1030            SignalValue::Unavailable => SignalValue::Unavailable,
1031        }
1032    }
1033
1034    /// Absolute difference between `self` and `other`. Returns `Unavailable` if either is unavailable.
1035    pub fn distance_to(self, other: SignalValue) -> SignalValue {
1036        match (self, other) {
1037            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => SignalValue::Scalar((a - b).abs()),
1038            _ => SignalValue::Unavailable,
1039        }
1040    }
1041
1042    /// Weighted blend: `self * (1 - weight) + other * weight`, clamping `weight` to `[0, 1]`.
1043    /// Returns `Unavailable` if either operand is unavailable.
1044    pub fn blend(self, other: SignalValue, weight: Decimal) -> SignalValue {
1045        match (self, other) {
1046            (SignalValue::Scalar(a), SignalValue::Scalar(b)) => {
1047                let w = weight.max(Decimal::ZERO).min(Decimal::ONE);
1048                SignalValue::Scalar(a * (Decimal::ONE - w) + b * w)
1049            }
1050            _ => SignalValue::Unavailable,
1051        }
1052    }
1053}
1054
1055impl From<Decimal> for SignalValue {
1056    fn from(d: Decimal) -> Self {
1057        SignalValue::Scalar(d)
1058    }
1059}
1060
1061impl std::fmt::Display for SignalValue {
1062    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1063        match self {
1064            SignalValue::Scalar(d) => write!(f, "{d}"),
1065            SignalValue::Unavailable => write!(f, "Unavailable"),
1066        }
1067    }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::*;
1073    use rust_decimal_macros::dec;
1074
1075    #[test]
1076    fn test_signal_value_and_then_scalar_returns_value() {
1077        let v = SignalValue::Scalar(dec!(50));
1078        let result = v.and_then(|x| SignalValue::Scalar(x * dec!(2)));
1079        assert_eq!(result, SignalValue::Scalar(dec!(100)));
1080    }
1081
1082    #[test]
1083    fn test_signal_value_and_then_scalar_can_return_unavailable() {
1084        let v = SignalValue::Scalar(dec!(5));
1085        let result = v.and_then(|x| {
1086            if x > dec!(10) { SignalValue::Scalar(x) } else { SignalValue::Unavailable }
1087        });
1088        assert_eq!(result, SignalValue::Unavailable);
1089    }
1090
1091    #[test]
1092    fn test_signal_value_and_then_unavailable_short_circuits() {
1093        let v = SignalValue::Unavailable;
1094        let result = v.and_then(|_| SignalValue::Scalar(dec!(999)));
1095        assert_eq!(result, SignalValue::Unavailable);
1096    }
1097
1098    #[test]
1099    fn test_signal_value_map_scalar() {
1100        let v = SignalValue::Scalar(dec!(10));
1101        assert_eq!(v.map(|x| x + dec!(5)), SignalValue::Scalar(dec!(15)));
1102    }
1103
1104    #[test]
1105    fn test_signal_value_map_unavailable() {
1106        assert_eq!(SignalValue::Unavailable.map(|x| x + dec!(5)), SignalValue::Unavailable);
1107    }
1108
1109    #[test]
1110    fn test_signal_value_zip_with_both_scalar() {
1111        let a = SignalValue::Scalar(dec!(10));
1112        let b = SignalValue::Scalar(dec!(3));
1113        assert_eq!(a.zip_with(b, |x, y| x - y), SignalValue::Scalar(dec!(7)));
1114    }
1115
1116    #[test]
1117    fn test_signal_value_zip_with_one_unavailable() {
1118        let a = SignalValue::Scalar(dec!(10));
1119        assert_eq!(a.zip_with(SignalValue::Unavailable, |x, y| x + y), SignalValue::Unavailable);
1120    }
1121
1122    #[test]
1123    fn test_signal_value_clamp_above_hi() {
1124        let v = SignalValue::Scalar(dec!(105));
1125        assert_eq!(v.clamp(dec!(0), dec!(100)), SignalValue::Scalar(dec!(100)));
1126    }
1127
1128    #[test]
1129    fn test_signal_value_clamp_below_lo() {
1130        let v = SignalValue::Scalar(dec!(-5));
1131        assert_eq!(v.clamp(dec!(0), dec!(100)), SignalValue::Scalar(dec!(0)));
1132    }
1133
1134    #[test]
1135    fn test_signal_value_clamp_within_range() {
1136        let v = SignalValue::Scalar(dec!(50));
1137        assert_eq!(v.clamp(dec!(0), dec!(100)), SignalValue::Scalar(dec!(50)));
1138    }
1139
1140    #[test]
1141    fn test_signal_value_clamp_unavailable_passthrough() {
1142        assert_eq!(SignalValue::Unavailable.clamp(dec!(0), dec!(100)), SignalValue::Unavailable);
1143    }
1144
1145    #[test]
1146    fn test_signal_value_exp_zero() {
1147        // e^0 = 1
1148        let v = SignalValue::Scalar(dec!(0));
1149        if let SignalValue::Scalar(r) = v.exp() {
1150            let diff = (r - dec!(1)).abs();
1151            assert!(diff < dec!(0.0001), "e^0 should be ~1, got {r}");
1152        } else { panic!("expected Scalar"); }
1153    }
1154
1155    #[test]
1156    fn test_signal_value_exp_overflow_guard() {
1157        assert_eq!(SignalValue::Scalar(dec!(701)).exp(), SignalValue::Unavailable);
1158    }
1159
1160    #[test]
1161    fn test_signal_value_exp_unavailable_passthrough() {
1162        assert_eq!(SignalValue::Unavailable.exp(), SignalValue::Unavailable);
1163    }
1164
1165    #[test]
1166    fn test_signal_value_floor_positive() {
1167        assert_eq!(SignalValue::Scalar(dec!(3.7)).floor(), SignalValue::Scalar(dec!(3)));
1168    }
1169
1170    #[test]
1171    fn test_signal_value_floor_negative() {
1172        assert_eq!(SignalValue::Scalar(dec!(-2.3)).floor(), SignalValue::Scalar(dec!(-3)));
1173    }
1174
1175    #[test]
1176    fn test_signal_value_ceil_positive() {
1177        assert_eq!(SignalValue::Scalar(dec!(3.2)).ceil(), SignalValue::Scalar(dec!(4)));
1178    }
1179
1180    #[test]
1181    fn test_signal_value_ceil_integer() {
1182        assert_eq!(SignalValue::Scalar(dec!(5)).ceil(), SignalValue::Scalar(dec!(5)));
1183    }
1184}
1185
1186/// A stateful indicator that updates on each new bar input.
1187///
1188/// # Implementors
1189/// - [`indicators::Sma`]: simple moving average
1190/// - [`indicators::Ema`]: exponential moving average
1191/// - [`indicators::Rsi`]: relative strength index
1192pub trait Signal: Send {
1193    /// Returns the name of this signal (unique within a pipeline).
1194    fn name(&self) -> &str;
1195
1196    /// Updates the signal with a [`BarInput`] and returns the current value.
1197    ///
1198    /// Accepting `BarInput` rather than `&OhlcvBar` lets signals be used on any
1199    /// price stream, not just OHLCV data.
1200    ///
1201    /// # Returns
1202    /// - `Ok(SignalValue::Scalar(v))` if enough bars have been accumulated
1203    /// - `Ok(SignalValue::Unavailable)` if fewer than `period` bars have been seen
1204    ///
1205    /// # Errors
1206    /// Returns [`FinError`] on arithmetic failure.
1207    fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError>;
1208
1209    /// Convenience wrapper: converts `bar` to [`BarInput`] and calls [`Self::update`].
1210    fn update_bar(&mut self, bar: &OhlcvBar) -> Result<SignalValue, FinError> {
1211        self.update(&BarInput::from(bar))
1212    }
1213
1214    /// Returns `true` if the signal has accumulated enough bars to produce a value.
1215    fn is_ready(&self) -> bool;
1216
1217    /// Returns the number of bars required before the signal produces a value.
1218    fn period(&self) -> usize;
1219
1220    /// Resets the signal to its initial state as if no bars had been seen.
1221    ///
1222    /// After calling `reset()`, `is_ready()` returns `false` and the next `period`
1223    /// bars will warm up the indicator again. Useful for walk-forward backtesting
1224    /// without creating a new indicator instance.
1225    fn reset(&mut self);
1226
1227    /// Feed a slice of historical bars to prime the indicator in one call.
1228    ///
1229    /// Equivalent to calling [`update`](Self::update) for each bar in sequence.
1230    /// Returns the value after the final bar, or `Ok(SignalValue::Unavailable)`
1231    /// if `bars` is empty.
1232    ///
1233    /// # Errors
1234    /// Propagates the first [`FinError`] returned by [`update`](Self::update).
1235    fn warm_up(&mut self, bars: &[BarInput]) -> Result<SignalValue, FinError> {
1236        let mut last = SignalValue::Unavailable;
1237        for bar in bars {
1238            last = self.update(bar)?;
1239        }
1240        Ok(last)
1241    }
1242}