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