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}