use crate::stats::rolling_median;
use super::price_levels::{swing_fibonacci_levels, PriceLevel};
use super::zigzag_advanced::ZigZagNode;
#[derive(Debug, Clone, PartialEq)]
pub struct RuleViolation {
pub rule: String,
pub detail: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CorrectionVariant {
Zigzag,
Flat,
ExpandedFlat,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImpulseValidation {
pub valid: bool,
pub violations: Vec<RuleViolation>,
pub pullback_quality: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CorrectionValidation {
pub variant: CorrectionVariant,
pub valid: bool,
pub violations: Vec<RuleViolation>,
pub pullback_quality: f64,
}
fn nearest_fib_distance(ratio: f64) -> f64 {
const COMMON: [f64; 3] = [0.382, 0.5, 0.618];
COMMON
.iter()
.map(|r| (r - ratio).abs())
.fold(f64::INFINITY, f64::min)
}
pub fn validate_impulse(nodes: &[ZigZagNode]) -> Option<ImpulseValidation> {
if nodes.len() != 6 {
return None;
}
if nodes.windows(2).any(|p| p[0].is_high == p[1].is_high) {
return None;
}
let bullish = nodes[1].price > nodes[0].price;
let (w0, w1, w2, w3, w4, w5) = (
nodes[0].price,
nodes[1].price,
nodes[2].price,
nodes[3].price,
nodes[4].price,
nodes[5].price,
);
let mut violations = Vec::new();
let wave2_ok = if bullish { w2 > w0 } else { w2 < w0 };
if !wave2_ok {
violations.push(RuleViolation {
rule: "wave2_no_full_retrace".to_string(),
detail: "Wave 2 retraced beyond the start of wave 1".to_string(),
});
}
let len1 = (w1 - w0).abs();
let len3 = (w3 - w2).abs();
let len5 = (w5 - w4).abs();
if len3 < len1 && len3 < len5 {
violations.push(RuleViolation {
rule: "wave3_not_shortest".to_string(),
detail: "Wave 3 is the shortest of waves 1, 3, and 5".to_string(),
});
}
let wave4_ok = if bullish { w4 > w1 } else { w4 < w1 };
if !wave4_ok {
violations.push(RuleViolation {
rule: "wave4_no_overlap".to_string(),
detail: "Wave 4 entered wave 1's price territory".to_string(),
});
}
let retrace2 = if len1 > 0.0 {
(w0 - w2).abs() / len1
} else {
f64::INFINITY
};
let len34 = (w3 - w2).abs();
let retrace4 = if len34 > 0.0 {
(w3 - w4).abs() / len34
} else {
f64::INFINITY
};
let pullback_quality = if retrace2.is_finite() && retrace4.is_finite() {
let d2 = nearest_fib_distance(retrace2);
let d4 = nearest_fib_distance(retrace4);
(1.0 - (d2 + d4)).clamp(0.0, 1.0)
} else {
0.0
};
Some(ImpulseValidation {
valid: violations.is_empty(),
violations,
pullback_quality,
})
}
pub fn validate_correction(nodes: &[ZigZagNode]) -> Option<CorrectionValidation> {
if nodes.len() != 4 {
return None;
}
if nodes.windows(2).any(|p| p[0].is_high == p[1].is_high) {
return None;
}
let bearish_correction = nodes[1].price > nodes[0].price; let _ = bearish_correction;
let (n0, a, b, c) = (
nodes[0].price,
nodes[1].price,
nodes[2].price,
nodes[3].price,
);
let leg_a = (a - n0).abs();
let leg_b_retrace = if leg_a > 0.0 {
(b - a).abs() / leg_a
} else {
f64::INFINITY
};
let leg_c = (c - b).abs();
let c_vs_a = if leg_a > 0.0 {
leg_c / leg_a
} else {
f64::INFINITY
};
let variant = if leg_b_retrace >= 1.0 {
CorrectionVariant::ExpandedFlat
} else if leg_b_retrace >= 0.90 {
CorrectionVariant::Flat
} else {
CorrectionVariant::Zigzag
};
let mut violations = Vec::new();
let a_dir_down = a < n0;
let c_continues = if a_dir_down { c < b } else { c > b };
if !c_continues {
violations.push(RuleViolation {
rule: "wave_c_must_extend_past_b".to_string(),
detail: "Wave C did not continue past wave B in wave A's direction".to_string(),
});
}
if variant == CorrectionVariant::Zigzag && leg_b_retrace > 0.786 {
violations.push(RuleViolation {
rule: "zigzag_b_retrace_bound".to_string(),
detail: "Wave B retraced more than a Zigzag's typical bound (78.6%) without qualifying as a Flat".to_string(),
});
}
let quality_ref = match variant {
CorrectionVariant::Zigzag => nearest_fib_distance(leg_b_retrace.min(1.0)),
CorrectionVariant::Flat | CorrectionVariant::ExpandedFlat => {
(1.0 - c_vs_a.min(2.0) / 1.0).abs().min(1.0)
}
};
let pullback_quality = (1.0 - quality_ref).clamp(0.0, 1.0);
Some(CorrectionValidation {
variant,
valid: violations.is_empty(),
violations,
pullback_quality,
})
}
pub fn c_setup_levels(wave_a_start: f64, wave_a_end: f64, is_uptrend: bool) -> Vec<PriceLevel> {
let (high, low) = if wave_a_end >= wave_a_start {
(wave_a_end, wave_a_start)
} else {
(wave_a_start, wave_a_end)
};
swing_fibonacci_levels(high, low, is_uptrend)
}
#[derive(Debug, Clone, Default)]
pub struct FibonacciReactionMemory {
observations: Vec<(f64, Vec<f64>)>,
}
impl FibonacciReactionMemory {
pub fn new() -> Self {
let observations = super::price_levels::FIBONACCI_RATIOS
.iter()
.map(|&r| (r, Vec::new()))
.collect();
Self { observations }
}
pub fn record(&mut self, ratio: f64, reaction_magnitude_atr: f64) {
if let Some((_, bucket)) = self
.observations
.iter_mut()
.min_by(|(a, _), (b, _)| (a - ratio).abs().total_cmp(&(b - ratio).abs()))
{
bucket.push(reaction_magnitude_atr);
}
}
pub fn median_reaction(&self, ratio: f64) -> Option<f64> {
self.observations
.iter()
.min_by(|(a, _), (b, _)| (a - ratio).abs().total_cmp(&(b - ratio).abs()))
.filter(|(_, bucket)| !bucket.is_empty())
.map(|(_, bucket)| rolling_median(bucket))
}
pub fn observation_count(&self, ratio: f64) -> usize {
self.observations
.iter()
.min_by(|(a, _), (b, _)| (a - ratio).abs().total_cmp(&(b - ratio).abs()))
.map(|(_, bucket)| bucket.len())
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(ts: i64, price: f64, is_high: bool) -> ZigZagNode {
ZigZagNode {
timestamp: ts,
price,
is_high,
confirmed: true,
}
}
#[test]
fn test_valid_bullish_impulse_passes_all_rules() {
let nodes = vec![
node(0, 100.0, false), node(1, 120.0, true), node(2, 110.0, false), node(3, 140.0, true), node(4, 130.0, false), node(5, 150.0, true), ];
let result = validate_impulse(&nodes).unwrap();
assert!(result.valid, "violations: {:?}", result.violations);
assert!(result.pullback_quality > 0.0);
}
#[test]
fn test_impulse_rejects_wave2_full_retrace() {
let nodes = vec![
node(0, 100.0, false),
node(1, 120.0, true),
node(2, 95.0, false), node(3, 140.0, true),
node(4, 130.0, false),
node(5, 150.0, true),
];
let result = validate_impulse(&nodes).unwrap();
assert!(!result.valid);
assert!(result
.violations
.iter()
.any(|v| v.rule == "wave2_no_full_retrace"));
}
#[test]
fn test_impulse_rejects_wave4_overlap() {
let nodes = vec![
node(0, 100.0, false),
node(1, 120.0, true),
node(2, 110.0, false),
node(3, 140.0, true),
node(4, 115.0, false), node(5, 150.0, true),
];
let result = validate_impulse(&nodes).unwrap();
assert!(!result.valid);
assert!(result
.violations
.iter()
.any(|v| v.rule == "wave4_no_overlap"));
}
#[test]
fn test_impulse_rejects_wave3_shortest() {
let nodes = vec![
node(0, 100.0, false),
node(1, 130.0, true), node(2, 120.0, false),
node(3, 135.0, true), node(4, 125.0, false),
node(5, 160.0, true), ];
let result = validate_impulse(&nodes).unwrap();
assert!(!result.valid);
assert!(result
.violations
.iter()
.any(|v| v.rule == "wave3_not_shortest"));
}
#[test]
fn test_validate_impulse_requires_exactly_six_alternating_nodes() {
let too_few = vec![node(0, 100.0, false), node(1, 120.0, true)];
assert!(validate_impulse(&too_few).is_none());
let non_alternating = vec![
node(0, 100.0, false),
node(1, 120.0, false),
node(2, 110.0, false),
node(3, 140.0, true),
node(4, 130.0, false),
node(5, 150.0, true),
];
assert!(validate_impulse(&non_alternating).is_none());
}
#[test]
fn test_correction_classifies_zigzag_vs_flat() {
let zigzag = vec![
node(0, 150.0, true),
node(1, 130.0, false), node(2, 141.0, true), node(3, 120.0, false), ];
let result = validate_correction(&zigzag).unwrap();
assert_eq!(result.variant, CorrectionVariant::Zigzag);
let flat = vec![
node(0, 150.0, true),
node(1, 130.0, false), node(2, 149.0, true), node(3, 128.0, false), ];
let result = validate_correction(&flat).unwrap();
assert_eq!(result.variant, CorrectionVariant::Flat);
}
#[test]
fn test_correction_rejects_c_not_extending_past_b() {
let nodes = vec![
node(0, 150.0, true),
node(1, 130.0, false),
node(2, 141.0, true),
node(3, 145.0, false),
];
let result = validate_correction(&nodes).unwrap();
assert!(!result.valid);
assert!(result
.violations
.iter()
.any(|v| v.rule == "wave_c_must_extend_past_b"));
}
#[test]
fn test_c_setup_levels_delegate_to_swing_fibonacci() {
let levels = c_setup_levels(100.0, 150.0, true);
assert_eq!(
levels.len(),
super::super::price_levels::FIBONACCI_RATIOS.len()
);
}
#[test]
fn test_reaction_memory_buckets_by_nearest_ratio() {
let mut memory = FibonacciReactionMemory::new();
memory.record(0.62, 1.5);
memory.record(0.615, 1.7);
memory.record(0.235, 0.5);
assert_eq!(memory.observation_count(0.618), 2);
let median = memory.median_reaction(0.618).unwrap();
assert!((median - 1.6).abs() < 0.2);
assert_eq!(memory.observation_count(0.236), 1);
}
}