use crate::zigzag::base_class::{classify_base_class, classify_high_class, compute_z, compute_z_high};
use crate::zigzag::config::{ZigZagConfig, compute_epsilon, compute_tau};
use crate::zigzag::direction::{Direction, InitState, Phase};
use crate::zigzag::types::{BarInput, ConfirmationStatus, Formation, Pivot, PivotKind, Segment, ZigZagOutput};
pub struct ZigZagState {
config: ZigZagConfig,
phase: Phase,
pending_price: i64,
pending_index: usize,
pending_timestamp_us: i64,
reversal_threshold: i64,
confirmed_pivots: Vec<Pivot>,
confirmation_generation: u64,
bars_processed: u64,
}
impl ZigZagState {
#[must_use]
pub fn new(config: ZigZagConfig) -> Self {
Self {
config,
phase: Phase::Uninitialized(InitState {
high: i64::MIN,
high_index: 0,
high_timestamp: 0,
low: i64::MAX,
low_index: 0,
low_timestamp: 0,
}),
pending_price: 0,
pending_index: 0,
pending_timestamp_us: 0,
reversal_threshold: 0,
confirmed_pivots: Vec::new(),
confirmation_generation: 0,
bars_processed: 0,
}
}
pub fn process_bar(&mut self, bar: &BarInput) -> ZigZagOutput {
self.bars_processed += 1;
match &self.phase {
Phase::Uninitialized(_) => self.process_init(bar),
Phase::Active(Direction::Up) => self.process_up(bar),
Phase::Active(Direction::Down) => self.process_down(bar),
}
}
#[must_use]
pub fn confirmed_pivots(&self) -> &[Pivot] {
&self.confirmed_pivots
}
#[must_use]
pub fn pending(&self) -> Option<Pivot> {
match &self.phase {
Phase::Uninitialized(_) => None,
Phase::Active(dir) => Some(Pivot {
bar_index: self.pending_index,
timestamp_us: self.pending_timestamp_us,
price: self.pending_price,
kind: match dir {
Direction::Up => PivotKind::High,
Direction::Down => PivotKind::Low,
},
status: ConfirmationStatus::Pending,
}),
}
}
#[must_use]
pub fn is_initialized(&self) -> bool {
matches!(self.phase, Phase::Active(_))
}
#[must_use]
pub fn bars_processed(&self) -> u64 {
self.bars_processed
}
#[must_use]
pub fn config(&self) -> &ZigZagConfig {
&self.config
}
fn process_init(&mut self, bar: &BarInput) -> ZigZagOutput {
let init = match &mut self.phase {
Phase::Uninitialized(s) => s,
Phase::Active(_) => unreachable!(),
};
if init.high == i64::MIN {
*init = InitState::from_bar(bar);
return ZigZagOutput {
pending: None,
newly_confirmed: None,
pending_updated: false,
completed_segment: None,
completed_formation: None,
};
}
init.update(bar);
let ih = init.high;
let ih_idx = init.high_index;
let ih_ts = init.high_timestamp;
let il = init.low;
let il_idx = init.low_index;
let il_ts = init.low_timestamp;
let tau_high = compute_tau(&self.config, ih);
if ih - bar.low >= tau_high {
let pivot = self.confirm_pivot(ih_idx, ih_ts, ih, PivotKind::High, bar.index);
self.phase = Phase::Active(Direction::Down);
self.pending_price = bar.low;
self.pending_index = bar.index;
self.pending_timestamp_us = bar.timestamp_us;
self.reversal_threshold = compute_tau(&self.config, bar.low);
tracing::debug!(
kind = "High",
price = ih,
bar = ih_idx,
"first pivot (init → Down)"
);
return ZigZagOutput {
pending: self.pending(),
newly_confirmed: Some(pivot),
pending_updated: false,
completed_segment: None,
completed_formation: None,
};
}
let tau_low = compute_tau(&self.config, il);
if bar.high - il >= tau_low {
let pivot = self.confirm_pivot(il_idx, il_ts, il, PivotKind::Low, bar.index);
self.phase = Phase::Active(Direction::Up);
self.pending_price = bar.high;
self.pending_index = bar.index;
self.pending_timestamp_us = bar.timestamp_us;
self.reversal_threshold = compute_tau(&self.config, bar.high);
tracing::debug!(
kind = "Low",
price = il,
bar = il_idx,
"first pivot (init → Up)"
);
return ZigZagOutput {
pending: self.pending(),
newly_confirmed: Some(pivot),
pending_updated: false,
completed_segment: None,
completed_formation: None,
};
}
ZigZagOutput {
pending: None,
newly_confirmed: None,
pending_updated: false,
completed_segment: None,
completed_formation: None,
}
}
fn process_up(&mut self, bar: &BarInput) -> ZigZagOutput {
self.process_active(
bar,
bar.high,
bar.low,
PivotKind::High,
Direction::Down,
false,
)
}
fn process_down(&mut self, bar: &BarInput) -> ZigZagOutput {
self.process_active(bar, bar.low, bar.high, PivotKind::Low, Direction::Up, true)
}
fn process_active(
&mut self,
bar: &BarInput,
extend_price: i64,
reversal_price: i64,
confirm_kind: PivotKind,
new_direction: Direction,
form_segment: bool,
) -> ZigZagOutput {
let mut pending_updated = false;
let extends = match confirm_kind {
PivotKind::High => extend_price > self.pending_price,
PivotKind::Low => extend_price < self.pending_price,
};
if extends {
self.pending_price = extend_price;
self.pending_index = bar.index;
self.pending_timestamp_us = bar.timestamp_us;
self.reversal_threshold = compute_tau(&self.config, self.pending_price);
pending_updated = true;
}
if (self.pending_price - reversal_price).abs() >= self.reversal_threshold {
let pivot = self.confirm_pivot(
self.pending_index,
self.pending_timestamp_us,
self.pending_price,
confirm_kind,
bar.index,
);
self.phase = Phase::Active(new_direction);
self.pending_price = reversal_price;
self.pending_index = bar.index;
self.pending_timestamp_us = bar.timestamp_us;
self.reversal_threshold = compute_tau(&self.config, reversal_price);
let (segment, formation) = if form_segment {
(self.try_form_segment(), self.try_form_formation())
} else {
(None, None)
};
tracing::debug!(
kind = ?confirm_kind,
price = pivot.price,
gen = self.confirmation_generation,
seg = segment.is_some(),
fmt = formation.is_some(),
"pivot confirmed"
);
return ZigZagOutput {
pending: self.pending(),
newly_confirmed: Some(pivot),
pending_updated: false,
completed_segment: segment,
completed_formation: formation,
};
}
ZigZagOutput {
pending: self.pending(),
newly_confirmed: None,
pending_updated,
completed_segment: None,
completed_formation: None,
}
}
fn confirm_pivot(
&mut self,
bar_index: usize,
timestamp_us: i64,
price: i64,
kind: PivotKind,
confirmed_at_bar: usize,
) -> Pivot {
self.confirmation_generation += 1;
let pivot = Pivot {
bar_index,
timestamp_us,
price,
kind,
status: ConfirmationStatus::Confirmed {
confirmed_at_bar,
generation: self.confirmation_generation,
},
};
self.confirmed_pivots.push(pivot);
pivot
}
fn try_form_segment(&self) -> Option<Segment> {
let len = self.confirmed_pivots.len();
if len < 3 {
return None;
}
let l2 = &self.confirmed_pivots[len - 1];
let h1 = &self.confirmed_pivots[len - 2];
let l0 = &self.confirmed_pivots[len - 3];
if l0.kind != PivotKind::Low || h1.kind != PivotKind::High || l2.kind != PivotKind::Low {
return None;
}
let segment_size = h1.price - l0.price;
if segment_size <= 0 {
return None;
}
let z = compute_z(l0.price, h1.price, l2.price)?;
let epsilon = compute_epsilon(&self.config, l0.price);
let base_class = classify_base_class(l0.price, l2.price, epsilon);
Some(Segment {
l0: *l0,
h1: *h1,
l2: *l2,
segment_size,
z,
base_class,
})
}
fn try_form_formation(&self) -> Option<Formation> {
let len = self.confirmed_pivots.len();
if len < 5 {
return None;
}
let l0 = &self.confirmed_pivots[len - 5];
let h1 = &self.confirmed_pivots[len - 4];
let l2 = &self.confirmed_pivots[len - 3];
let h3 = &self.confirmed_pivots[len - 2];
if l0.kind != PivotKind::Low
|| h1.kind != PivotKind::High
|| l2.kind != PivotKind::Low
|| h3.kind != PivotKind::High
{
return None;
}
let first_leg_size = h1.price - l0.price;
let second_leg_size = h3.price - l2.price;
if first_leg_size <= 0 || second_leg_size <= 0 {
return None;
}
let z_low = compute_z(l0.price, h1.price, l2.price)?;
let z_high = compute_z_high(h1.price, l2.price, h3.price)?;
let epsilon_low = compute_epsilon(&self.config, l0.price);
let epsilon_high = compute_epsilon(&self.config, h1.price);
let base_class = classify_base_class(l0.price, l2.price, epsilon_low);
let high_class = classify_high_class(h1.price, h3.price, epsilon_high);
Some(Formation {
l0: *l0,
h1: *h1,
l2: *l2,
h3: *h3,
base_class,
high_class,
z_low,
z_high,
first_leg_size,
second_leg_size,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::zigzag::types::BaseClass;
const SCALE: i64 = 100_000_000;
fn fp(value: f64) -> i64 {
(value * SCALE as f64).round() as i64
}
fn make_bar(index: usize, high: f64, low: f64, close: f64) -> BarInput {
BarInput {
index,
timestamp_us: (index as i64) * 1_000_000,
high: fp(high),
low: fp(low),
close: fp(close),
duration_us: Some(60_000_000),
}
}
fn test_config() -> ZigZagConfig {
ZigZagConfig::new(3.0, 1.0, 250).unwrap()
}
#[test]
fn test_initialization_starts_uninitialized() {
let state = ZigZagState::new(test_config());
assert!(!state.is_initialized());
assert!(state.pending().is_none());
assert!(state.confirmed_pivots().is_empty());
}
#[test]
fn test_first_bar_seeds_init() {
let mut state = ZigZagState::new(test_config());
let output = state.process_bar(&make_bar(0, 50125.0, 50000.0, 50060.0));
assert!(!state.is_initialized());
assert!(!output.has_event());
}
#[test]
fn test_upward_reversal_initializes() {
let mut state = ZigZagState::new(test_config());
state.process_bar(&make_bar(0, 50100.0, 50000.0, 50050.0));
let output = state.process_bar(&make_bar(1, 50400.0, 50200.0, 50300.0));
assert!(state.is_initialized());
let pivot = output.newly_confirmed.unwrap();
assert_eq!(pivot.kind, PivotKind::Low);
assert_eq!(pivot.price, fp(50000.0));
}
#[test]
fn test_downward_reversal_initializes() {
let mut state = ZigZagState::new(test_config());
state.process_bar(&make_bar(0, 50500.0, 50400.0, 50450.0));
let output = state.process_bar(&make_bar(1, 50200.0, 50100.0, 50150.0));
assert!(state.is_initialized());
let pivot = output.newly_confirmed.unwrap();
assert_eq!(pivot.kind, PivotKind::High);
assert_eq!(pivot.price, fp(50500.0));
}
#[test]
fn test_alternating_pivots() {
let mut state = ZigZagState::new(test_config());
let mut kinds: Vec<PivotKind> = Vec::new();
let bars = vec![
make_bar(0, 50100.0, 50000.0, 50050.0),
make_bar(1, 50400.0, 50200.0, 50300.0),
make_bar(2, 50600.0, 50400.0, 50500.0),
make_bar(3, 50700.0, 50500.0, 50600.0),
make_bar(4, 50300.0, 50100.0, 50200.0),
make_bar(5, 50100.0, 49900.0, 50000.0),
make_bar(6, 50500.0, 50300.0, 50400.0),
];
for bar in &bars {
if let Some(p) = state.process_bar(bar).newly_confirmed {
kinds.push(p.kind);
}
}
for w in kinds.windows(2) {
assert_ne!(w[0], w[1]);
}
}
#[test]
fn test_pending_updates() {
let mut state = ZigZagState::new(test_config());
state.process_bar(&make_bar(0, 50100.0, 50000.0, 50050.0));
state.process_bar(&make_bar(1, 50400.0, 50200.0, 50300.0));
let p1 = state.pending().unwrap();
assert_eq!(p1.kind, PivotKind::High);
let output = state.process_bar(&make_bar(2, 50500.0, 50350.0, 50450.0));
assert!(output.pending_updated);
assert_eq!(state.pending().unwrap().price, fp(50500.0));
}
#[test]
fn test_segment_formation() {
let mut state = ZigZagState::new(test_config());
let mut segments = Vec::new();
let bars = vec![
make_bar(0, 50100.0, 50000.0, 50050.0),
make_bar(1, 50500.0, 50300.0, 50400.0),
make_bar(2, 50800.0, 50600.0, 50700.0),
make_bar(3, 51000.0, 50800.0, 50900.0),
make_bar(4, 50700.0, 50400.0, 50500.0),
make_bar(5, 50300.0, 50100.0, 50200.0),
make_bar(6, 50600.0, 50400.0, 50500.0),
];
for bar in &bars {
if let Some(seg) = state.process_bar(bar).completed_segment {
segments.push(seg);
}
}
assert!(!segments.is_empty());
let seg = &segments[0];
assert_eq!(seg.l0.kind, PivotKind::Low);
assert_eq!(seg.h1.kind, PivotKind::High);
assert_eq!(seg.l2.kind, PivotKind::Low);
assert!(seg.segment_size > 0);
}
#[test]
fn test_generation_monotonic() {
let mut state = ZigZagState::new(test_config());
let mut gens: Vec<u64> = Vec::new();
let bars = vec![
make_bar(0, 50100.0, 50000.0, 50050.0),
make_bar(1, 50500.0, 50200.0, 50300.0),
make_bar(2, 51000.0, 50800.0, 50900.0),
make_bar(3, 50500.0, 50200.0, 50300.0),
make_bar(4, 50100.0, 49800.0, 49900.0),
make_bar(5, 50500.0, 50300.0, 50400.0),
];
for bar in &bars {
if let Some(p) = state.process_bar(bar).newly_confirmed {
if let ConfirmationStatus::Confirmed { generation, .. } = p.status {
gens.push(generation);
}
}
}
for w in gens.windows(2) {
assert!(w[1] > w[0]);
}
}
#[test]
fn test_no_event_on_quiet_bar() {
let mut state = ZigZagState::new(test_config());
state.process_bar(&make_bar(0, 50100.0, 50000.0, 50050.0));
state.process_bar(&make_bar(1, 50500.0, 50200.0, 50300.0));
let output = state.process_bar(&make_bar(2, 50450.0, 50250.0, 50350.0));
assert!(!output.pending_updated);
assert!(output.newly_confirmed.is_none());
}
#[test]
fn test_up_reversal_no_segment() {
let mut state = ZigZagState::new(test_config());
let bars = vec![
make_bar(0, 50100.0, 50000.0, 50050.0),
make_bar(1, 50500.0, 50200.0, 50300.0), make_bar(2, 50800.0, 50600.0, 50700.0),
make_bar(3, 51000.0, 50800.0, 50900.0), make_bar(4, 50700.0, 50400.0, 50500.0),
make_bar(5, 50300.0, 50100.0, 50200.0), make_bar(6, 50600.0, 50400.0, 50500.0),
make_bar(7, 50900.0, 50700.0, 50800.0), make_bar(8, 51200.0, 51000.0, 51100.0),
make_bar(9, 51400.0, 51200.0, 51300.0), make_bar(10, 51000.0, 50700.0, 50800.0),
make_bar(11, 50600.0, 50400.0, 50500.0), ];
for bar in &bars {
let output = state.process_bar(bar);
if let Some(ref pivot) = output.newly_confirmed {
if pivot.kind == PivotKind::High {
assert!(
output.completed_segment.is_none(),
"Up→Down reversal at bar {} produced a segment (should never happen)",
bar.index
);
}
}
}
let high_count = state
.confirmed_pivots()
.iter()
.filter(|p| p.kind == PivotKind::High)
.count();
assert!(high_count >= 2, "expected ≥2 High pivots, got {high_count}");
}
#[test]
fn test_down_reversal_produces_segment() {
let mut state = ZigZagState::new(test_config());
let mut got_segment = false;
let bars = vec![
make_bar(0, 50100.0, 50000.0, 50050.0),
make_bar(1, 50500.0, 50300.0, 50400.0), make_bar(2, 50800.0, 50600.0, 50700.0),
make_bar(3, 51000.0, 50800.0, 50900.0),
make_bar(4, 50700.0, 50400.0, 50500.0),
make_bar(5, 50300.0, 50100.0, 50200.0), make_bar(6, 50600.0, 50400.0, 50500.0),
make_bar(7, 50900.0, 50700.0, 50800.0), ];
for bar in &bars {
let output = state.process_bar(bar);
if output.completed_segment.is_some() {
got_segment = true;
let seg = output.completed_segment.unwrap();
assert_eq!(seg.l0.kind, PivotKind::Low);
assert_eq!(seg.h1.kind, PivotKind::High);
assert_eq!(seg.l2.kind, PivotKind::Low);
}
}
assert!(
got_segment,
"expected a segment from Down→Up reversal with ≥3 pivots"
);
}
#[test]
fn test_pending_updates_down() {
let mut state = ZigZagState::new(test_config());
state.process_bar(&make_bar(0, 50500.0, 50400.0, 50450.0));
state.process_bar(&make_bar(1, 50200.0, 50100.0, 50150.0)); assert!(state.is_initialized());
let p1 = state.pending().unwrap();
assert_eq!(p1.kind, PivotKind::Low);
let output = state.process_bar(&make_bar(2, 50150.0, 50050.0, 50100.0));
assert!(output.pending_updated);
assert_eq!(state.pending().unwrap().price, fp(50050.0));
let output2 = state.process_bar(&make_bar(3, 50100.0, 49950.0, 50000.0));
assert!(output2.pending_updated);
assert_eq!(state.pending().unwrap().price, fp(49950.0));
let output3 = state.process_bar(&make_bar(4, 50050.0, 49980.0, 50000.0));
assert!(!output3.pending_updated);
}
#[test]
fn test_base_class_assignment() {
let mut state = ZigZagState::new(test_config());
let mut segments = Vec::new();
let bars = vec![
make_bar(0, 50100.0, 50000.0, 50050.0),
make_bar(1, 50500.0, 50200.0, 50400.0),
make_bar(2, 50800.0, 50600.0, 50700.0),
make_bar(3, 51100.0, 50900.0, 51000.0),
make_bar(4, 50700.0, 50400.0, 50500.0),
make_bar(5, 50500.0, 50300.0, 50400.0),
make_bar(6, 50900.0, 50700.0, 50800.0),
];
for bar in &bars {
if let Some(seg) = state.process_bar(bar).completed_segment {
segments.push(seg);
}
}
if !segments.is_empty() {
let seg = &segments[0];
if seg.l2.price > seg.l0.price {
assert_eq!(seg.base_class, BaseClass::HL);
assert!(seg.z > 0.0 && seg.z < 1.0);
}
}
}
#[test]
fn test_formation_detection() {
use crate::zigzag::types::{HighClass, Formation};
let mut state = ZigZagState::new(test_config());
let mut formations: Vec<Formation> = Vec::new();
let bars = vec![
make_bar(0, 50100.0, 50000.0, 50050.0),
make_bar(1, 50500.0, 50200.0, 50400.0), make_bar(2, 50800.0, 50600.0, 50700.0),
make_bar(3, 51000.0, 50800.0, 50900.0), make_bar(4, 50700.0, 50500.0, 50600.0),
make_bar(5, 50500.0, 50350.0, 50400.0), make_bar(6, 50700.0, 50500.0, 50600.0),
make_bar(7, 50900.0, 50700.0, 50800.0), make_bar(8, 51200.0, 51000.0, 51100.0),
make_bar(9, 51500.0, 51300.0, 51400.0), make_bar(10, 51100.0, 50900.0, 51000.0),
make_bar(11, 50800.0, 50600.0, 50700.0), make_bar(12, 51000.0, 50800.0, 50900.0),
make_bar(13, 51200.0, 51000.0, 51100.0), ];
for bar in &bars {
let output = state.process_bar(bar);
if let Some(f) = output.completed_formation {
formations.push(f);
}
}
assert!(
!formations.is_empty(),
"expected at least one formation from 5+ pivots, got none. \
confirmed pivots: {}",
state.confirmed_pivots().len()
);
let f = &formations[0];
assert_eq!(f.l0.kind, PivotKind::Low);
assert_eq!(f.h1.kind, PivotKind::High);
assert_eq!(f.l2.kind, PivotKind::Low);
assert_eq!(f.h3.kind, PivotKind::High);
assert!(f.first_leg_size > 0);
assert!(f.second_leg_size > 0);
assert_eq!(f.base_class, BaseClass::HL);
assert_eq!(f.high_class, HighClass::HH);
}
}