indexes_rs/v1/support_resistance/main.rs
1//! # Support & Resistance Module
2//!
3//! This module implements a support/resistance indicator based on swing highs and lows.
4//! It keeps a sliding window of prices (with a customizable period) and calculates support
5//! and resistance levels using a specified threshold (default is 2%).
6//!
7//! The module provides a `calculate` method that returns an `SRResult` containing:
8//! - The nearest support level
9//! - The nearest resistance level
10//! - A support strength (0-100%)
11//! - A resistance strength (0-100%)
12//! - A breakout potential (based on the weaker of the two strengths)
13//! - A price position (relative to the support/resistance levels)
14//!
15//! # Example
16//!
17//! ```rust
18//! use indexes_rs::v1::support_resistance::main::SupportResistance;
19//! use indexes_rs::v1::support_resistance::types::{SRResult, PricePosition};
20//!
21//! let mut sr = SupportResistance::new(SupportResistance::DEFAULT_PERIOD, SupportResistance::DEFAULT_THRESHOLD);
22//!
23//! // Simulate adding prices (in a real scenario, these would be updated on every tick)
24//! let prices = vec![100.0, 101.0, 102.0, 101.5, 100.5, 99.0, 98.5, 99.5, 100.0, 101.0, 102.0, 103.0];
25//! let mut result: Option<SRResult> = None;
26//! for price in prices {
27//! result = sr.calculate(price);
28//! }
29//!
30//! if let Some(res) = result {
31//! println!("Nearest support: {:?}", res.nearest_support);
32//! println!("Nearest resistance: {:?}", res.nearest_resistance);
33//! println!("Support strength: {:.2}%", res.support_strength);
34//! println!("Resistance strength: {:.2}%", res.resistance_strength);
35//! println!("Breakout potential: {:.2}%", res.breakout_potential);
36//! println!("Price position: {:?}", res.price_position);
37//! }
38//! ```
39
40use super::types::*; // This module should define SRResult and PricePosition
41
42/// A Support/Resistance indicator based on a sliding window of prices and swing detection.
43pub struct SupportResistance {
44 period: usize,
45 prices: Vec<f64>,
46 swing_high_threshold: f64,
47 swing_low_threshold: f64,
48 support_levels: Vec<f64>,
49 resistance_levels: Vec<f64>,
50}
51
52impl SupportResistance {
53 /// Default period (number of prices to consider) for swing detection.
54 pub const DEFAULT_PERIOD: usize = 20;
55 /// Default threshold (2%): used to validate or clean up support/resistance levels.
56 pub const DEFAULT_THRESHOLD: f64 = 0.02;
57
58 /// Creates a new `SupportResistance` indicator.
59 ///
60 /// # Arguments
61 ///
62 /// * `period` - The number of prices to include in the sliding window.
63 /// * `threshold` - The percentage threshold (as a decimal, e.g. 0.02 for 2%)
64 /// to determine swing levels.
65 pub fn new(period: usize, threshold: f64) -> Self {
66 SupportResistance {
67 period,
68 prices: Vec::new(),
69 swing_high_threshold: 1.0 + threshold,
70 swing_low_threshold: 1.0 - threshold,
71 support_levels: Vec::new(),
72 resistance_levels: Vec::new(),
73 }
74 }
75
76 /// Updates the indicator with a new price and returns the current support/resistance result.
77 ///
78 /// This method pushes the new price into the internal price window, updates the
79 /// detected support and resistance levels, and returns an `SRResult` if there are
80 /// enough prices.
81 ///
82 /// # Arguments
83 ///
84 /// * `price` - The new price to add.
85 ///
86 /// # Returns
87 ///
88 /// * `Some(SRResult)` if there are enough prices for calculation.
89 /// * `None` if there aren't enough prices yet.
90 pub fn calculate(&mut self, price: f64) -> Option<SRResult> {
91 self.prices.push(price);
92 // Keep the sliding window limited to at most period*2 values.
93 if self.prices.len() > self.period * 2 {
94 self.prices.remove(0);
95 }
96
97 // Ensure we have enough data.
98 if self.prices.len() < self.period {
99 return None;
100 }
101
102 self.update_levels();
103
104 Some(SRResult {
105 nearest_support: self.find_nearest_support(price),
106 nearest_resistance: self.find_nearest_resistance(price),
107 support_strength: self.calculate_support_strength(price),
108 resistance_strength: self.calculate_resistance_strength(price),
109 breakout_potential: self.calculate_breakout_potential(price),
110 price_position: self.determine_price_position(price),
111 })
112 }
113
114 /// Updates support and resistance levels based on the latest price window.
115 ///
116 /// This method checks if the current window shows a swing high or swing low,
117 /// updates the respective levels, and cleans out old or invalidated levels.
118 fn update_levels(&mut self) {
119 if let Some(window) = self.prices.get(self.prices.len().saturating_sub(self.period)..) {
120 let mid_index = window.len() / 2;
121 if self.is_swing_high(window) {
122 // Record the swing high (the mid value in the window).
123 let swing_high = window[mid_index];
124 self.resistance_levels.push(swing_high);
125 }
126 if self.is_swing_low(window) {
127 // Record the swing low (the mid value in the window).
128 let swing_low = window[mid_index];
129 self.support_levels.push(swing_low);
130 }
131 }
132 // Use the latest price for cleaning.
133 let current_price = *self.prices.last().unwrap_or(&0.0);
134 self.clean_levels(current_price);
135 }
136
137 /// Determines if the given window is a swing high.
138 ///
139 /// A swing high is defined as the middle value being higher than all other values in the window.
140 fn is_swing_high(&self, window: &[f64]) -> bool {
141 if window.len() < 3 {
142 return false;
143 }
144 let mid = window.len() / 2;
145 let mid_price = window[mid];
146 window[..mid].iter().all(|&p| p < mid_price) && window[mid + 1..].iter().all(|&p| p < mid_price)
147 }
148
149 /// Determines if the given window is a swing low.
150 ///
151 /// A swing low is defined as the middle value being lower than all other values in the window.
152 fn is_swing_low(&self, window: &[f64]) -> bool {
153 if window.len() < 3 {
154 return false;
155 }
156 let mid = window.len() / 2;
157 let mid_price = window[mid];
158 window[..mid].iter().all(|&p| p > mid_price) && window[mid + 1..].iter().all(|&p| p > mid_price)
159 }
160
161 /// Cleans out old or invalidated support and resistance levels.
162 ///
163 /// Levels that are too far from the current price (based on the swing thresholds)
164 /// are removed.
165 fn clean_levels(&mut self, current_price: f64) {
166 self.support_levels.retain(|&level| level < current_price * self.swing_high_threshold);
167 self.resistance_levels.retain(|&level| level > current_price * self.swing_low_threshold);
168 }
169
170 /// Finds the nearest support level below the given price.
171 fn find_nearest_support(&self, price: f64) -> Option<f64> {
172 self.support_levels.iter().filter(|&&s| s < price).max_by(|a, b| a.partial_cmp(b).unwrap()).copied()
173 }
174
175 /// Finds the nearest resistance level above the given price.
176 fn find_nearest_resistance(&self, price: f64) -> Option<f64> {
177 self.resistance_levels.iter().filter(|&&r| r > price).min_by(|a, b| a.partial_cmp(b).unwrap()).copied()
178 }
179
180 /// Calculates the strength of the support level as a percentage (0-100).
181 ///
182 /// Strength is determined by the relative distance between the current price and the support.
183 fn calculate_support_strength(&self, price: f64) -> f64 {
184 if let Some(support) = self.find_nearest_support(price) {
185 let distance = (price - support).abs() / price;
186 (1.0 - distance).clamp(0.0, 1.0) * 100.0
187 } else {
188 0.0
189 }
190 }
191
192 /// Calculates the strength of the resistance level as a percentage (0-100).
193 ///
194 /// Strength is determined by the relative distance between the resistance and the current price.
195 fn calculate_resistance_strength(&self, price: f64) -> f64 {
196 if let Some(resistance) = self.find_nearest_resistance(price) {
197 let distance = (resistance - price).abs() / price;
198 (1.0 - distance).clamp(0.0, 1.0) * 100.0
199 } else {
200 0.0
201 }
202 }
203
204 /// Calculates the breakout potential based on the weaker of the support or resistance strengths.
205 fn calculate_breakout_potential(&self, price: f64) -> f64 {
206 let support_strength = self.calculate_support_strength(price);
207 let resistance_strength = self.calculate_resistance_strength(price);
208 if support_strength > resistance_strength {
209 resistance_strength
210 } else {
211 support_strength
212 }
213 }
214
215 /// Determines the price position relative to the nearest support and resistance levels.
216 ///
217 /// Returns a `PricePosition` value indicating whether the price is in the middle, near support,
218 /// near resistance, or outside the known levels.
219 fn determine_price_position(&self, price: f64) -> PricePosition {
220 match (self.find_nearest_support(price), self.find_nearest_resistance(price)) {
221 (Some(s), Some(r)) => {
222 let mid_point = (s + r) / 2.0;
223 if (price - mid_point).abs() < (r - s) * 0.1 {
224 PricePosition::Middle
225 } else if price > mid_point {
226 PricePosition::NearResistance
227 } else {
228 PricePosition::NearSupport
229 }
230 }
231 (Some(_), None) => PricePosition::AboveResistance,
232 (None, Some(_)) => PricePosition::BelowSupport,
233 (None, None) => PricePosition::Unknown,
234 }
235 }
236}