Skip to main content

fin_primitives/signals/
mod.rs

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