qta 2.9.0

Streaming technical analysis indicators for quantitative trading
Documentation
//! Core types for the ZigZag indicator.
//!
//! All prices use the FixedPoint i64×1e8 convention from opendeviationbar-core.

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

/// Input bar for the ZigZag indicator.
///
/// Uses raw i64 values following the FixedPoint convention (i64 × 1e8).
/// Only high, low, close are needed — open is not used in the ZigZag algorithm.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BarInput {
    pub index: usize,
    pub timestamp_us: i64,
    /// Highest price in the bar (FixedPoint i64×1e8)
    pub high: i64,
    /// Lowest price in the bar (FixedPoint i64×1e8)
    pub low: i64,
    /// Closing price (FixedPoint i64×1e8)
    pub close: i64,
    /// Bar duration in microseconds (None for the first/incomplete bar)
    pub duration_us: Option<i64>,
}

/// Whether a pivot marks a swing high or swing low.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PivotKind {
    High,
    Low,
}

impl PivotKind {
    #[must_use]
    pub fn opposite(self) -> Self {
        match self {
            Self::High => Self::Low,
            Self::Low => Self::High,
        }
    }
}

impl std::fmt::Display for PivotKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::High => write!(f, "High"),
            Self::Low => write!(f, "Low"),
        }
    }
}

/// Confirmation status of a pivot.
///
/// **Repainting contract**: Once a pivot transitions to `Confirmed`, its price
/// and bar_index NEVER change. This invariant is verified by proptest.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ConfirmationStatus {
    Confirmed {
        /// Bar index at which the reversal was detected (not the pivot's own bar).
        confirmed_at_bar: usize,
        /// Monotonically increasing generation counter.
        generation: u64,
    },
    Pending,
}

impl ConfirmationStatus {
    #[must_use]
    pub fn is_confirmed(&self) -> bool {
        matches!(self, Self::Confirmed { .. })
    }

    #[must_use]
    pub fn is_pending(&self) -> bool {
        matches!(self, Self::Pending)
    }
}

/// A swing pivot (high or low) identified by the ZigZag algorithm.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Pivot {
    pub bar_index: usize,
    pub timestamp_us: i64,
    /// Pivot price (FixedPoint i64×1e8).
    pub price: i64,
    pub kind: PivotKind,
    pub status: ConfirmationStatus,
}

/// Output from processing a single bar through the ZigZag state machine.
#[derive(Debug, Clone)]
pub struct ZigZagOutput {
    /// Current pending pivot (always `Some` after initialization).
    pub pending: Option<Pivot>,
    /// Newly confirmed pivot from this bar (if a reversal was detected).
    pub newly_confirmed: Option<Pivot>,
    /// Whether the pending pivot was updated (leg extension) on this bar.
    pub pending_updated: bool,
    /// Completed L₀-H₁-L₂ segment (if the newly confirmed Low forms a triplet).
    pub completed_segment: Option<Segment>,
    /// Completed L₀→H₁→L₂→H₃ formation (if 4 confirmed pivots form a pattern).
    pub completed_formation: Option<Formation>,
}

impl ZigZagOutput {
    #[must_use]
    pub fn has_event(&self) -> bool {
        self.newly_confirmed.is_some()
            || self.pending_updated
            || self.completed_segment.is_some()
            || self.completed_formation.is_some()
    }
}

/// A completed L₀→H₁→L₂ swing segment with classification.
///
/// Formed whenever a Low pivot is confirmed and a preceding Low-High pair exists.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Segment {
    /// Starting low pivot (L₀).
    pub l0: Pivot,
    /// Swing high pivot (H₁).
    pub h1: Pivot,
    /// Ending low pivot (L₂) — the retrace.
    pub l2: Pivot,
    /// Swing magnitude: H₁ - L₀ (always positive).
    pub segment_size: i64,
    /// Normalized retracement: (L₂ - L₀) / (H₁ - L₀).
    /// z ∈ (0, 1) for HL, z ≈ 0 for EL, z < 0 for LL.
    pub z: f64,
    /// Base class derived from z and ε.
    pub base_class: BaseClass,
}

/// Three-way swing classification for L₀-H₁-L₂ segments.
///
/// - **EL** (Equal Low): |L₂ - L₀| ≤ ε
/// - **HL** (Higher Low): L₂ > L₀ + ε
/// - **LL** (Lower Low): L₂ < L₀ - ε
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum BaseClass {
    EL,
    HL,
    LL,
}

impl std::fmt::Display for BaseClass {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::EL => write!(f, "EL"),
            Self::HL => write!(f, "HL"),
            Self::LL => write!(f, "LL"),
        }
    }
}

/// Three-way swing classification for H₃ vs H₁ in a 4-pivot formation.
///
/// Mirrors [`BaseClass`] (which classifies lows) for the high dimension.
/// - **HH** (Higher High): H₃ > H₁ + ε
/// - **EH** (Equal High): |H₃ - H₁| ≤ ε
/// - **LH** (Lower High): H₃ < H₁ - ε
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum HighClass {
    HH,
    EH,
    LH,
}

impl std::fmt::Display for HighClass {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::HH => write!(f, "HH"),
            Self::EH => write!(f, "EH"),
            Self::LH => write!(f, "LH"),
        }
    }
}

/// A completed L₀→H₁→L₂→H₃ four-pivot formation with full classification.
///
/// Composed from two consecutive segments sharing the L₂/H₁ boundary.
/// Emitted when a Down-to-Up reversal confirms a new Low after H₃,
/// meaning H₃ is the most recently confirmed High before the new Low.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Formation {
    /// Starting low pivot (L₀).
    pub l0: Pivot,
    /// First swing high (H₁).
    pub h1: Pivot,
    /// Retrace low (L₂) — shared boundary.
    pub l2: Pivot,
    /// Second swing high (H₃).
    pub h3: Pivot,
    /// Base class for L₂ relative to L₀.
    pub base_class: BaseClass,
    /// Base class for H₃ relative to H₁.
    pub high_class: HighClass,
    /// Normalized retracement of the low side: (L₂ - L₀) / (H₁ - L₀).
    pub z_low: f64,
    /// Normalized comparison of highs: (H₃ - H₁) / (H₁ - L₂).
    pub z_high: f64,
    /// First leg magnitude: H₁ - L₀ (always positive).
    pub first_leg_size: i64,
    /// Second leg magnitude: H₃ - L₂ (always positive).
    pub second_leg_size: i64,
}