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 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 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 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 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}