wickra_core/indicators/
rectangle_range.rs1use crate::indicators::pattern_swing::{
4 approx_equal, recent_legs, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
5};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
26pub struct RectangleRange {
27 swing: SwingTracker,
28 has_emitted: bool,
29}
30
31impl RectangleRange {
32 pub const fn new() -> Self {
34 Self {
35 swing: SwingTracker::new(SWING_THRESHOLD, 4),
36 has_emitted: false,
37 }
38 }
39}
40
41impl Default for RectangleRange {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl Indicator for RectangleRange {
48 type Input = Candle;
49 type Output = f64;
50
51 #[inline]
52 fn update(&mut self, candle: Candle) -> Option<f64> {
53 let advanced = self.swing.update(candle);
54 let pivots = self.swing.pivots();
55 if pivots.len() < 4 {
58 return None;
59 }
60 self.has_emitted = true;
61 if !advanced {
64 return Some(0.0);
65 }
66 let (high_old, high_new, low_old, low_new) = recent_legs(pivots);
67 let flat_highs = approx_equal(high_old, high_new, LEVEL_TOLERANCE);
68 let flat_lows = approx_equal(low_old, low_new, LEVEL_TOLERANCE);
69 if flat_highs && flat_lows {
70 let last_is_high = pivots[pivots.len() - 1].direction > 0.0;
71 return Some(if last_is_high { -1.0 } else { 1.0 });
72 }
73 Some(0.0)
74 }
75
76 fn reset(&mut self) {
77 self.swing.reset();
78 self.has_emitted = false;
79 }
80
81 #[inline]
82 fn warmup_period(&self) -> usize {
83 5
85 }
86
87 #[inline]
88 fn is_ready(&self) -> bool {
89 self.has_emitted
90 }
91
92 #[inline]
93 fn name(&self) -> &'static str {
94 "RectangleRange"
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::indicators::pattern_swing::candles_for_pivots;
102 use crate::traits::BatchExt;
103
104 fn run(pivots: &[f64]) -> Vec<f64> {
105 let mut indicator = RectangleRange::new();
106 candles_for_pivots(pivots)
107 .into_iter()
108 .filter_map(|c| indicator.update(c))
109 .collect()
110 }
111
112 #[test]
113 fn accessors_and_metadata() {
114 let indicator = RectangleRange::new();
115 assert_eq!(indicator.name(), "RectangleRange");
116 assert_eq!(indicator.warmup_period(), 5);
117 assert!(!indicator.is_ready());
118 assert!(!RectangleRange::default().is_ready());
119 }
120
121 #[test]
122 fn range_bounce_off_support_is_plus_one() {
123 let out = run(&[120.0, 100.0, 121.0, 99.0]);
125 assert_eq!(*out.last().unwrap(), 1.0);
126 }
127
128 #[test]
129 fn range_rejection_at_resistance_is_minus_one() {
130 let out = run(&[130.0, 100.0, 120.0, 99.0, 121.0]);
132 assert_eq!(*out.last().unwrap(), -1.0);
133 }
134
135 #[test]
136 fn trending_highs_are_not_a_rectangle() {
137 let out = run(&[120.0, 100.0, 140.0, 99.0]);
139 assert_eq!(*out.last().unwrap(), 0.0);
140 }
141
142 #[test]
143 fn reset_clears_state() {
144 let mut indicator = RectangleRange::new();
145 for c in candles_for_pivots(&[120.0, 100.0, 121.0]) {
146 let _ = indicator.update(c);
147 }
148 indicator.reset();
149 assert!(!indicator.is_ready());
150 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
151 assert_eq!(indicator.update(c), None);
152 }
153
154 #[test]
155 fn batch_equals_streaming() {
156 let candles = candles_for_pivots(&[120.0, 100.0, 121.0, 99.0]);
157 let mut a = RectangleRange::new();
158 let mut b = RectangleRange::new();
159 assert_eq!(
160 a.batch(&candles),
161 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
162 );
163 }
164}