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#[derive(Debug, Clone)]
36pub struct GoldenPocket {
37    swing: SwingTracker,
38}
39
40impl GoldenPocket {
41    /// Construct a new Golden Pocket tracker.
42    #[must_use]
43    pub const fn new() -> Self {
44        Self {
45            swing: SwingTracker::new(SWING_THRESHOLD, 2),
46        }
47    }
48
49    fn zone(&self) -> Option<GoldenPocketOutput> {
50        let pivots = self.swing.pivots();
51        let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
52        let span = start - end;
53        let edge_low = end + RATIO_LOW * span;
54        let edge_high = end + RATIO_HIGH * span;
55        let low = edge_low.min(edge_high);
56        let high = edge_low.max(edge_high);
57        Some(GoldenPocketOutput {
58            low,
59            mid: f64::midpoint(low, high),
60            high,
61        })
62    }
63}
64
65impl Default for GoldenPocket {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl Indicator for GoldenPocket {
72    type Input = Candle;
73    type Output = GoldenPocketOutput;
74
75    fn update(&mut self, candle: Candle) -> Option<GoldenPocketOutput> {
76        self.swing.update(candle);
77        self.zone()
78    }
79
80    fn reset(&mut self) {
81        self.swing.reset();
82    }
83
84    fn warmup_period(&self) -> usize {
85        2
86    }
87
88    fn is_ready(&self) -> bool {
89        self.swing.pivots().len() >= 2
90    }
91
92    fn name(&self) -> &'static str {
93        "GoldenPocket"
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::indicators::pattern_swing::candles_for_pivots;
101    use crate::traits::BatchExt;
102    use approx::assert_relative_eq;
103
104    #[test]
105    fn accessors_and_metadata() {
106        let indicator = GoldenPocket::new();
107        assert_eq!(indicator.name(), "GoldenPocket");
108        assert_eq!(indicator.warmup_period(), 2);
109        assert!(!indicator.is_ready());
110        assert!(!GoldenPocket::default().is_ready());
111    }
112
113    #[test]
114    fn no_output_before_two_pivots() {
115        let mut indicator = GoldenPocket::new();
116        let outputs: Vec<_> = candles_for_pivots(&[120.0])
117            .into_iter()
118            .map(|c| indicator.update(c))
119            .collect();
120        assert!(outputs.iter().all(Option::is_none));
121    }
122
123    #[test]
124    fn zone_of_a_down_leg() {
125        // Leg 200 (high) -> 100 (low), span = 100.
126        let mut indicator = GoldenPocket::new();
127        let mut last = None;
128        for candle in candles_for_pivots(&[200.0, 100.0]) {
129            last = indicator.update(candle);
130        }
131        let v = last.unwrap();
132        assert!(indicator.is_ready());
133        // 61.8% = 161.8, 65% = 165 → sorted band [161.8, 165], mid 163.4.
134        assert_relative_eq!(v.low, 161.8);
135        assert_relative_eq!(v.high, 165.0);
136        assert_relative_eq!(v.mid, 163.4);
137    }
138
139    #[test]
140    fn band_is_sorted_for_an_up_leg() {
141        // Latest leg 100 (low) -> 250 (high): span negative, edges flip, but
142        // low <= high must still hold.
143        let mut indicator = GoldenPocket::new();
144        let mut last = None;
145        for candle in candles_for_pivots(&[200.0, 100.0, 250.0]) {
146            last = indicator.update(candle);
147        }
148        let v = last.unwrap();
149        assert!(v.low <= v.high);
150        assert_relative_eq!(v.mid, f64::midpoint(v.low, v.high));
151    }
152
153    #[test]
154    fn reset_clears_state() {
155        let mut indicator = GoldenPocket::new();
156        for candle in candles_for_pivots(&[200.0, 100.0]) {
157            let _ = indicator.update(candle);
158        }
159        indicator.reset();
160        assert!(!indicator.is_ready());
161        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
162        assert!(indicator.update(c).is_none());
163    }
164
165    #[test]
166    fn batch_equals_streaming() {
167        let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
168        let mut a = GoldenPocket::new();
169        let mut b = GoldenPocket::new();
170        assert_eq!(
171            a.batch(&candles),
172            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
173        );
174    }
175}