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