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}