Skip to main content

wickra_core/indicators/
golden_pocket.rs

1//! Golden Pocket — the 0.618-0.65 optimal-trade-entry zone of the last swing.
2
3use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7/// Lower bound of the golden pocket (the 61.8% retracement).
8const RATIO_LOW: f64 = 0.618;
9/// Upper bound of the golden pocket (the 65% retracement).
10const RATIO_HIGH: f64 = 0.65;
11
12/// The golden-pocket zone of the most recent swing leg.
13///
14/// `low`/`high` bracket the 0.618-0.65 retracement band (sorted, so `low <=
15/// high` regardless of swing direction); `mid` is their midpoint.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct GoldenPocketOutput {
18    /// Lower price of the golden-pocket band.
19    pub low: f64,
20    /// Midpoint of the band.
21    pub mid: f64,
22    /// Upper price of the golden-pocket band.
23    pub high: f64,
24}
25
26/// Golden Pocket (`GoldenPocket`).
27///
28/// The 0.618-0.65 retracement band of the most recent confirmed swing leg — the
29/// "optimal trade entry" zone many swing traders watch for continuation.
30///
31/// Parameter-free; construction is infallible. Returns `None` until the first
32/// leg is complete.
33///
34/// See `crates/wickra-core/src/indicators/golden_pocket.rs`.
35/// # Example
36///
37/// ```
38/// use wickra_core::{GoldenPocket, Candle, Indicator};
39///
40/// let mut indicator = GoldenPocket::new();
41/// // `None` during warmup, then `Some(_)` once enough bars are seen.
42/// let mut out = None;
43/// for i in 0..40i64 {
44///     let p = 100.0 + (i as f64 * 0.4).sin() * 5.0;
45///     let candle = Candle::new(p, p + 1.5, p - 1.5, p + 0.3, 1_000.0, i).unwrap();
46///     out = indicator.update(candle);
47/// }
48/// let _ = out;
49/// ```
50#[derive(Debug, Clone)]
51pub struct GoldenPocket {
52    swing: SwingTracker,
53}
54
55impl GoldenPocket {
56    /// Construct a new Golden Pocket tracker.
57    #[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        // Leg 200 (high) -> 100 (low), span = 100.
145        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        // 61.8% = 161.8, 65% = 165 → sorted band [161.8, 165], mid 163.4.
153        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        // Latest leg 100 (low) -> 250 (high): span negative, edges flip, but
161        // low <= high must still hold.
162        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}