1use crate::model::SeriesCapabilities;
13use crate::stats::rolling_median;
14
15use super::price_levels::{swing_fibonacci_levels, PriceLevel};
16use super::zigzag_advanced::ZigZagNode;
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct RuleViolation {
21 pub rule: String,
22 pub detail: String,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CorrectionVariant {
27 Zigzag,
28 Flat,
29 ExpandedFlat,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct ImpulseValidation {
34 pub valid: bool,
35 pub violations: Vec<RuleViolation>,
36 pub pullback_quality: f64,
40 pub series_capabilities: Option<SeriesCapabilities>,
46}
47
48impl ImpulseValidation {
49 pub fn with_capabilities(mut self, capabilities: SeriesCapabilities) -> Self {
52 self.series_capabilities = Some(capabilities);
53 self
54 }
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub struct CorrectionValidation {
59 pub variant: CorrectionVariant,
60 pub valid: bool,
61 pub violations: Vec<RuleViolation>,
62 pub pullback_quality: f64,
63 pub series_capabilities: Option<SeriesCapabilities>,
65}
66
67impl CorrectionValidation {
68 pub fn with_capabilities(mut self, capabilities: SeriesCapabilities) -> Self {
71 self.series_capabilities = Some(capabilities);
72 self
73 }
74}
75
76fn nearest_fib_distance(ratio: f64) -> f64 {
77 const COMMON: [f64; 3] = [0.382, 0.5, 0.618];
78 COMMON
79 .iter()
80 .map(|r| (r - ratio).abs())
81 .fold(f64::INFINITY, f64::min)
82}
83
84pub fn validate_impulse(nodes: &[ZigZagNode]) -> Option<ImpulseValidation> {
89 let [node0, node1, node2, node3, node4, node5] = nodes else {
90 return None;
91 };
92 if nodes.windows(2).any(|p| match p {
93 [a, b] => a.is_high == b.is_high,
94 _ => false,
95 }) {
96 return None;
97 }
98
99 let bullish = node1.price > node0.price;
100 let (w0, w1, w2, w3, w4, w5) = (
101 node0.price,
102 node1.price,
103 node2.price,
104 node3.price,
105 node4.price,
106 node5.price,
107 );
108
109 let mut violations = Vec::new();
110
111 let wave2_ok = if bullish { w2 > w0 } else { w2 < w0 };
112 if !wave2_ok {
113 violations.push(RuleViolation {
114 rule: "wave2_no_full_retrace".to_string(),
115 detail: "Wave 2 retraced beyond the start of wave 1".to_string(),
116 });
117 }
118
119 let len1 = (w1 - w0).abs();
120 let len3 = (w3 - w2).abs();
121 let len5 = (w5 - w4).abs();
122 if len3 < len1 && len3 < len5 {
123 violations.push(RuleViolation {
124 rule: "wave3_not_shortest".to_string(),
125 detail: "Wave 3 is the shortest of waves 1, 3, and 5".to_string(),
126 });
127 }
128
129 let wave4_ok = if bullish { w4 > w1 } else { w4 < w1 };
130 if !wave4_ok {
131 violations.push(RuleViolation {
132 rule: "wave4_no_overlap".to_string(),
133 detail: "Wave 4 entered wave 1's price territory".to_string(),
134 });
135 }
136
137 let retrace2 = if len1 > 0.0 {
138 (w0 - w2).abs() / len1
139 } else {
140 f64::INFINITY
141 };
142 let len34 = (w3 - w2).abs();
143 let retrace4 = if len34 > 0.0 {
144 (w3 - w4).abs() / len34
145 } else {
146 f64::INFINITY
147 };
148 let pullback_quality = if retrace2.is_finite() && retrace4.is_finite() {
149 let d2 = nearest_fib_distance(retrace2);
150 let d4 = nearest_fib_distance(retrace4);
151 (1.0 - (d2 + d4)).clamp(0.0, 1.0)
152 } else {
153 0.0
154 };
155
156 Some(ImpulseValidation {
157 valid: violations.is_empty(),
158 violations,
159 pullback_quality,
160 series_capabilities: None,
161 })
162}
163
164pub fn validate_correction(nodes: &[ZigZagNode]) -> Option<CorrectionValidation> {
169 let [node0, node1, node2, node3] = nodes else {
170 return None;
171 };
172 if nodes.windows(2).any(|p| match p {
173 [a, b] => a.is_high == b.is_high,
174 _ => false,
175 }) {
176 return None;
177 }
178
179 let bearish_correction = node1.price > node0.price; let _ = bearish_correction;
181
182 let (n0, a, b, c) = (node0.price, node1.price, node2.price, node3.price);
183 let leg_a = (a - n0).abs();
184 let leg_b_retrace = if leg_a > 0.0 {
185 (b - a).abs() / leg_a
186 } else {
187 f64::INFINITY
188 };
189 let leg_c = (c - b).abs();
190 let c_vs_a = if leg_a > 0.0 {
191 leg_c / leg_a
192 } else {
193 f64::INFINITY
194 };
195
196 let variant = if leg_b_retrace >= 1.0 {
197 CorrectionVariant::ExpandedFlat
198 } else if leg_b_retrace >= 0.90 {
199 CorrectionVariant::Flat
200 } else {
201 CorrectionVariant::Zigzag
202 };
203
204 let mut violations = Vec::new();
205 let a_dir_down = a < n0;
208 let c_continues = if a_dir_down { c < b } else { c > b };
209 if !c_continues {
210 violations.push(RuleViolation {
211 rule: "wave_c_must_extend_past_b".to_string(),
212 detail: "Wave C did not continue past wave B in wave A's direction".to_string(),
213 });
214 }
215
216 if variant == CorrectionVariant::Zigzag && leg_b_retrace > 0.786 {
217 violations.push(RuleViolation {
218 rule: "zigzag_b_retrace_bound".to_string(),
219 detail: "Wave B retraced more than a Zigzag's typical bound (78.6%) without qualifying as a Flat".to_string(),
220 });
221 }
222
223 let quality_ref = match variant {
224 CorrectionVariant::Zigzag => nearest_fib_distance(leg_b_retrace.min(1.0)),
225 CorrectionVariant::Flat | CorrectionVariant::ExpandedFlat => {
226 (1.0 - c_vs_a.min(2.0) / 1.0).abs().min(1.0)
227 }
228 };
229 let pullback_quality = (1.0 - quality_ref).clamp(0.0, 1.0);
230
231 Some(CorrectionValidation {
232 variant,
233 valid: violations.is_empty(),
234 violations,
235 pullback_quality,
236 series_capabilities: None,
237 })
238}
239
240pub fn c_setup_levels(wave_a_start: f64, wave_a_end: f64, is_uptrend: bool) -> Vec<PriceLevel> {
244 let (high, low) = if wave_a_end >= wave_a_start {
245 (wave_a_end, wave_a_start)
246 } else {
247 (wave_a_start, wave_a_end)
248 };
249 swing_fibonacci_levels(high, low, is_uptrend)
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub enum DiagonalKind {
257 Leading,
258 Ending,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum DiagonalVariant {
268 Contracting,
269 Expanding,
270}
271
272#[derive(Debug, Clone, PartialEq)]
273pub struct DiagonalValidation {
274 pub kind: DiagonalKind,
275 pub variant: DiagonalVariant,
276 pub valid: bool,
277 pub violations: Vec<RuleViolation>,
278 pub wave4_overlaps_wave1: bool,
282 pub series_capabilities: Option<SeriesCapabilities>,
284}
285
286impl DiagonalValidation {
287 pub fn with_capabilities(mut self, capabilities: SeriesCapabilities) -> Self {
290 self.series_capabilities = Some(capabilities);
291 self
292 }
293}
294
295pub fn validate_diagonal(nodes: &[ZigZagNode], kind: DiagonalKind) -> Option<DiagonalValidation> {
300 let [node0, node1, node2, node3, node4, node5] = nodes else {
301 return None;
302 };
303 if nodes.windows(2).any(|p| match p {
304 [a, b] => a.is_high == b.is_high,
305 _ => false,
306 }) {
307 return None;
308 }
309
310 let bullish = node1.price > node0.price;
311 let (w0, w1, w2, w3, w4, w5) = (
312 node0.price,
313 node1.price,
314 node2.price,
315 node3.price,
316 node4.price,
317 node5.price,
318 );
319
320 let mut violations = Vec::new();
321
322 let wave2_ok = if bullish { w2 > w0 } else { w2 < w0 };
323 if !wave2_ok {
324 violations.push(RuleViolation {
325 rule: "wave2_no_full_retrace".to_string(),
326 detail: "Wave 2 retraced beyond the start of wave 1".to_string(),
327 });
328 }
329
330 let len1 = (w1 - w0).abs();
331 let len2 = (w2 - w1).abs();
332 let len3 = (w3 - w2).abs();
333 let len4 = (w4 - w3).abs();
334 let len5 = (w5 - w4).abs();
335 if len3 < len1 && len3 < len5 {
336 violations.push(RuleViolation {
337 rule: "wave3_not_shortest".to_string(),
338 detail: "Wave 3 is the shortest of waves 1, 3, and 5".to_string(),
339 });
340 }
341
342 let wave4_overlaps_wave1 = if bullish { w4 <= w1 } else { w4 >= w1 };
343
344 let variant = if len5 < len3 && len3 < len1 && len4 < len2 {
345 DiagonalVariant::Contracting
346 } else {
347 DiagonalVariant::Expanding
348 };
349
350 Some(DiagonalValidation {
351 kind,
352 variant,
353 valid: violations.is_empty(),
354 violations,
355 wave4_overlaps_wave1,
356 series_capabilities: None,
357 })
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366pub enum TriangleVariant {
367 Contracting,
368 Expanding,
369 RunningOrBarrier,
370}
371
372#[derive(Debug, Clone, PartialEq)]
373pub struct TriangleValidation {
374 pub variant: TriangleVariant,
375 pub series_capabilities: Option<SeriesCapabilities>,
377}
378
379impl TriangleValidation {
380 pub fn with_capabilities(mut self, capabilities: SeriesCapabilities) -> Self {
383 self.series_capabilities = Some(capabilities);
384 self
385 }
386}
387
388pub fn validate_triangle(nodes: &[ZigZagNode]) -> Option<TriangleValidation> {
393 let [node0, node1, node2, node3, node4, node5] = nodes else {
394 return None;
395 };
396 if nodes.windows(2).any(|p| match p {
397 [a, b] => a.is_high == b.is_high,
398 _ => false,
399 }) {
400 return None;
401 }
402
403 let von_tief = node1.price > node0.price;
413 let (erstes_extrem, zweites_extrem, drittes_extrem) = (node1.price, node3.price, node5.price);
414 let (erste_gegenseite, zweite_gegenseite) = (node2.price, node4.price);
415
416 let obere_faellt = if von_tief {
417 zweites_extrem < erstes_extrem && drittes_extrem < zweites_extrem
418 } else {
419 zweite_gegenseite < erste_gegenseite
420 };
421 let untere_steigt = if von_tief {
422 zweite_gegenseite > erste_gegenseite
423 } else {
424 zweites_extrem > erstes_extrem && drittes_extrem > zweites_extrem
425 };
426
427 let obere_steigt = if von_tief {
428 zweites_extrem > erstes_extrem && drittes_extrem > zweites_extrem
429 } else {
430 zweite_gegenseite > erste_gegenseite
431 };
432 let untere_faellt = if von_tief {
433 zweite_gegenseite < erste_gegenseite
434 } else {
435 zweites_extrem < erstes_extrem && drittes_extrem < zweites_extrem
436 };
437
438 let variant = if obere_faellt && untere_steigt {
439 TriangleVariant::Contracting
440 } else if obere_steigt && untere_faellt {
441 TriangleVariant::Expanding
442 } else {
443 TriangleVariant::RunningOrBarrier
444 };
445
446 Some(TriangleValidation {
447 variant,
448 series_capabilities: None,
449 })
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub enum CombinationVariant {
456 DoubleThree,
458 TripleThree,
460}
461
462#[derive(Debug, Clone, PartialEq)]
463pub struct CombinationValidation {
464 pub variant: CombinationVariant,
465 pub segments: Vec<CorrectionValidation>,
467 pub valid: bool,
469 pub series_capabilities: Option<SeriesCapabilities>,
471}
472
473impl CombinationValidation {
474 pub fn with_capabilities(mut self, capabilities: SeriesCapabilities) -> Self {
477 self.series_capabilities = Some(capabilities);
478 self
479 }
480}
481
482pub fn validate_combination(nodes: &[ZigZagNode]) -> Option<CombinationValidation> {
489 if nodes.len() != 8 && nodes.len() != 12 {
490 return None;
491 }
492 if nodes.windows(2).any(|p| match p {
493 [a, b] => a.is_high == b.is_high,
494 _ => false,
495 }) {
496 return None;
497 }
498
499 let variant = if nodes.len() == 8 {
500 CombinationVariant::DoubleThree
501 } else {
502 CombinationVariant::TripleThree
503 };
504
505 let segment_count = nodes.len() / 4;
511 let mut segments = Vec::with_capacity(segment_count);
512 for i in 0..segment_count {
513 let start = i * 4;
514 let segment = &nodes[start..start + 4];
515 let validation = validate_correction(segment)?;
516 segments.push(validation);
517 }
518
519 let valid = segments.iter().all(|s| s.valid);
520
521 Some(CombinationValidation {
522 variant,
523 segments,
524 valid,
525 series_capabilities: None,
526 })
527}
528
529pub fn identify_extended_wave(nodes: &[ZigZagNode]) -> Option<u8> {
534 let [node0, node1, node2, node3, node4, node5] = nodes else {
535 return None;
536 };
537 if nodes.windows(2).any(|p| match p {
538 [a, b] => a.is_high == b.is_high,
539 _ => false,
540 }) {
541 return None;
542 }
543
544 let len1 = (node1.price - node0.price).abs();
545 let len3 = (node3.price - node2.price).abs();
546 let len5 = (node5.price - node4.price).abs();
547
548 const EXTENSION_RATIO: f64 = 1.618;
549
550 if len1 >= EXTENSION_RATIO * len3.max(len5) {
551 Some(1)
552 } else if len3 >= EXTENSION_RATIO * len1.max(len5) {
553 Some(3)
554 } else if len5 >= EXTENSION_RATIO * len1.max(len3) {
555 Some(5)
556 } else {
557 None
558 }
559}
560
561pub fn is_truncated_fifth(nodes: &[ZigZagNode]) -> Option<bool> {
565 let [node0, node1, _node2, node3, _node4, node5] = nodes else {
566 return None;
567 };
568 if nodes.windows(2).any(|p| match p {
569 [a, b] => a.is_high == b.is_high,
570 _ => false,
571 }) {
572 return None;
573 }
574
575 let bullish = node1.price > node0.price;
576 Some(if bullish {
577 node5.price <= node3.price
578 } else {
579 node5.price >= node3.price
580 })
581}
582
583#[derive(Debug, Clone, Default)]
587pub struct FibonacciReactionMemory {
588 observations: Vec<(f64, Vec<f64>)>,
591}
592
593impl FibonacciReactionMemory {
594 pub fn new() -> Self {
595 let observations = super::price_levels::FIBONACCI_RATIOS
596 .iter()
597 .map(|&r| (r, Vec::new()))
598 .collect();
599 Self { observations }
600 }
601
602 pub fn record(&mut self, ratio: f64, reaction_magnitude_atr: f64) {
604 if let Some((_, bucket)) = self
605 .observations
606 .iter_mut()
607 .min_by(|(a, _), (b, _)| (a - ratio).abs().total_cmp(&(b - ratio).abs()))
608 {
609 bucket.push(reaction_magnitude_atr);
610 }
611 }
612
613 pub fn median_reaction(&self, ratio: f64) -> Option<f64> {
616 self.observations
617 .iter()
618 .min_by(|(a, _), (b, _)| (a - ratio).abs().total_cmp(&(b - ratio).abs()))
619 .filter(|(_, bucket)| !bucket.is_empty())
620 .map(|(_, bucket)| rolling_median(bucket))
621 }
622
623 pub fn observation_count(&self, ratio: f64) -> usize {
624 self.observations
625 .iter()
626 .min_by(|(a, _), (b, _)| (a - ratio).abs().total_cmp(&(b - ratio).abs()))
627 .map(|(_, bucket)| bucket.len())
628 .unwrap_or(0)
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 fn node(ts: i64, price: f64, is_high: bool) -> ZigZagNode {
637 ZigZagNode {
638 timestamp: ts,
639 price,
640 is_high,
641 confirmed: true,
642 }
643 }
644
645 #[test]
646 fn test_valid_bullish_impulse_passes_all_rules() {
647 let nodes = vec![
648 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), ];
655 let result = validate_impulse(&nodes).unwrap();
656 assert!(result.valid, "violations: {:?}", result.violations);
657 assert!(result.pullback_quality > 0.0);
658 }
659
660 #[test]
661 fn test_impulse_rejects_wave2_full_retrace() {
662 let nodes = vec![
663 node(0, 100.0, false),
664 node(1, 120.0, true),
665 node(2, 95.0, false), node(3, 140.0, true),
667 node(4, 130.0, false),
668 node(5, 150.0, true),
669 ];
670 let result = validate_impulse(&nodes).unwrap();
671 assert!(!result.valid);
672 assert!(result
673 .violations
674 .iter()
675 .any(|v| v.rule == "wave2_no_full_retrace"));
676 }
677
678 #[test]
679 fn test_impulse_rejects_wave4_overlap() {
680 let nodes = vec![
681 node(0, 100.0, false),
682 node(1, 120.0, true),
683 node(2, 110.0, false),
684 node(3, 140.0, true),
685 node(4, 115.0, false), node(5, 150.0, true),
687 ];
688 let result = validate_impulse(&nodes).unwrap();
689 assert!(!result.valid);
690 assert!(result
691 .violations
692 .iter()
693 .any(|v| v.rule == "wave4_no_overlap"));
694 }
695
696 #[test]
697 fn test_impulse_rejects_wave3_shortest() {
698 let nodes = vec![
699 node(0, 100.0, false),
700 node(1, 130.0, true), node(2, 120.0, false),
702 node(3, 135.0, true), node(4, 125.0, false),
704 node(5, 160.0, true), ];
706 let result = validate_impulse(&nodes).unwrap();
707 assert!(!result.valid);
708 assert!(result
709 .violations
710 .iter()
711 .any(|v| v.rule == "wave3_not_shortest"));
712 }
713
714 #[test]
715 fn test_validate_impulse_requires_exactly_six_alternating_nodes() {
716 let too_few = vec![node(0, 100.0, false), node(1, 120.0, true)];
717 assert!(validate_impulse(&too_few).is_none());
718
719 let non_alternating = vec![
720 node(0, 100.0, false),
721 node(1, 120.0, false),
722 node(2, 110.0, false),
723 node(3, 140.0, true),
724 node(4, 130.0, false),
725 node(5, 150.0, true),
726 ];
727 assert!(validate_impulse(&non_alternating).is_none());
728 }
729
730 #[test]
731 fn test_correction_classifies_zigzag_vs_flat() {
732 let zigzag = vec![
733 node(0, 150.0, true),
734 node(1, 130.0, false), node(2, 141.0, true), node(3, 120.0, false), ];
738 let result = validate_correction(&zigzag).unwrap();
739 assert_eq!(result.variant, CorrectionVariant::Zigzag);
740
741 let flat = vec![
742 node(0, 150.0, true),
743 node(1, 130.0, false), node(2, 149.0, true), node(3, 128.0, false), ];
747 let result = validate_correction(&flat).unwrap();
748 assert_eq!(result.variant, CorrectionVariant::Flat);
749 }
750
751 #[test]
752 fn test_correction_rejects_c_not_extending_past_b() {
753 let nodes = vec![
756 node(0, 150.0, true),
757 node(1, 130.0, false),
758 node(2, 141.0, true),
759 node(3, 145.0, false),
760 ];
761 let result = validate_correction(&nodes).unwrap();
762 assert!(!result.valid);
763 assert!(result
764 .violations
765 .iter()
766 .any(|v| v.rule == "wave_c_must_extend_past_b"));
767 }
768
769 #[test]
770 fn test_c_setup_levels_delegate_to_swing_fibonacci() {
771 let levels = c_setup_levels(100.0, 150.0, true);
772 assert_eq!(
773 levels.len(),
774 super::super::price_levels::FIBONACCI_RATIOS.len()
775 );
776 }
777
778 #[test]
779 fn test_reaction_memory_buckets_by_nearest_ratio() {
780 let mut memory = FibonacciReactionMemory::new();
781 memory.record(0.62, 1.5);
782 memory.record(0.615, 1.7);
783 memory.record(0.235, 0.5);
784
785 assert_eq!(memory.observation_count(0.618), 2);
786 let median = memory.median_reaction(0.618).unwrap();
787 assert!((median - 1.6).abs() < 0.2);
788 assert_eq!(memory.observation_count(0.236), 1);
789 }
790
791 fn sample_capabilities() -> SeriesCapabilities {
792 SeriesCapabilities {
793 volume: crate::model::VolumeKind::RealTurnover,
794 trade_direction: false,
795 session: crate::model::SessionKind::Regular,
796 continuity: crate::model::ContinuityKind::SingleContract,
797 price_adjustment: crate::model::PriceAdjustment::Raw,
798 provenance: crate::model::Provenance::Exchange,
799 liquidity_tier: crate::model::LiquidityTier::Deep,
800 }
801 }
802
803 #[test]
804 fn test_impulse_validation_defaults_to_no_capabilities_and_can_be_tagged() {
805 let nodes = vec![
806 node(0, 100.0, false),
807 node(1, 120.0, true),
808 node(2, 110.0, false),
809 node(3, 140.0, true),
810 node(4, 130.0, false),
811 node(5, 150.0, true),
812 ];
813 let result = validate_impulse(&nodes).unwrap();
814 assert_eq!(result.series_capabilities, None);
815
816 let tagged = result.with_capabilities(sample_capabilities());
817 assert_eq!(tagged.series_capabilities, Some(sample_capabilities()));
818 }
819
820 #[test]
821 fn test_correction_validation_defaults_to_no_capabilities_and_can_be_tagged() {
822 let nodes = vec![
823 node(0, 150.0, true),
824 node(1, 130.0, false),
825 node(2, 141.0, true),
826 node(3, 100.0, false),
827 ];
828 let result = validate_correction(&nodes).unwrap();
829 assert_eq!(result.series_capabilities, None);
830
831 let tagged = result.with_capabilities(sample_capabilities());
832 assert_eq!(tagged.series_capabilities, Some(sample_capabilities()));
833 }
834
835 #[test]
836 fn test_diagonal_allows_wave4_overlap_that_would_fail_a_plain_impulse() {
837 let nodes = vec![
841 node(0, 100.0, false),
842 node(1, 120.0, true),
843 node(2, 110.0, false),
844 node(3, 140.0, true),
845 node(4, 115.0, false),
846 node(5, 150.0, true),
847 ];
848 assert!(validate_impulse(&nodes)
849 .unwrap()
850 .violations
851 .iter()
852 .any(|v| v.rule == "wave4_no_overlap"));
853
854 let result = validate_diagonal(&nodes, DiagonalKind::Ending).unwrap();
855 assert!(result.valid, "violations: {:?}", result.violations);
856 assert!(result.wave4_overlaps_wave1);
857 }
858
859 #[test]
860 fn test_diagonal_still_rejects_wave2_full_retrace() {
861 let nodes = vec![
862 node(0, 100.0, false),
863 node(1, 120.0, true),
864 node(2, 95.0, false),
865 node(3, 140.0, true),
866 node(4, 130.0, false),
867 node(5, 150.0, true),
868 ];
869 let result = validate_diagonal(&nodes, DiagonalKind::Leading).unwrap();
870 assert!(!result.valid);
871 assert!(result
872 .violations
873 .iter()
874 .any(|v| v.rule == "wave2_no_full_retrace"));
875 }
876
877 #[test]
878 fn test_diagonal_classifies_contracting_vs_expanding() {
879 let contracting = vec![
880 node(0, 100.0, false),
881 node(1, 120.0, true), node(2, 110.0, false), node(3, 125.0, true), node(4, 116.0, false), node(5, 124.0, true), ];
887 let result = validate_diagonal(&contracting, DiagonalKind::Ending).unwrap();
888 assert!(result.valid, "violations: {:?}", result.violations);
889 assert_eq!(result.variant, DiagonalVariant::Contracting);
890
891 let expanding = vec![
892 node(0, 100.0, false),
893 node(1, 110.0, true), node(2, 102.0, false), node(3, 125.0, true), node(4, 90.0, false), node(5, 140.0, true), ];
899 let result = validate_diagonal(&expanding, DiagonalKind::Leading).unwrap();
900 assert!(result.valid, "violations: {:?}", result.violations);
901 assert_eq!(result.variant, DiagonalVariant::Expanding);
902 }
903
904 #[test]
905 fn test_triangle_classifies_contracting_expanding_and_running() {
906 let contracting = vec![
908 node(0, 100.0, false),
909 node(1, 130.0, true),
910 node(2, 110.0, false),
911 node(3, 125.0, true),
912 node(4, 115.0, false),
913 node(5, 122.0, true),
914 ];
915 assert_eq!(
916 validate_triangle(&contracting).unwrap().variant,
917 TriangleVariant::Contracting
918 );
919
920 let expanding = vec![
922 node(0, 100.0, false),
923 node(1, 110.0, true),
924 node(2, 95.0, false),
925 node(3, 120.0, true),
926 node(4, 85.0, false),
927 node(5, 140.0, true),
928 ];
929 assert_eq!(
930 validate_triangle(&expanding).unwrap().variant,
931 TriangleVariant::Expanding
932 );
933
934 let running = vec![
937 node(0, 100.0, false),
938 node(1, 130.0, true),
939 node(2, 110.0, false),
940 node(3, 125.0, true),
941 node(4, 108.0, false),
942 node(5, 120.0, true),
943 ];
944 assert_eq!(
945 validate_triangle(&running).unwrap().variant,
946 TriangleVariant::RunningOrBarrier
947 );
948
949 let contracting_von_hoch = vec![
952 node(0, 140.0, true),
953 node(1, 110.0, false),
954 node(2, 130.0, true),
955 node(3, 115.0, false),
956 node(4, 125.0, true),
957 node(5, 118.0, false),
958 ];
959 assert_eq!(
960 validate_triangle(&contracting_von_hoch).unwrap().variant,
961 TriangleVariant::Contracting
962 );
963 }
964
965 #[test]
966 fn test_validate_combination_classifies_wxy_from_two_valid_corrections() {
967 let nodes = vec![
968 node(0, 150.0, true),
969 node(1, 130.0, false), node(2, 141.0, true), node(3, 120.0, false), node(4, 140.0, true), node(5, 125.0, false), node(6, 134.0, true), node(7, 112.0, false), ];
977 let result = validate_combination(&nodes).unwrap();
978 assert_eq!(result.variant, CombinationVariant::DoubleThree);
979 assert_eq!(result.segments.len(), 2);
980 assert!(result.valid, "segments: {:?}", result.segments);
981 assert!(result
982 .segments
983 .iter()
984 .all(|s| s.variant == CorrectionVariant::Zigzag));
985 }
986
987 #[test]
988 fn test_validate_combination_rejects_wrong_node_count() {
989 let too_few = vec![node(0, 100.0, false), node(1, 120.0, true)];
990 assert!(validate_combination(&too_few).is_none());
991 }
992
993 #[test]
994 fn test_identify_extended_wave() {
995 let wave3_extended = vec![
996 node(0, 100.0, false),
997 node(1, 110.0, true), node(2, 105.0, false),
999 node(3, 145.0, true), node(4, 135.0, false),
1001 node(5, 150.0, true), ];
1003 assert_eq!(identify_extended_wave(&wave3_extended), Some(3));
1004
1005 let no_extension = vec![
1006 node(0, 100.0, false),
1007 node(1, 120.0, true), node(2, 110.0, false),
1009 node(3, 142.0, true), node(4, 130.0, false),
1011 node(5, 148.0, true), ];
1013 assert_eq!(identify_extended_wave(&no_extension), None);
1014 }
1015
1016 #[test]
1017 fn test_is_truncated_fifth() {
1018 let truncated = vec![
1019 node(0, 100.0, false),
1020 node(1, 130.0, true),
1021 node(2, 115.0, false),
1022 node(3, 150.0, true),
1023 node(4, 135.0, false),
1024 node(5, 145.0, true), ];
1026 assert_eq!(is_truncated_fifth(&truncated), Some(true));
1027
1028 let not_truncated = vec![
1029 node(0, 100.0, false),
1030 node(1, 130.0, true),
1031 node(2, 115.0, false),
1032 node(3, 150.0, true),
1033 node(4, 135.0, false),
1034 node(5, 160.0, true),
1035 ];
1036 assert_eq!(is_truncated_fifth(¬_truncated), Some(false));
1037 }
1038}