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    fn update(&mut self, candle: Candle) -> Option<GoldenPocketOutput> {
91        self.swing.update(candle);
92        self.zone()
93    }
94
95    fn reset(&mut self) {
96        self.swing.reset();
97    }
98
99    fn warmup_period(&self) -> usize {
100        2
101    }
102
103    fn is_ready(&self) -> bool {
104        self.swing.pivots().len() >= 2
105    }
106
107    fn name(&self) -> &'static str {
108        "GoldenPocket"
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::indicators::pattern_swing::candles_for_pivots;
116    use crate::traits::BatchExt;
117    use approx::assert_relative_eq;
118
119    #[test]
120    fn accessors_and_metadata() {
121        let indicator = GoldenPocket::new();
122        assert_eq!(indicator.name(), "GoldenPocket");
123        assert_eq!(indicator.warmup_period(), 2);
124        assert!(!indicator.is_ready());
125        assert!(!GoldenPocket::default().is_ready());
126    }
127
128    #[test]
129    fn no_output_before_two_pivots() {
130        let mut indicator = GoldenPocket::new();
131        let outputs: Vec<_> = candles_for_pivots(&[120.0])
132            .into_iter()
133            .map(|c| indicator.update(c))
134            .collect();
135        assert!(outputs.iter().all(Option::is_none));
136    }
137
138    #[test]
139    fn zone_of_a_down_leg() {
140        // Leg 200 (high) -> 100 (low), span = 100.
141        let mut indicator = GoldenPocket::new();
142        let mut last = None;
143        for candle in candles_for_pivots(&[200.0, 100.0]) {
144            last = indicator.update(candle);
145        }
146        let v = last.unwrap();
147        assert!(indicator.is_ready());
148        // 61.8% = 161.8, 65% = 165 → sorted band [161.8, 165], mid 163.4.
149        assert_relative_eq!(v.low, 161.8);
150        assert_relative_eq!(v.high, 165.0);
151        assert_relative_eq!(v.mid, 163.4);
152    }
153
154    #[test]
155    fn band_is_sorted_for_an_up_leg() {
156        // Latest leg 100 (low) -> 250 (high): span negative, edges flip, but
157        // low <= high must still hold.
158        let mut indicator = GoldenPocket::new();
159        let mut last = None;
160        for candle in candles_for_pivots(&[200.0, 100.0, 250.0]) {
161            last = indicator.update(candle);
162        }
163        let v = last.unwrap();
164        assert!(v.low <= v.high);
165        assert_relative_eq!(v.mid, f64::midpoint(v.low, v.high));
166    }
167
168    #[test]
169    fn reset_clears_state() {
170        let mut indicator = GoldenPocket::new();
171        for candle in candles_for_pivots(&[200.0, 100.0]) {
172            let _ = indicator.update(candle);
173        }
174        indicator.reset();
175        assert!(!indicator.is_ready());
176        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
177        assert!(indicator.update(c).is_none());
178    }
179
180    #[test]
181    fn batch_equals_streaming() {
182        let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
183        let mut a = GoldenPocket::new();
184        let mut b = GoldenPocket::new();
185        assert_eq!(
186            a.batch(&candles),
187            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
188        );
189    }
190}