use crate::zigzag::types::{Pivot, PivotKind};
#[derive(Debug, Clone)]
pub struct CTrendsConfig {
pub min_retrace_ratio: f64,
pub min_counter_bars: usize,
}
impl Default for CTrendsConfig {
fn default() -> Self {
Self {
min_retrace_ratio: 0.25,
min_counter_bars: 2,
}
}
}
pub fn filter_ctrends(pivots: &[Pivot], config: &CTrendsConfig) -> Vec<Pivot> {
if pivots.len() < 4 {
return pivots.to_vec();
}
let mut work = pivots.to_vec();
let mut changed = true;
while changed {
changed = false;
let mut result: Vec<Pivot> = Vec::with_capacity(work.len());
let mut i = 0;
while i < work.len() {
if i + 2 < work.len() {
let p0 = &work[i];
let p1 = &work[i + 1];
let p2 = &work[i + 2];
let main_move = (p1.price - p0.price).abs();
let counter_move = (p2.price - p1.price).abs();
let counter_bars = p2.bar_index.saturating_sub(p1.bar_index);
let should_merge = if main_move == 0 {
false
} else {
let ratio = counter_move as f64 / main_move as f64;
ratio < config.min_retrace_ratio || counter_bars < config.min_counter_bars
};
if should_merge {
let merged = if p0.kind == PivotKind::Low {
if p0.price <= p2.price { *p0 } else { *p2 }
} else if p0.price >= p2.price {
*p0
} else {
*p2
};
result.push(merged);
i += 3; changed = true;
continue;
}
}
result.push(work[i]);
i += 1;
}
work = result;
}
debug_assert!(verify_alternation(&work));
work
}
fn verify_alternation(pivots: &[Pivot]) -> bool {
pivots.windows(2).all(|w| w[0].kind != w[1].kind)
}
pub fn is_noise_counter_move(
trend_move_size: i64,
counter_move_size: i64,
counter_bars: usize,
config: &CTrendsConfig,
) -> bool {
if trend_move_size == 0 {
return false;
}
let ratio = counter_move_size as f64 / trend_move_size as f64;
ratio < config.min_retrace_ratio || counter_bars < config.min_counter_bars
}
#[cfg(test)]
mod tests {
use super::*;
use crate::zigzag::types::ConfirmationStatus;
fn make_pivot(index: usize, price: i64, kind: PivotKind) -> Pivot {
Pivot {
bar_index: index,
timestamp_us: (index as i64) * 1_000_000,
price,
kind,
status: ConfirmationStatus::Confirmed {
confirmed_at_bar: index + 3,
generation: index as u64,
},
}
}
#[test]
fn test_no_filtering_needed() {
let pivots = vec![
make_pivot(0, 100, PivotKind::Low),
make_pivot(10, 200, PivotKind::High),
make_pivot(20, 150, PivotKind::Low),
make_pivot(30, 250, PivotKind::High),
];
let filtered = filter_ctrends(&pivots, &CTrendsConfig::default());
assert_eq!(filtered.len(), 4);
}
#[test]
fn test_filter_small_counter_move() {
let pivots = vec![
make_pivot(0, 100, PivotKind::Low),
make_pivot(10, 200, PivotKind::High),
make_pivot(12, 195, PivotKind::Low),
make_pivot(20, 250, PivotKind::High),
];
let filtered = filter_ctrends(&pivots, &CTrendsConfig::default());
assert!(filtered.len() < 4);
assert!(verify_alternation(&filtered));
}
#[test]
fn test_too_few_pivots() {
let pivots = vec![
make_pivot(0, 100, PivotKind::Low),
make_pivot(10, 200, PivotKind::High),
make_pivot(20, 150, PivotKind::Low),
];
let filtered = filter_ctrends(&pivots, &CTrendsConfig::default());
assert_eq!(filtered.len(), 3);
}
#[test]
fn test_is_noise() {
let config = CTrendsConfig::default();
assert!(is_noise_counter_move(100, 5, 5, &config));
assert!(!is_noise_counter_move(100, 30, 5, &config));
assert!(is_noise_counter_move(100, 30, 1, &config));
}
}