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 fn update(&mut self, candle: Candle) -> Option<f64> {
52 self.has_emitted = true;
53 if !self.swing.update(candle) {
54 return Some(0.0);
55 }
56 let pivots = self.swing.pivots();
57 if pivots.len() < 5 {
58 return Some(0.0);
59 }
60 let n = pivots.len();
61 let first = pivots[n - 5];
62 let middle = pivots[n - 3];
63 let last = pivots[n - 1];
64 let outer_match = approx_equal(first.price, middle.price, LEVEL_TOLERANCE);
65 let inner_match = approx_equal(middle.price, last.price, LEVEL_TOLERANCE);
66 if outer_match && inner_match {
67 return Some(if last.direction > 0.0 { -1.0 } else { 1.0 });
68 }
69 Some(0.0)
70 }
71
72 fn reset(&mut self) {
73 self.swing.reset();
74 self.has_emitted = false;
75 }
76
77 fn warmup_period(&self) -> usize {
78 6
81 }
82
83 fn is_ready(&self) -> bool {
84 self.has_emitted
85 }
86
87 fn name(&self) -> &'static str {
88 "TripleTopBottom"
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::indicators::pattern_swing::candles_for_pivots;
96 use crate::traits::BatchExt;
97
98 fn run(pivots: &[f64]) -> Vec<f64> {
99 let mut indicator = TripleTopBottom::new();
100 candles_for_pivots(pivots)
101 .into_iter()
102 .map(|c| indicator.update(c).unwrap())
103 .collect()
104 }
105
106 #[test]
107 fn accessors_and_metadata() {
108 let indicator = TripleTopBottom::new();
109 assert_eq!(indicator.name(), "TripleTopBottom");
110 assert_eq!(indicator.warmup_period(), 6);
111 assert!(!indicator.is_ready());
112 assert!(!TripleTopBottom::default().is_ready());
113 }
114
115 #[test]
116 fn triple_top_is_minus_one() {
117 let out = run(&[120.0, 100.0, 121.0, 99.0, 119.0]);
119 assert_eq!(*out.last().unwrap(), -1.0);
120 assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
121 }
122
123 #[test]
124 fn triple_bottom_is_plus_one() {
125 let out = run(&[130.0, 100.0, 120.0, 99.0, 122.0, 101.0]);
127 assert_eq!(*out.last().unwrap(), 1.0);
128 }
129
130 #[test]
131 fn unequal_third_peak_does_not_trigger() {
132 let out = run(&[120.0, 100.0, 121.0, 99.0, 140.0]);
134 assert_eq!(*out.last().unwrap(), 0.0);
135 assert!(out.iter().all(|&x| x == 0.0));
136 }
137
138 #[test]
139 fn reset_clears_state() {
140 let mut indicator = TripleTopBottom::new();
141 for c in candles_for_pivots(&[120.0, 100.0, 121.0]) {
142 let _ = indicator.update(c);
143 }
144 indicator.reset();
145 assert!(!indicator.is_ready());
146 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
147 assert_eq!(indicator.update(c), Some(0.0));
148 }
149
150 #[test]
151 fn batch_equals_streaming() {
152 let candles = candles_for_pivots(&[120.0, 100.0, 121.0, 99.0, 119.0]);
153 let mut a = TripleTopBottom::new();
154 let mut b = TripleTopBottom::new();
155 assert_eq!(
156 a.batch(&candles),
157 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
158 );
159 }
160}