wickra_core/indicators/
cup_and_handle.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)]
29pub struct CupAndHandle {
30 swing: SwingTracker,
31 has_emitted: bool,
32}
33
34impl CupAndHandle {
35 pub const fn new() -> Self {
37 Self {
38 swing: SwingTracker::new(SWING_THRESHOLD, 4),
39 has_emitted: false,
40 }
41 }
42}
43
44impl Default for CupAndHandle {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl Indicator for CupAndHandle {
51 type Input = Candle;
52 type Output = f64;
53
54 #[inline]
55 fn update(&mut self, candle: Candle) -> Option<f64> {
56 let advanced = self.swing.update(candle);
57 let pivots = self.swing.pivots();
58 if pivots.len() < 4 {
61 return None;
62 }
63 self.has_emitted = true;
64 if !advanced {
67 return Some(0.0);
68 }
69 let n = pivots.len();
70 let rim_left = pivots[n - 4];
71 let extreme = pivots[n - 3];
72 let rim_right = pivots[n - 2];
73 let handle = pivots[n - 1];
74 let rims_match = approx_equal(rim_left.price, rim_right.price, LEVEL_TOLERANCE);
75
76 if handle.direction < 0.0 {
77 if rims_match && handle.price > extreme.price && handle.price < rim_right.price {
80 return Some(1.0);
81 }
82 } else if rims_match && handle.price < extreme.price && handle.price > rim_right.price {
83 return Some(-1.0);
85 }
86 Some(0.0)
87 }
88
89 fn reset(&mut self) {
90 self.swing.reset();
91 self.has_emitted = false;
92 }
93
94 #[inline]
95 fn warmup_period(&self) -> usize {
96 5
98 }
99
100 #[inline]
101 fn is_ready(&self) -> bool {
102 self.has_emitted
103 }
104
105 #[inline]
106 fn name(&self) -> &'static str {
107 "CupAndHandle"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::indicators::pattern_swing::candles_for_pivots;
115 use crate::traits::BatchExt;
116
117 fn run(pivots: &[f64]) -> Vec<f64> {
118 let mut indicator = CupAndHandle::new();
119 candles_for_pivots(pivots)
120 .into_iter()
121 .filter_map(|c| indicator.update(c))
122 .collect()
123 }
124
125 #[test]
126 fn accessors_and_metadata() {
127 let indicator = CupAndHandle::new();
128 assert_eq!(indicator.name(), "CupAndHandle");
129 assert_eq!(indicator.warmup_period(), 5);
130 assert!(!indicator.is_ready());
131 assert!(!CupAndHandle::default().is_ready());
132 }
133
134 #[test]
135 fn cup_and_handle_is_plus_one() {
136 let out = run(&[120.0, 90.0, 121.0, 110.0]);
138 assert_eq!(*out.last().unwrap(), 1.0);
139 }
140
141 #[test]
142 fn inverse_cup_and_handle_is_minus_one() {
143 let out = run(&[140.0, 100.0, 130.0, 101.0, 110.0]);
145 assert_eq!(*out.last().unwrap(), -1.0);
146 }
147
148 #[test]
149 fn deep_handle_is_not_cup_and_handle() {
150 let out = run(&[120.0, 90.0, 121.0, 85.0]);
152 assert_eq!(*out.last().unwrap(), 0.0);
153 }
154
155 #[test]
156 fn inverse_with_mismatched_rims_does_not_trigger() {
157 let out = run(&[140.0, 100.0, 130.0, 90.0, 110.0]);
160 assert_eq!(*out.last().unwrap(), 0.0);
161 }
162
163 #[test]
164 fn reset_clears_state() {
165 let mut indicator = CupAndHandle::new();
166 for c in candles_for_pivots(&[120.0, 90.0, 121.0]) {
167 let _ = indicator.update(c);
168 }
169 indicator.reset();
170 assert!(!indicator.is_ready());
171 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
172 assert_eq!(indicator.update(c), None);
173 }
174
175 #[test]
176 fn batch_equals_streaming() {
177 let candles = candles_for_pivots(&[120.0, 90.0, 121.0, 110.0]);
178 let mut a = CupAndHandle::new();
179 let mut b = CupAndHandle::new();
180 assert_eq!(
181 a.batch(&candles),
182 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
183 );
184 }
185}