Skip to main content

quant_indicators/
combinators.rs

1//! Indicator combinators for mathematical composition.
2//!
3//! Combinators allow combining indicators mathematically:
4//! - `Diff<A, B>`: A - B
5//! - `Ratio<A, B>`: A / B
6//! - `Lag<I>`: Shift values by N periods
7//! - `Scale<I>`: Multiply by constant
8
9use chrono::{DateTime, Utc};
10use quant_primitives::Candle;
11use rust_decimal::Decimal;
12
13use crate::error::IndicatorError;
14use crate::indicator::Indicator;
15use crate::series::Series;
16
17/// Alignment info for combining two series from the end (most recent values).
18struct SeriesAlignment {
19    output_len: usize,
20    offset_a: usize,
21    offset_b: usize,
22}
23
24/// Align two series by taking intersection from the end (more recent values).
25///
26/// Returns alignment info: output length and offsets into each series.
27fn align_series_from_end(len_a: usize, len_b: usize) -> SeriesAlignment {
28    let output_len = len_a.min(len_b);
29    SeriesAlignment {
30        output_len,
31        offset_a: len_a - output_len,
32        offset_b: len_b - output_len,
33    }
34}
35
36/// Apply a binary operation to two aligned series.
37fn apply_binary_op<F>(
38    values_a: &[(DateTime<Utc>, Decimal)],
39    values_b: &[(DateTime<Utc>, Decimal)],
40    op: F,
41) -> Vec<(DateTime<Utc>, Decimal)>
42where
43    F: Fn(Decimal, Decimal) -> Decimal,
44{
45    let align = align_series_from_end(values_a.len(), values_b.len());
46    let mut result = Vec::with_capacity(align.output_len);
47
48    for i in 0..align.output_len {
49        let (ts, val_a) = values_a[align.offset_a + i];
50        let val_b = values_b[align.offset_b + i].1;
51        result.push((ts, op(val_a, val_b)));
52    }
53
54    result
55}
56
57/// Difference between two indicators: A - B
58#[derive(Debug, Clone)]
59pub struct Diff<A, B> {
60    a: A,
61    b: B,
62    name: String,
63}
64
65impl<A: Indicator, B: Indicator> Diff<A, B> {
66    /// Create a new Diff combinator.
67    pub fn new(a: A, b: B) -> Self {
68        let name = format!("Diff({},{})", a.name(), b.name());
69        Self { a, b, name }
70    }
71}
72
73impl<A: Indicator, B: Indicator> Indicator for Diff<A, B> {
74    fn name(&self) -> &str {
75        &self.name
76    }
77
78    fn warmup_period(&self) -> usize {
79        self.a.warmup_period().max(self.b.warmup_period())
80    }
81
82    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
83        let series_a = self.a.compute(candles)?;
84        let series_b = self.b.compute(candles)?;
85
86        let values = apply_binary_op(series_a.values(), series_b.values(), |a, b| a - b);
87
88        Ok(Series::new(values))
89    }
90}
91
92/// Ratio between two indicators: A / B
93#[derive(Debug, Clone)]
94pub struct Ratio<A, B> {
95    a: A,
96    b: B,
97    name: String,
98}
99
100impl<A: Indicator, B: Indicator> Ratio<A, B> {
101    /// Create a new Ratio combinator.
102    pub fn new(a: A, b: B) -> Self {
103        let name = format!("Ratio({},{})", a.name(), b.name());
104        Self { a, b, name }
105    }
106}
107
108impl<A: Indicator, B: Indicator> Indicator for Ratio<A, B> {
109    fn name(&self) -> &str {
110        &self.name
111    }
112
113    fn warmup_period(&self) -> usize {
114        self.a.warmup_period().max(self.b.warmup_period())
115    }
116
117    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
118        let series_a = self.a.compute(candles)?;
119        let series_b = self.b.compute(candles)?;
120
121        let values = apply_binary_op(series_a.values(), series_b.values(), |a, b| {
122            // Avoid division by zero
123            if b.is_zero() {
124                Decimal::ZERO
125            } else {
126                a / b
127            }
128        });
129
130        Ok(Series::new(values))
131    }
132}
133
134/// Lag shifts indicator values by N periods.
135///
136/// A lag of 1 means each output value is the previous period's indicator value.
137#[derive(Debug, Clone)]
138pub struct Lag<I> {
139    inner: I,
140    periods: usize,
141    name: String,
142}
143
144impl<I: Indicator> Lag<I> {
145    /// Create a new Lag combinator.
146    ///
147    /// # Arguments
148    ///
149    /// * `inner` - The indicator to lag
150    /// * `periods` - Number of periods to lag (must be > 0)
151    pub fn new(inner: I, periods: usize) -> Result<Self, IndicatorError> {
152        if periods == 0 {
153            return Err(IndicatorError::InvalidParameter {
154                message: "Lag periods must be > 0".to_string(),
155            });
156        }
157        let name = format!("Lag({},{})", inner.name(), periods);
158        Ok(Self {
159            inner,
160            periods,
161            name,
162        })
163    }
164}
165
166impl<I: Indicator> Indicator for Lag<I> {
167    fn name(&self) -> &str {
168        &self.name
169    }
170
171    fn warmup_period(&self) -> usize {
172        self.inner.warmup_period() + self.periods
173    }
174
175    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
176        let inner_series = self.inner.compute(candles)?;
177        let values = inner_series.values();
178
179        if values.len() <= self.periods {
180            return Err(IndicatorError::InsufficientData {
181                required: self.inner.warmup_period() + self.periods,
182                actual: candles.len(),
183            });
184        }
185
186        // Drop the last N values, shift timestamps
187        let output_len = values.len() - self.periods;
188        let mut lagged_values = Vec::with_capacity(output_len);
189
190        for i in 0..output_len {
191            // Value from i, but timestamp from i + periods
192            let value = values[i].1;
193            let ts = values[i + self.periods].0;
194            lagged_values.push((ts, value));
195        }
196
197        Ok(Series::new(lagged_values))
198    }
199}
200
201/// Scale multiplies indicator values by a constant factor.
202#[derive(Debug, Clone)]
203pub struct Scale<I> {
204    inner: I,
205    factor: Decimal,
206    name: String,
207}
208
209impl<I: Indicator> Scale<I> {
210    /// Create a new Scale combinator.
211    pub fn new(inner: I, factor: Decimal) -> Self {
212        let name = format!("Scale({},{})", inner.name(), factor);
213        Self {
214            inner,
215            factor,
216            name,
217        }
218    }
219}
220
221impl<I: Indicator> Indicator for Scale<I> {
222    fn name(&self) -> &str {
223        &self.name
224    }
225
226    fn warmup_period(&self) -> usize {
227        self.inner.warmup_period()
228    }
229
230    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
231        let inner_series = self.inner.compute(candles)?;
232        let values: Vec<_> = inner_series
233            .values()
234            .iter()
235            .map(|(ts, v)| (*ts, *v * self.factor))
236            .collect();
237        Ok(Series::new(values))
238    }
239}
240
241#[cfg(test)]
242#[path = "combinators_tests.rs"]
243mod tests;