Skip to main content

tesser_indicators/
core.rs

1//! Core traits shared by all indicators.
2
3use rust_decimal::{prelude::FromPrimitive, Decimal};
4use tesser_core::Candle;
5use thiserror::Error;
6
7use crate::combinators::PipedIndicator;
8
9/// Provides a uniform way to extract a `Decimal` value from different input types.
10pub trait Input {
11    /// Returns the canonical decimal representation for the input.
12    fn value(&self) -> Decimal;
13}
14
15impl Input for Decimal {
16    fn value(&self) -> Decimal {
17        *self
18    }
19}
20
21impl Input for f64 {
22    fn value(&self) -> Decimal {
23        decimal_from_f64(*self)
24    }
25}
26
27impl Input for Candle {
28    fn value(&self) -> Decimal {
29        self.close
30    }
31}
32
33impl<T> Input for &T
34where
35    T: Input + ?Sized,
36{
37    fn value(&self) -> Decimal {
38        T::value(self)
39    }
40}
41
42/// Common configuration errors emitted by indicators.
43#[derive(Debug, Error, PartialEq)]
44pub enum IndicatorError {
45    /// Returned when a period of zero is provided.
46    #[error("{name} requires period > 0 (got {period})")]
47    InvalidPeriod {
48        /// Human-readable indicator name.
49        name: &'static str,
50        /// User-provided period value.
51        period: usize,
52    },
53    /// Returned when a parameter must be non-negative.
54    #[error("{name} parameter '{parameter}' must be non-negative (got {value})")]
55    InvalidParameter {
56        /// Human-readable indicator name.
57        name: &'static str,
58        /// Name of the invalid parameter (e.g., `std_multiplier`).
59        parameter: &'static str,
60        /// Provided parameter value.
61        value: Decimal,
62    },
63}
64
65impl IndicatorError {
66    /// Helper constructor for invalid period errors.
67    pub fn invalid_period(name: &'static str, period: usize) -> Self {
68        Self::InvalidPeriod { name, period }
69    }
70
71    /// Helper constructor for invalid parameter errors.
72    pub fn invalid_parameter(name: &'static str, parameter: &'static str, value: Decimal) -> Self {
73        Self::InvalidParameter {
74            name,
75            parameter,
76            value,
77        }
78    }
79}
80
81/// Core abstraction implemented by every indicator in the library.
82pub trait Indicator {
83    /// Input type accepted by the indicator.
84    type Input: Input;
85    /// Value produced after each update.
86    type Output;
87
88    /// Consumes a new data point and returns the most recent value, if available.
89    fn next(&mut self, input: Self::Input) -> Option<Self::Output>;
90
91    /// Resets the indicator to its initial state.
92    fn reset(&mut self);
93
94    /// Chains the current indicator with another indicator, feeding this output into the next.
95    fn pipe<Next>(self, next: Next) -> PipedIndicator<Self, Next>
96    where
97        Self: Sized,
98        Next: Indicator<Input = Self::Output>,
99    {
100        PipedIndicator::new(self, next)
101    }
102}
103
104fn decimal_from_f64(value: f64) -> Decimal {
105    Decimal::from_f64(value)
106        .or_else(|| Decimal::from_f64_retain(value))
107        .expect("failed to convert f64 into Decimal")
108}
109
110pub(crate) fn decimal_from_usize(value: usize) -> Decimal {
111    Decimal::from_usize(value).expect("usize should convert into Decimal")
112}
113
114#[cfg(test)]
115mod tests {
116    use std::str::FromStr;
117
118    use chrono::Utc;
119    use rust_decimal::Decimal;
120    use tesser_core::{Candle, Interval, Symbol};
121
122    use super::{decimal_from_f64, Input};
123
124    #[test]
125    fn converts_f64_to_decimal() {
126        let value = 42.1234_f64;
127        let decimal = decimal_from_f64(value);
128        assert_eq!(decimal.round_dp(4), Decimal::from_str("42.1234").unwrap());
129    }
130
131    #[test]
132    fn candle_input_uses_close_price() {
133        let candle = Candle {
134            symbol: Symbol::from("BTCUSDT"),
135            interval: Interval::OneMinute,
136            open: Decimal::from(10),
137            high: Decimal::from(15),
138            low: decimal_from_f64(9.5),
139            close: decimal_from_f64(12.5),
140            volume: Decimal::from(100),
141            timestamp: Utc::now(),
142        };
143
144        let decimal = candle.value();
145        assert_eq!(decimal.round_dp(1), Decimal::from_str("12.5").unwrap());
146    }
147
148    #[test]
149    fn reference_input_delegates_to_inner_type() {
150        let price = Decimal::from(100);
151        assert_eq!((price).value(), Decimal::from(100));
152    }
153}