tesser_indicators/
core.rs1use rust_decimal::{prelude::FromPrimitive, Decimal};
4use tesser_core::Candle;
5use thiserror::Error;
6
7use crate::combinators::PipedIndicator;
8
9pub trait Input {
11 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#[derive(Debug, Error, PartialEq)]
44pub enum IndicatorError {
45 #[error("{name} requires period > 0 (got {period})")]
47 InvalidPeriod {
48 name: &'static str,
50 period: usize,
52 },
53 #[error("{name} parameter '{parameter}' must be non-negative (got {value})")]
55 InvalidParameter {
56 name: &'static str,
58 parameter: &'static str,
60 value: Decimal,
62 },
63}
64
65impl IndicatorError {
66 pub fn invalid_period(name: &'static str, period: usize) -> Self {
68 Self::InvalidPeriod { name, period }
69 }
70
71 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
81pub trait Indicator {
83 type Input: Input;
85 type Output;
87
88 fn next(&mut self, input: Self::Input) -> Option<Self::Output>;
90
91 fn reset(&mut self);
93
94 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}