wickra_core/indicators/
golden_pocket.rs1use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7const RATIO_LOW: f64 = 0.618;
9const RATIO_HIGH: f64 = 0.65;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct GoldenPocketOutput {
18 pub low: f64,
20 pub mid: f64,
22 pub high: f64,
24}
25
26#[derive(Debug, Clone)]
51pub struct GoldenPocket {
52 swing: SwingTracker,
53}
54
55impl GoldenPocket {
56 #[must_use]
58 pub const fn new() -> Self {
59 Self {
60 swing: SwingTracker::new(SWING_THRESHOLD, 2),
61 }
62 }
63
64 fn zone(&self) -> Option<GoldenPocketOutput> {
65 let pivots = self.swing.pivots();
66 let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
67 let span = start - end;
68 let edge_low = end + RATIO_LOW * span;
69 let edge_high = end + RATIO_HIGH * span;
70 let low = edge_low.min(edge_high);
71 let high = edge_low.max(edge_high);
72 Some(GoldenPocketOutput {
73 low,
74 mid: f64::midpoint(low, high),
75 high,
76 })
77 }
78}
79
80impl Default for GoldenPocket {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl Indicator for GoldenPocket {
87 type Input = Candle;
88 type Output = GoldenPocketOutput;
89
90 #[inline]
91 fn update(&mut self, candle: Candle) -> Option<GoldenPocketOutput> {
92 self.swing.update(candle);
93 self.zone()
94 }
95
96 fn reset(&mut self) {
97 self.swing.reset();
98 }
99
100 #[inline]
101 fn warmup_period(&self) -> usize {
102 2
103 }
104
105 #[inline]
106 fn is_ready(&self) -> bool {
107 self.swing.pivots().len() >= 2
108 }
109
110 #[inline]
111 fn name(&self) -> &'static str {
112 "GoldenPocket"
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::indicators::pattern_swing::candles_for_pivots;
120 use crate::traits::BatchExt;
121 use approx::assert_relative_eq;
122
123 #[test]
124 fn accessors_and_metadata() {
125 let indicator = GoldenPocket::new();
126 assert_eq!(indicator.name(), "GoldenPocket");
127 assert_eq!(indicator.warmup_period(), 2);
128 assert!(!indicator.is_ready());
129 assert!(!GoldenPocket::default().is_ready());
130 }
131
132 #[test]
133 fn no_output_before_two_pivots() {
134 let mut indicator = GoldenPocket::new();
135 let outputs: Vec<_> = candles_for_pivots(&[120.0])
136 .into_iter()
137 .map(|c| indicator.update(c))
138 .collect();
139 assert!(outputs.iter().all(Option::is_none));
140 }
141
142 #[test]
143 fn zone_of_a_down_leg() {
144 let mut indicator = GoldenPocket::new();
146 let mut last = None;
147 for candle in candles_for_pivots(&[200.0, 100.0]) {
148 last = indicator.update(candle);
149 }
150 let v = last.unwrap();
151 assert!(indicator.is_ready());
152 assert_relative_eq!(v.low, 161.8);
154 assert_relative_eq!(v.high, 165.0);
155 assert_relative_eq!(v.mid, 163.4);
156 }
157
158 #[test]
159 fn band_is_sorted_for_an_up_leg() {
160 let mut indicator = GoldenPocket::new();
163 let mut last = None;
164 for candle in candles_for_pivots(&[200.0, 100.0, 250.0]) {
165 last = indicator.update(candle);
166 }
167 let v = last.unwrap();
168 assert!(v.low <= v.high);
169 assert_relative_eq!(v.mid, f64::midpoint(v.low, v.high));
170 }
171
172 #[test]
173 fn reset_clears_state() {
174 let mut indicator = GoldenPocket::new();
175 for candle in candles_for_pivots(&[200.0, 100.0]) {
176 let _ = indicator.update(candle);
177 }
178 indicator.reset();
179 assert!(!indicator.is_ready());
180 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
181 assert!(indicator.update(c).is_none());
182 }
183
184 #[test]
185 fn batch_equals_streaming() {
186 let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
187 let mut a = GoldenPocket::new();
188 let mut b = GoldenPocket::new();
189 assert_eq!(
190 a.batch(&candles),
191 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
192 );
193 }
194}