qta 2.56.2

Streaming technical analysis indicators for quantitative trading
Documentation
//! ZigZag configuration and threshold computation.
//!
//! Range-bar-native threshold formula: τ = N × δ, where δ is the bar's deviation
//! threshold in price units and N is the reversal depth multiplier.

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

use crate::zigzag::ZigZagError;

/// Scale factor for decimal basis points (matches opendeviationbar-core convention).
const BASIS_POINTS_SCALE: i128 = 100_000;

/// ZigZag indicator configuration.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ZigZagConfig {
    /// N in τ = N × δ. Default: 3.0.
    pub reversal_depth: f64,
    /// E in ε = E × δ. Must be < reversal_depth. Default: 1.0.
    pub epsilon_multiplier: f64,
    /// Bar construction threshold in decimal basis points. Example: 250 = 0.25%.
    pub bar_threshold_dbps: u32,
    /// Threshold computation mode.
    pub threshold_mode: ThresholdMode,
}

/// How the reversal threshold τ is computed.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ThresholdMode {
    /// τ = N × δ — pure bar-count scaling. Default for range bars.
    BarCount,
}

impl ZigZagConfig {
    /// Create a new config with `BarCount` threshold mode.
    pub fn new(
        reversal_depth: f64,
        epsilon_multiplier: f64,
        bar_threshold_dbps: u32,
    ) -> Result<Self, ZigZagError> {
        Self::validate(reversal_depth, epsilon_multiplier, bar_threshold_dbps)?;
        Ok(Self {
            reversal_depth,
            epsilon_multiplier,
            bar_threshold_dbps,
            threshold_mode: ThresholdMode::BarCount,
        })
    }

    fn validate(
        reversal_depth: f64,
        epsilon_multiplier: f64,
        bar_threshold_dbps: u32,
    ) -> Result<(), ZigZagError> {
        if reversal_depth <= 0.0 {
            return Err(ZigZagError::InvalidConfig(
                "reversal_depth must be positive",
            ));
        }
        if epsilon_multiplier < 0.0 {
            return Err(ZigZagError::InvalidConfig(
                "epsilon_multiplier must be non-negative",
            ));
        }
        if epsilon_multiplier >= reversal_depth {
            return Err(ZigZagError::InvalidConfig(
                "epsilon_multiplier must be less than reversal_depth",
            ));
        }
        if bar_threshold_dbps == 0 {
            return Err(ZigZagError::InvalidConfig(
                "bar_threshold_dbps must be non-zero",
            ));
        }
        Ok(())
    }
}

/// Compute δ (bar threshold in price units) from a reference price.
#[inline]
pub fn compute_delta(reference_price: i64, bar_threshold_dbps: u32) -> i64 {
    let delta = (reference_price as i128 * bar_threshold_dbps as i128) / BASIS_POINTS_SCALE;
    delta as i64
}

/// Compute τ (reversal threshold): τ = N × δ.
#[inline]
pub fn compute_tau(config: &ZigZagConfig, reference_price: i64) -> i64 {
    let delta = compute_delta(reference_price, config.bar_threshold_dbps);
    (config.reversal_depth * delta as f64).round() as i64
}

/// Compute ε (tolerance band): ε = E × δ.
#[inline]
pub fn compute_epsilon(config: &ZigZagConfig, reference_price: i64) -> i64 {
    let delta = compute_delta(reference_price, config.bar_threshold_dbps);
    (config.epsilon_multiplier * delta as f64).round() as i64
}

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

    #[test]
    fn test_config_validation() {
        assert!(ZigZagConfig::new(3.0, 1.0, 250).is_ok());
        assert!(ZigZagConfig::new(3.0, 3.0, 250).is_err());
        assert!(ZigZagConfig::new(3.0, 4.0, 250).is_err());
        assert!(ZigZagConfig::new(0.0, 0.0, 250).is_err());
        assert!(ZigZagConfig::new(-1.0, 0.0, 250).is_err());
        assert!(ZigZagConfig::new(3.0, 1.0, 0).is_err());
    }

    #[test]
    fn test_compute_delta_btcusdt() {
        let price = 5_000_000_000_000_i64; // 50000.0
        let delta = compute_delta(price, 250);
        assert_eq!(delta, 12_500_000_000); // 125.0 in FixedPoint
    }

    #[test]
    fn test_compute_tau() {
        let config = ZigZagConfig::new(3.0, 1.0, 250).unwrap();
        let price = 5_000_000_000_000_i64;
        assert_eq!(compute_tau(&config, price), 37_500_000_000); // 375.0
    }

    #[test]
    fn test_compute_epsilon() {
        let config = ZigZagConfig::new(3.0, 1.0, 250).unwrap();
        let price = 5_000_000_000_000_i64;
        assert_eq!(compute_epsilon(&config, price), 12_500_000_000); // 125.0
    }
}