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}