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 fn update(&mut self, candle: Candle) -> Option<f64> {
55 self.has_emitted = true;
56 if !self.swing.update(candle) {
57 return Some(0.0);
58 }
59 let pivots = self.swing.pivots();
60 if pivots.len() < 4 {
61 return Some(0.0);
62 }
63 let n = pivots.len();
64 let rim_left = pivots[n - 4];
65 let extreme = pivots[n - 3];
66 let rim_right = pivots[n - 2];
67 let handle = pivots[n - 1];
68 let rims_match = approx_equal(rim_left.price, rim_right.price, LEVEL_TOLERANCE);
69
70 if handle.direction < 0.0 {
71 if rims_match && handle.price > extreme.price && handle.price < rim_right.price {
74 return Some(1.0);
75 }
76 } else if rims_match && handle.price < extreme.price && handle.price > rim_right.price {
77 return Some(-1.0);
79 }
80 Some(0.0)
81 }
82
83 fn reset(&mut self) {
84 self.swing.reset();
85 self.has_emitted = false;
86 }
87
88 fn warmup_period(&self) -> usize {
89 5
91 }
92
93 fn is_ready(&self) -> bool {
94 self.has_emitted
95 }
96
97 fn name(&self) -> &'static str {
98 "CupAndHandle"
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::indicators::pattern_swing::candles_for_pivots;
106 use crate::traits::BatchExt;
107
108 fn run(pivots: &[f64]) -> Vec<f64> {
109 let mut indicator = CupAndHandle::new();
110 candles_for_pivots(pivots)
111 .into_iter()
112 .map(|c| indicator.update(c).unwrap())
113 .collect()
114 }
115
116 #[test]
117 fn accessors_and_metadata() {
118 let indicator = CupAndHandle::new();
119 assert_eq!(indicator.name(), "CupAndHandle");
120 assert_eq!(indicator.warmup_period(), 5);
121 assert!(!indicator.is_ready());
122 assert!(!CupAndHandle::default().is_ready());
123 }
124
125 #[test]
126 fn cup_and_handle_is_plus_one() {
127 let out = run(&[120.0, 90.0, 121.0, 110.0]);
129 assert_eq!(*out.last().unwrap(), 1.0);
130 }
131
132 #[test]
133 fn inverse_cup_and_handle_is_minus_one() {
134 let out = run(&[140.0, 100.0, 130.0, 101.0, 110.0]);
136 assert_eq!(*out.last().unwrap(), -1.0);
137 }
138
139 #[test]
140 fn deep_handle_is_not_cup_and_handle() {
141 let out = run(&[120.0, 90.0, 121.0, 85.0]);
143 assert_eq!(*out.last().unwrap(), 0.0);
144 }
145
146 #[test]
147 fn inverse_with_mismatched_rims_does_not_trigger() {
148 let out = run(&[140.0, 100.0, 130.0, 90.0, 110.0]);
151 assert_eq!(*out.last().unwrap(), 0.0);
152 }
153
154 #[test]
155 fn reset_clears_state() {
156 let mut indicator = CupAndHandle::new();
157 for c in candles_for_pivots(&[120.0, 90.0, 121.0]) {
158 let _ = indicator.update(c);
159 }
160 indicator.reset();
161 assert!(!indicator.is_ready());
162 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
163 assert_eq!(indicator.update(c), Some(0.0));
164 }
165
166 #[test]
167 fn batch_equals_streaming() {
168 let candles = candles_for_pivots(&[120.0, 90.0, 121.0, 110.0]);
169 let mut a = CupAndHandle::new();
170 let mut b = CupAndHandle::new();
171 assert_eq!(
172 a.batch(&candles),
173 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
174 );
175 }
176}