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    /// Completed L₀→H₁→L₂→H₃ formation (if 4 confirmed pivots form a pattern).
106    pub completed_formation: Option<Formation>,
107}
108
109impl ZigZagOutput {
110    #[must_use]
111    pub fn has_event(&self) -> bool {
112        self.newly_confirmed.is_some()
113            || self.pending_updated
114            || self.completed_segment.is_some()
115            || self.completed_formation.is_some()
116    }
117}
118
119/// A completed L₀→H₁→L₂ swing segment with classification.
120///
121/// Formed whenever a Low pivot is confirmed and a preceding Low-High pair exists.
122#[derive(Debug, Clone)]
123#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124pub struct Segment {
125    /// Starting low pivot (L₀).
126    pub l0: Pivot,
127    /// Swing high pivot (H₁).
128    pub h1: Pivot,
129    /// Ending low pivot (L₂) — the retrace.
130    pub l2: Pivot,
131    /// Swing magnitude: H₁ - L₀ (always positive).
132    pub segment_size: i64,
133    /// Normalized retracement: (L₂ - L₀) / (H₁ - L₀).
134    /// z ∈ (0, 1) for HL, z ≈ 0 for EL, z < 0 for LL.
135    pub z: f64,
136    /// Base class derived from z and ε.
137    pub base_class: BaseClass,
138}
139
140/// Three-way swing classification for L₀-H₁-L₂ segments.
141///
142/// - **EL** (Equal Low): |L₂ - L₀| ≤ ε
143/// - **HL** (Higher Low): L₂ > L₀ + ε
144/// - **LL** (Lower Low): L₂ < L₀ - ε
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
146#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
147pub enum BaseClass {
148    EL,
149    HL,
150    LL,
151}
152
153impl std::fmt::Display for BaseClass {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            Self::EL => write!(f, "EL"),
157            Self::HL => write!(f, "HL"),
158            Self::LL => write!(f, "LL"),
159        }
160    }
161}
162
163/// Three-way swing classification for H₃ vs H₁ in a 4-pivot formation.
164///
165/// Mirrors [`BaseClass`] (which classifies lows) for the high dimension.
166/// - **HH** (Higher High): H₃ > H₁ + ε
167/// - **EH** (Equal High): |H₃ - H₁| ≤ ε
168/// - **LH** (Lower High): H₃ < H₁ - ε
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
170#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
171pub enum HighClass {
172    HH,
173    EH,
174    LH,
175}
176
177impl std::fmt::Display for HighClass {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        match self {
180            Self::HH => write!(f, "HH"),
181            Self::EH => write!(f, "EH"),
182            Self::LH => write!(f, "LH"),
183        }
184    }
185}
186
187/// A completed L₀→H₁→L₂→H₃ four-pivot formation with full classification.
188///
189/// Composed from two consecutive segments sharing the L₂/H₁ boundary.
190/// Emitted when a Down-to-Up reversal confirms a new Low after H₃,
191/// meaning H₃ is the most recently confirmed High before the new Low.
192#[derive(Debug, Clone)]
193#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
194pub struct Formation {
195    /// Starting low pivot (L₀).
196    pub l0: Pivot,
197    /// First swing high (H₁).
198    pub h1: Pivot,
199    /// Retrace low (L₂) — shared boundary.
200    pub l2: Pivot,
201    /// Second swing high (H₃).
202    pub h3: Pivot,
203    /// Base class for L₂ relative to L₀.
204    pub base_class: BaseClass,
205    /// Base class for H₃ relative to H₁.
206    pub high_class: HighClass,
207    /// Normalized retracement of the low side: (L₂ - L₀) / (H₁ - L₀).
208    pub z_low: f64,
209    /// Normalized comparison of highs: (H₃ - H₁) / (H₁ - L₂).
210    pub z_high: f64,
211    /// First leg magnitude: H₁ - L₀ (always positive).
212    pub first_leg_size: i64,
213    /// Second leg magnitude: H₃ - L₂ (always positive).
214    pub second_leg_size: i64,
215}