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