Skip to main content

qta/zigzag/
types.rs

1//! Core types for the ZigZag indicator.
2//!
3//! All prices use the FixedPoint i64×1e8 convention from opendeviationbar-core.
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// Input bar for the ZigZag indicator.
9///
10/// Uses raw i64 values following the FixedPoint convention (i64 × 1e8).
11/// Only high, low, close are needed — open is not used in the ZigZag algorithm.
12#[derive(Debug, Clone, Copy)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct BarInput {
15    pub index: usize,
16    pub timestamp_us: i64,
17    /// Highest price in the bar (FixedPoint i64×1e8)
18    pub high: i64,
19    /// Lowest price in the bar (FixedPoint i64×1e8)
20    pub low: i64,
21    /// Closing price (FixedPoint i64×1e8)
22    pub close: i64,
23    /// Bar duration in microseconds (None for the first/incomplete bar)
24    pub duration_us: Option<i64>,
25}
26
27/// Whether a pivot marks a swing high or swing low.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30pub enum PivotKind {
31    High,
32    Low,
33}
34
35impl PivotKind {
36    #[must_use]
37    pub fn opposite(self) -> Self {
38        match self {
39            Self::High => Self::Low,
40            Self::Low => Self::High,
41        }
42    }
43}
44
45impl std::fmt::Display for PivotKind {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::High => write!(f, "High"),
49            Self::Low => write!(f, "Low"),
50        }
51    }
52}
53
54/// Confirmation status of a pivot.
55///
56/// **Repainting contract**: Once a pivot transitions to `Confirmed`, its price
57/// and bar_index NEVER change. This invariant is verified by proptest.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60pub enum ConfirmationStatus {
61    Confirmed {
62        /// Bar index at which the reversal was detected (not the pivot's own bar).
63        confirmed_at_bar: usize,
64        /// Monotonically increasing generation counter.
65        generation: u64,
66    },
67    Pending,
68}
69
70impl ConfirmationStatus {
71    #[must_use]
72    pub fn is_confirmed(&self) -> bool {
73        matches!(self, Self::Confirmed { .. })
74    }
75
76    #[must_use]
77    pub fn is_pending(&self) -> bool {
78        matches!(self, Self::Pending)
79    }
80}
81
82/// A swing pivot (high or low) identified by the ZigZag algorithm.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
85pub struct Pivot {
86    pub bar_index: usize,
87    pub timestamp_us: i64,
88    /// Pivot price (FixedPoint i64×1e8).
89    pub price: i64,
90    pub kind: PivotKind,
91    pub status: ConfirmationStatus,
92}
93
94/// Output from processing a single bar through the ZigZag state machine.
95#[derive(Debug, Clone)]
96pub struct ZigZagOutput {
97    /// Current pending pivot (always `Some` after initialization).
98    pub pending: Option<Pivot>,
99    /// Newly confirmed pivot from this bar (if a reversal was detected).
100    pub newly_confirmed: Option<Pivot>,
101    /// Whether the pending pivot was updated (leg extension) on this bar.
102    pub pending_updated: bool,
103    /// Completed L₀-H₁-L₂ segment (if the newly confirmed Low forms a triplet).
104    pub completed_segment: Option<Segment>,
105}
106
107impl ZigZagOutput {
108    #[must_use]
109    pub fn has_event(&self) -> bool {
110        self.newly_confirmed.is_some() || self.pending_updated || self.completed_segment.is_some()
111    }
112}
113
114/// A completed L₀→H₁→L₂ swing segment with classification.
115///
116/// Formed whenever a Low pivot is confirmed and a preceding Low-High pair exists.
117#[derive(Debug, Clone)]
118#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
119pub struct Segment {
120    /// Starting low pivot (L₀).
121    pub l0: Pivot,
122    /// Swing high pivot (H₁).
123    pub h1: Pivot,
124    /// Ending low pivot (L₂) — the retrace.
125    pub l2: Pivot,
126    /// Swing magnitude: H₁ - L₀ (always positive).
127    pub segment_size: i64,
128    /// Normalized retracement: (L₂ - L₀) / (H₁ - L₀).
129    /// z ∈ (0, 1) for HL, z ≈ 0 for EL, z < 0 for LL.
130    pub z: f64,
131    /// Base class derived from z and ε.
132    pub base_class: BaseClass,
133}
134
135/// Three-way swing classification for L₀-H₁-L₂ segments.
136///
137/// - **EL** (Equal Low): |L₂ - L₀| ≤ ε
138/// - **HL** (Higher Low): L₂ > L₀ + ε
139/// - **LL** (Lower Low): L₂ < L₀ - ε
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
142pub enum BaseClass {
143    EL,
144    HL,
145    LL,
146}
147
148impl std::fmt::Display for BaseClass {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self {
151            Self::EL => write!(f, "EL"),
152            Self::HL => write!(f, "HL"),
153            Self::LL => write!(f, "LL"),
154        }
155    }
156}