qta 2.9.0

Streaming technical analysis indicators for quantitative trading
Documentation
//! Base class classification for L₀-H₁-L₂ segments and H₁-H₃ high classification.

use crate::zigzag::types::{BaseClass, HighClass};

/// Classify the relationship between L₂ and L₀ using tolerance ε.
#[inline]
pub fn classify_base_class(l0_price: i64, l2_price: i64, epsilon: i64) -> BaseClass {
    let diff = l2_price - l0_price;
    if diff.abs() <= epsilon {
        BaseClass::EL
    } else if diff > 0 {
        BaseClass::HL
    } else {
        BaseClass::LL
    }
}

/// Compute normalized retracement z = (L₂ - L₀) / (H₁ - L₀).
#[inline]
pub fn compute_z(l0_price: i64, h1_price: i64, l2_price: i64) -> Option<f64> {
    let segment_size = h1_price - l0_price;
    if segment_size == 0 {
        return None;
    }
    Some((l2_price - l0_price) as f64 / segment_size as f64)
}

/// Classify the relationship between H₃ and H₁ using tolerance ε.
///
/// Mirrors [`classify_base_class`] for the high dimension of a 4-pivot formation.
/// - **HH** (Higher High): H₃ > H₁ + ε
/// - **EH** (Equal High): |H₃ - H₁| ≤ ε
/// - **LH** (Lower High): H₃ < H₁ - ε
#[inline]
pub fn classify_high_class(h1_price: i64, h3_price: i64, epsilon: i64) -> HighClass {
    let diff = h3_price - h1_price;
    if diff.abs() <= epsilon {
        HighClass::EH
    } else if diff > 0 {
        HighClass::HH
    } else {
        HighClass::LH
    }
}

/// Compute normalized high comparison z = (H₃ - H₁) / (H₁ - L₂).
///
/// Returns `None` if H₁ == L₂ (degenerate second leg).
#[inline]
pub fn compute_z_high(h1_price: i64, l2_price: i64, h3_price: i64) -> Option<f64> {
    let second_leg = h1_price - l2_price;
    if second_leg == 0 {
        return None;
    }
    Some((h3_price - h1_price) as f64 / second_leg as f64)
}

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

    const SCALE: i64 = 100_000_000;

    fn fp(value: f64) -> i64 {
        (value * SCALE as f64).round() as i64
    }

    #[test]
    fn test_higher_low() {
        assert_eq!(
            classify_base_class(fp(100.0), fp(102.0), fp(0.5)),
            BaseClass::HL
        );
    }

    #[test]
    fn test_equal_low() {
        assert_eq!(
            classify_base_class(fp(100.0), fp(100.3), fp(0.5)),
            BaseClass::EL
        );
    }

    #[test]
    fn test_equal_low_negative_diff() {
        assert_eq!(
            classify_base_class(fp(100.0), fp(99.8), fp(0.5)),
            BaseClass::EL
        );
    }

    #[test]
    fn test_lower_low() {
        assert_eq!(
            classify_base_class(fp(100.0), fp(98.0), fp(0.5)),
            BaseClass::LL
        );
    }

    #[test]
    fn test_boundary_equal() {
        let l0 = fp(100.0);
        let epsilon = fp(1.0);
        assert_eq!(
            classify_base_class(l0, l0 + epsilon, epsilon),
            BaseClass::EL
        );
        assert_eq!(
            classify_base_class(l0, l0 - epsilon, epsilon),
            BaseClass::EL
        );
    }

    #[test]
    fn test_compute_z_higher_low() {
        let z = compute_z(fp(100.0), fp(110.0), fp(105.0)).unwrap();
        assert!((z - 0.5).abs() < 1e-10);
    }

    #[test]
    fn test_compute_z_equal_low() {
        let z = compute_z(fp(100.0), fp(110.0), fp(100.0)).unwrap();
        assert!(z.abs() < 1e-10);
    }

    #[test]
    fn test_compute_z_lower_low() {
        let z = compute_z(fp(100.0), fp(110.0), fp(95.0)).unwrap();
        assert!((z - (-0.5)).abs() < 1e-10);
    }

    #[test]
    fn test_compute_z_degenerate() {
        assert!(compute_z(fp(100.0), fp(100.0), fp(100.0)).is_none());
    }

    // ── HighClass tests ──────────────────────────────────────────

    #[test]
    fn test_higher_high() {
        assert_eq!(
            classify_high_class(fp(100.0), fp(102.0), fp(0.5)),
            HighClass::HH
        );
    }

    #[test]
    fn test_equal_high() {
        assert_eq!(
            classify_high_class(fp(100.0), fp(100.3), fp(0.5)),
            HighClass::EH
        );
    }

    #[test]
    fn test_equal_high_negative_diff() {
        assert_eq!(
            classify_high_class(fp(100.0), fp(99.8), fp(0.5)),
            HighClass::EH
        );
    }

    #[test]
    fn test_lower_high() {
        assert_eq!(
            classify_high_class(fp(100.0), fp(98.0), fp(0.5)),
            HighClass::LH
        );
    }

    #[test]
    fn test_boundary_equal_high() {
        let h1 = fp(100.0);
        let epsilon = fp(1.0);
        assert_eq!(
            classify_high_class(h1, h1 + epsilon, epsilon),
            HighClass::EH
        );
        assert_eq!(
            classify_high_class(h1, h1 - epsilon, epsilon),
            HighClass::EH
        );
        // One tick beyond boundary
        assert_eq!(
            classify_high_class(h1, h1 + epsilon + 1, epsilon),
            HighClass::HH
        );
        assert_eq!(
            classify_high_class(h1, h1 - epsilon - 1, epsilon),
            HighClass::LH
        );
    }

    #[test]
    fn test_compute_z_high_higher() {
        // H1=100, L2=90, H3=110 => z_high = (110-100)/(100-90) = 1.0
        let z = compute_z_high(fp(100.0), fp(90.0), fp(110.0)).unwrap();
        assert!((z - 1.0).abs() < 1e-10);
    }

    #[test]
    fn test_compute_z_high_lower() {
        // H1=100, L2=90, H3=95 => z_high = (95-100)/(100-90) = -0.5
        let z = compute_z_high(fp(100.0), fp(90.0), fp(95.0)).unwrap();
        assert!((z - (-0.5)).abs() < 1e-10);
    }

    #[test]
    fn test_compute_z_high_degenerate() {
        assert!(compute_z_high(fp(100.0), fp(100.0), fp(105.0)).is_none());
    }
}