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)]
36pub struct GoldenPocket {
37 swing: SwingTracker,
38}
39
40impl GoldenPocket {
41 #[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 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 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 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}