quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
//! Indicator combinators for mathematical composition.
//!
//! Combinators allow combining indicators mathematically:
//! - `Diff<A, B>`: A - B
//! - `Ratio<A, B>`: A / B
//! - `Lag<I>`: Shift values by N periods
//! - `Scale<I>`: Multiply by constant

use chrono::{DateTime, Utc};
use quant_primitives::Candle;
use rust_decimal::Decimal;

use crate::error::IndicatorError;
use crate::indicator::Indicator;
use crate::series::Series;

/// Alignment info for combining two series from the end (most recent values).
struct SeriesAlignment {
    output_len: usize,
    offset_a: usize,
    offset_b: usize,
}

/// Align two series by taking intersection from the end (more recent values).
///
/// Returns alignment info: output length and offsets into each series.
fn align_series_from_end(len_a: usize, len_b: usize) -> SeriesAlignment {
    let output_len = len_a.min(len_b);
    SeriesAlignment {
        output_len,
        offset_a: len_a - output_len,
        offset_b: len_b - output_len,
    }
}

/// Apply a binary operation to two aligned series.
fn apply_binary_op<F>(
    values_a: &[(DateTime<Utc>, Decimal)],
    values_b: &[(DateTime<Utc>, Decimal)],
    op: F,
) -> Vec<(DateTime<Utc>, Decimal)>
where
    F: Fn(Decimal, Decimal) -> Decimal,
{
    let align = align_series_from_end(values_a.len(), values_b.len());
    let mut result = Vec::with_capacity(align.output_len);

    for i in 0..align.output_len {
        let (ts, val_a) = values_a[align.offset_a + i];
        let val_b = values_b[align.offset_b + i].1;
        result.push((ts, op(val_a, val_b)));
    }

    result
}

/// Difference between two indicators: A - B
#[derive(Debug, Clone)]
pub struct Diff<A, B> {
    a: A,
    b: B,
    name: String,
}

impl<A: Indicator, B: Indicator> Diff<A, B> {
    /// Create a new Diff combinator.
    pub fn new(a: A, b: B) -> Self {
        let name = format!("Diff({},{})", a.name(), b.name());
        Self { a, b, name }
    }
}

impl<A: Indicator, B: Indicator> Indicator for Diff<A, B> {
    fn name(&self) -> &str {
        &self.name
    }

    fn warmup_period(&self) -> usize {
        self.a.warmup_period().max(self.b.warmup_period())
    }

    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        let series_a = self.a.compute(candles)?;
        let series_b = self.b.compute(candles)?;

        let values = apply_binary_op(series_a.values(), series_b.values(), |a, b| a - b);

        Ok(Series::new(values))
    }
}

/// Ratio between two indicators: A / B
#[derive(Debug, Clone)]
pub struct Ratio<A, B> {
    a: A,
    b: B,
    name: String,
}

impl<A: Indicator, B: Indicator> Ratio<A, B> {
    /// Create a new Ratio combinator.
    pub fn new(a: A, b: B) -> Self {
        let name = format!("Ratio({},{})", a.name(), b.name());
        Self { a, b, name }
    }
}

impl<A: Indicator, B: Indicator> Indicator for Ratio<A, B> {
    fn name(&self) -> &str {
        &self.name
    }

    fn warmup_period(&self) -> usize {
        self.a.warmup_period().max(self.b.warmup_period())
    }

    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        let series_a = self.a.compute(candles)?;
        let series_b = self.b.compute(candles)?;

        let values = apply_binary_op(series_a.values(), series_b.values(), |a, b| {
            // Avoid division by zero
            if b.is_zero() {
                Decimal::ZERO
            } else {
                a / b
            }
        });

        Ok(Series::new(values))
    }
}

/// Lag shifts indicator values by N periods.
///
/// A lag of 1 means each output value is the previous period's indicator value.
#[derive(Debug, Clone)]
pub struct Lag<I> {
    inner: I,
    periods: usize,
    name: String,
}

impl<I: Indicator> Lag<I> {
    /// Create a new Lag combinator.
    ///
    /// # Arguments
    ///
    /// * `inner` - The indicator to lag
    /// * `periods` - Number of periods to lag (must be > 0)
    pub fn new(inner: I, periods: usize) -> Result<Self, IndicatorError> {
        if periods == 0 {
            return Err(IndicatorError::InvalidParameter {
                message: "Lag periods must be > 0".to_string(),
            });
        }
        let name = format!("Lag({},{})", inner.name(), periods);
        Ok(Self {
            inner,
            periods,
            name,
        })
    }
}

impl<I: Indicator> Indicator for Lag<I> {
    fn name(&self) -> &str {
        &self.name
    }

    fn warmup_period(&self) -> usize {
        self.inner.warmup_period() + self.periods
    }

    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        let inner_series = self.inner.compute(candles)?;
        let values = inner_series.values();

        if values.len() <= self.periods {
            return Err(IndicatorError::InsufficientData {
                required: self.inner.warmup_period() + self.periods,
                actual: candles.len(),
            });
        }

        // Drop the last N values, shift timestamps
        let output_len = values.len() - self.periods;
        let mut lagged_values = Vec::with_capacity(output_len);

        for i in 0..output_len {
            // Value from i, but timestamp from i + periods
            let value = values[i].1;
            let ts = values[i + self.periods].0;
            lagged_values.push((ts, value));
        }

        Ok(Series::new(lagged_values))
    }
}

/// Scale multiplies indicator values by a constant factor.
#[derive(Debug, Clone)]
pub struct Scale<I> {
    inner: I,
    factor: Decimal,
    name: String,
}

impl<I: Indicator> Scale<I> {
    /// Create a new Scale combinator.
    pub fn new(inner: I, factor: Decimal) -> Self {
        let name = format!("Scale({},{})", inner.name(), factor);
        Self {
            inner,
            factor,
            name,
        }
    }
}

impl<I: Indicator> Indicator for Scale<I> {
    fn name(&self) -> &str {
        &self.name
    }

    fn warmup_period(&self) -> usize {
        self.inner.warmup_period()
    }

    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        let inner_series = self.inner.compute(candles)?;
        let values: Vec<_> = inner_series
            .values()
            .iter()
            .map(|(ts, v)| (*ts, *v * self.factor))
            .collect();
        Ok(Series::new(values))
    }
}

#[cfg(test)]
#[path = "combinators_tests.rs"]
mod tests;