mylittleindicators 0.1.8

Multi-stream financial indicators library — 556 bar indicators + 21 event primitives across 35 categories. Consumes 27 stream kinds from digdigdig3 exchange connectors: OHLCV bars, ticks, orderbook (snapshot/delta/L3), funding/predicted funding/funding settlement, mark price, index price, open interest, liquidations, ticker, agg trades, long/short ratio, option greeks, volatility index, historical volatility, basis (derived), composite index, settlement events, block trades, insurance fund, risk limit, market warning, and three kline-family variants. Live-verified on 12 exchanges (89% pass-rate on a 150s BTC slice).
Documentation
// DPO Bands on oscillator scale: upper/lower = +/- k * std(DPO)

use crate::bar_indicators::average::moving_average::MovingAverageType;
use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::momentum::dpo::DetrendedPriceOscillator;
#[derive(Clone)]
pub struct DpoBands {
    dpo: DetrendedPriceOscillator,
    window: usize,
    k: f64,
    buf: Vec<f64>,
    idx: usize,
    filled: bool,
    upper: f64,
    middle: f64,
    lower: f64,
}

impl DpoBands {
    pub fn new(period: usize, window: usize, k: f64) -> Self {
        Self {
            dpo: DetrendedPriceOscillator::with_period(period.max(2)),
            window: window.clamp(5, 512),
            k: if k > 0.0 { k } else { 2.0 },
            buf: Vec::with_capacity(window.clamp(5, 512)),
            idx: 0,
            filled: false,
            upper: 0.0,
            middle: 0.0,
            lower: 0.0,
        }
    }

    /// Create DpoBands with a specific MA type for the internal DPO.
    ///
    /// Default (`new`) uses `SMA`. Use this to switch to EMA, RMA, etc.
    pub fn with_ma_type(period: usize, window: usize, k: f64, ma_type: MovingAverageType) -> Self {
        let win = window.clamp(5, 512);
        Self {
            dpo: DetrendedPriceOscillator::with_period_and_ma_type(period.max(2), ma_type),
            window: win,
            k: if k > 0.0 { k } else { 2.0 },
            buf: Vec::with_capacity(win),
            idx: 0,
            filled: false,
            upper: 0.0,
            middle: 0.0,
            lower: 0.0,
        }
    }
    #[inline]
    pub fn reset(&mut self) {
        self.dpo.reset();
        self.buf.clear();
        self.idx = 0;
        self.filled = false;
        self.upper = 0.0;
        self.middle = 0.0;
        self.lower = 0.0;
    }
    #[inline]
    pub fn is_ready(&self) -> bool {
        self.filled && self.dpo.is_ready()
    }
    #[inline]
    pub fn value(&self) -> IndicatorValue {
        IndicatorValue::Channel3 {
            upper: self.upper,
            middle: self.middle,
            lower: self.lower,
        }
    }

    #[inline]
    pub fn value_tuple(&self) -> (f64, f64, f64) {
        (self.upper, self.middle, self.lower)
    }
    pub fn update_bar(&mut self, o: f64, h: f64, l: f64, c: f64, v: f64) -> (f64, f64, f64) {
        let d = self.dpo.update_bar(o, h, l, c, v);
        if self.buf.len() < self.window {
            self.buf.push(d);
            if self.buf.len() == self.window {
                self.filled = true;
            }
        } else {
            self.buf[self.idx] = d;
        }
        self.idx = (self.idx + 1) % self.window;
        self.middle = 0.0;
        if self.is_ready() {
            let mean = self.buf.iter().sum::<f64>() / (self.window as f64);
            let var = self
                .buf
                .iter()
                .map(|&x| {
                    let dd = x - mean;
                    dd * dd
                })
                .sum::<f64>()
                / (self.window as f64);
            let sd = var.sqrt();
            self.upper = self.k * sd;
            self.lower = -self.k * sd;
        }
        (self.upper, self.middle, self.lower)
    }

    pub fn window(&self) -> usize {
        self.window
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_with_ma_type_non_default() {
        let mut db = DpoBands::with_ma_type(14, 20, 2.0, MovingAverageType::EMA);
        assert!(!db.is_ready());
        for i in 0..50 {
            let p = 100.0 + (i as f64 * 0.1).sin() * 5.0;
            let (u, m, l) = db.update_bar(p, p + 1.0, p - 1.0, p, 1000.0);
            assert!(u.is_finite());
            assert!(m.is_finite());
            assert!(l.is_finite());
        }
        assert!(db.is_ready());
    }

    #[test]
    fn test_dpo_bands_creation() {
        let db = DpoBands::new(14, 20, 2.0);
        assert!(!db.is_ready());
        assert_eq!(db.window(), 20);
    }

    #[test]
    fn test_dpo_bands_warmup() {
        let mut db = DpoBands::new(14, 20, 2.0);
        for i in 0..50 {
            let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
            db.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
        }
        assert!(db.is_ready());
    }

    #[test]
    fn test_dpo_bands_symmetric() {
        let mut db = DpoBands::new(14, 20, 2.0);
        for i in 0..50 {
            let price = 100.0 + (i as f64 * 0.2).sin() * 10.0;
            let (upper, middle, lower) = db.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
            if db.is_ready() {
                assert_eq!(middle, 0.0, "Middle should be 0");
                assert!((upper + lower).abs() < 1e-9, "Bands should be symmetric around 0");
            }
        }
    }

    #[test]
    fn test_dpo_bands_reset() {
        let mut db = DpoBands::new(14, 20, 2.0);
        for i in 0..50 {
            db.update_bar(100.0 + i as f64, 101.0, 99.0, 100.0 + i as f64, 1000.0);
        }
        db.reset();
        assert!(!db.is_ready());
    }
}