wickra_core/indicators/
triple_top_bottom.rs1use crate::indicators::pattern_swing::{
4 approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
5};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
26pub struct TripleTopBottom {
27 swing: SwingTracker,
28 has_emitted: bool,
29}
30
31impl TripleTopBottom {
32 pub const fn new() -> Self {
34 Self {
35 swing: SwingTracker::new(SWING_THRESHOLD, 5),
36 has_emitted: false,
37 }
38 }
39}
40
41impl Default for TripleTopBottom {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl Indicator for TripleTopBottom {
48 type Input = Candle;
49 type Output = f64;
50
51 #[inline]
52 fn update(&mut self, candle: Candle) -> Option<f64> {
53 let advanced = self.swing.update(candle);
54 let pivots = self.swing.pivots();
55 if pivots.len() < 5 {
58 return None;
59 }
60 self.has_emitted = true;
61 if !advanced {
64 return Some(0.0);
65 }
66 let n = pivots.len();
67 let first = pivots[n - 5];
68 let middle = pivots[n - 3];
69 let last = pivots[n - 1];
70 let outer_match = approx_equal(first.price, middle.price, LEVEL_TOLERANCE);
71 let inner_match = approx_equal(middle.price, last.price, LEVEL_TOLERANCE);
72 if outer_match && inner_match {
73 return Some(if last.direction > 0.0 { -1.0 } else { 1.0 });
74 }
75 Some(0.0)
76 }
77
78 fn reset(&mut self) {
79 self.swing.reset();
80 self.has_emitted = false;
81 }
82
83 #[inline]
84 fn warmup_period(&self) -> usize {
85 6
88 }
89
90 #[inline]
91 fn is_ready(&self) -> bool {
92 self.has_emitted
93 }
94
95 #[inline]
96 fn name(&self) -> &'static str {
97 "TripleTopBottom"
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::indicators::pattern_swing::candles_for_pivots;
105 use crate::traits::BatchExt;
106
107 fn run(pivots: &[f64]) -> Vec<f64> {
108 let mut indicator = TripleTopBottom::new();
109 candles_for_pivots(pivots)
110 .into_iter()
111 .filter_map(|c| indicator.update(c))
112 .collect()
113 }
114
115 #[test]
116 fn accessors_and_metadata() {
117 let indicator = TripleTopBottom::new();
118 assert_eq!(indicator.name(), "TripleTopBottom");
119 assert_eq!(indicator.warmup_period(), 6);
120 assert!(!indicator.is_ready());
121 assert!(!TripleTopBottom::default().is_ready());
122 }
123
124 #[test]
125 fn triple_top_is_minus_one() {
126 let out = run(&[120.0, 100.0, 121.0, 99.0, 119.0]);
128 assert_eq!(*out.last().unwrap(), -1.0);
129 assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
130 }
131
132 #[test]
133 fn triple_bottom_is_plus_one() {
134 let out = run(&[130.0, 100.0, 120.0, 99.0, 122.0, 101.0]);
136 assert_eq!(*out.last().unwrap(), 1.0);
137 }
138
139 #[test]
140 fn unequal_third_peak_does_not_trigger() {
141 let out = run(&[120.0, 100.0, 121.0, 99.0, 140.0]);
143 assert_eq!(*out.last().unwrap(), 0.0);
144 assert!(out.iter().all(|&x| x == 0.0));
145 }
146
147 #[test]
148 fn reset_clears_state() {
149 let mut indicator = TripleTopBottom::new();
150 for c in candles_for_pivots(&[120.0, 100.0, 121.0]) {
151 let _ = indicator.update(c);
152 }
153 indicator.reset();
154 assert!(!indicator.is_ready());
155 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
156 assert_eq!(indicator.update(c), None);
157 }
158
159 #[test]
160 fn batch_equals_streaming() {
161 let candles = candles_for_pivots(&[120.0, 100.0, 121.0, 99.0, 119.0]);
162 let mut a = TripleTopBottom::new();
163 let mut b = TripleTopBottom::new();
164 assert_eq!(
165 a.batch(&candles),
166 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
167 );
168 }
169}