indexes_rs/v2/williams_r/
main.rs

1use crate::v2::williams_r::types::{WilliamsRConfig, WilliamsRError, WilliamsRInput, WilliamsRMarketCondition, WilliamsROutput, WilliamsRState};
2
3/// Williams %R Indicator
4///
5/// Williams %R is a momentum oscillator that measures overbought and oversold levels.
6/// It's similar to the Stochastic Oscillator but with an inverted scale (0 to -100).
7///
8/// Formula:
9/// Williams %R = (Highest High - Close) / (Highest High - Lowest Low) × -100
10///
11/// Where:
12/// - Highest High = Highest high over the lookback period
13/// - Lowest Low = Lowest low over the lookback period
14/// - Close = Current closing price
15///
16/// Interpretation:
17/// - Values range from 0 to -100
18/// - Above -20: Overbought condition
19/// - Below -80: Oversold condition
20/// - Above -10: Extremely overbought
21/// - Below -90: Extremely oversold
22///
23/// Williams %R is particularly useful for:
24/// - Identifying extreme price conditions
25/// - Timing entry/exit points
26/// - Confirming trend reversals
27pub struct WilliamsR {
28    state: WilliamsRState,
29}
30
31impl WilliamsR {
32    /// Create a new Williams %R calculator with default configuration (period=14)
33    pub fn new() -> Self {
34        // Use default config directly to avoid validation issues during testing
35        Self {
36            state: WilliamsRState::new(WilliamsRConfig::default()),
37        }
38    }
39
40    /// Create a new Williams %R calculator with custom period
41    pub fn with_period(period: usize) -> Result<Self, WilliamsRError> {
42        if period == 0 {
43            return Err(WilliamsRError::InvalidPeriod);
44        }
45
46        let config = WilliamsRConfig { period, ..Default::default() };
47        Ok(Self::with_config(config))
48    }
49
50    /// Create a new Williams %R calculator with custom period and thresholds
51    pub fn with_thresholds(period: usize, overbought: f64, oversold: f64, extreme_overbought: f64, extreme_oversold: f64) -> Result<Self, WilliamsRError> {
52        if period == 0 {
53            return Err(WilliamsRError::InvalidPeriod);
54        }
55
56        // Williams %R thresholds should be negative and in descending order (towards more negative)
57        // Scale: 0 (most overbought) to -100 (most oversold)
58        // Valid order: extreme_overbought > overbought > oversold > extreme_oversold
59        // Example: -10 > -20 > -80 > -90
60        if overbought >= 0.0
61            || oversold >= overbought          // Want: -80 < -20 → -80 >= -20 is false ✅
62            || extreme_overbought >= 0.0
63            || extreme_overbought <= overbought // Want: -10 > -20 → -10 <= -20 is false ✅
64            || extreme_oversold >= oversold
65        {
66            // Want: -90 < -80 → -90 >= -80 is false ✅
67            return Err(WilliamsRError::InvalidThresholds);
68        }
69
70        let config = WilliamsRConfig {
71            period,
72            overbought,
73            oversold,
74            extreme_overbought,
75            extreme_oversold,
76        };
77        Ok(Self::with_config(config))
78    }
79
80    /// Create a new Williams %R calculator with custom configuration
81    pub fn with_config(config: WilliamsRConfig) -> Self {
82        Self {
83            state: WilliamsRState::new(config),
84        }
85    }
86
87    /// Calculate Williams %R for the given input
88    pub fn calculate(&mut self, input: WilliamsRInput) -> Result<WilliamsROutput, WilliamsRError> {
89        // Validate input
90        self.validate_input(&input)?;
91        self.validate_config()?;
92
93        // Update price history
94        self.update_price_history(input.high, input.low);
95
96        // Calculate Williams %R if we have enough data
97        let williams_r = if self.state.has_sufficient_data {
98            self.calculate_williams_r_value(input.close)?
99        } else {
100            -50.0 // Default middle value when insufficient data
101        };
102
103        // Determine market condition
104        let market_condition = self.determine_market_condition(williams_r);
105
106        // Calculate distances from key levels
107        let distance_from_overbought = williams_r - self.state.config.overbought;
108        let distance_from_oversold = williams_r - self.state.config.oversold;
109
110        // Calculate price range
111        let price_range = self.state.highest_high - self.state.lowest_low;
112
113        Ok(WilliamsROutput {
114            williams_r,
115            highest_high: self.state.highest_high,
116            lowest_low: self.state.lowest_low,
117            close: input.close,
118            price_range,
119            market_condition,
120            distance_from_overbought,
121            distance_from_oversold,
122        })
123    }
124
125    /// Calculate Williams %R for a batch of inputs
126    pub fn calculate_batch(&mut self, inputs: &[WilliamsRInput]) -> Result<Vec<WilliamsROutput>, WilliamsRError> {
127        inputs.iter().map(|input| self.calculate(*input)).collect()
128    }
129
130    /// Reset the calculator state
131    pub fn reset(&mut self) {
132        self.state = WilliamsRState::new(self.state.config);
133    }
134
135    /// Get current state (for serialization/debugging)
136    pub fn get_state(&self) -> &WilliamsRState {
137        &self.state
138    }
139
140    /// Restore state (for deserialization)
141    pub fn set_state(&mut self, state: WilliamsRState) {
142        self.state = state;
143    }
144
145    /// Check if currently overbought
146    pub fn is_overbought(&self, williams_r: f64) -> bool {
147        williams_r >= self.state.config.overbought
148    }
149
150    /// Check if currently oversold
151    pub fn is_oversold(&self, williams_r: f64) -> bool {
152        williams_r <= self.state.config.oversold
153    }
154
155    /// Check if in extreme condition
156    pub fn is_extreme_condition(&self, williams_r: f64) -> bool {
157        williams_r >= self.state.config.extreme_overbought || williams_r <= self.state.config.extreme_oversold
158    }
159
160    /// Get signal strength (0.0 to 1.0, where 1.0 is strongest)
161    pub fn signal_strength(&self, williams_r: f64) -> f64 {
162        // Convert Williams %R to signal strength
163        // More extreme values (closer to 0 or -100) have higher strength
164        let distance_from_center = (williams_r + 50.0).abs(); // Distance from -50 (center)
165        (distance_from_center / 50.0).min(1.0)
166    }
167
168    // Private helper methods
169
170    fn validate_input(&self, input: &WilliamsRInput) -> Result<(), WilliamsRError> {
171        // Check for valid prices
172        if !input.high.is_finite() || !input.low.is_finite() || !input.close.is_finite() {
173            return Err(WilliamsRError::InvalidPrice);
174        }
175
176        // Check HLC relationship
177        if input.high < input.low {
178            return Err(WilliamsRError::InvalidHLC);
179        }
180
181        if input.close < input.low || input.close > input.high {
182            return Err(WilliamsRError::InvalidHLC);
183        }
184
185        Ok(())
186    }
187
188    fn validate_config(&self) -> Result<(), WilliamsRError> {
189        if self.state.config.period == 0 {
190            return Err(WilliamsRError::InvalidPeriod);
191        }
192
193        let config = &self.state.config;
194
195        // Williams %R thresholds validation:
196        // Scale: 0 (most overbought) to -100 (most oversold)
197        // We need: extreme_overbought > overbought > oversold > extreme_oversold
198        if config.overbought >= 0.0
199            || config.oversold >= config.overbought
200            || config.extreme_overbought >= 0.0
201            || config.extreme_overbought <= config.overbought
202            || config.extreme_oversold >= config.oversold
203        {
204            return Err(WilliamsRError::InvalidThresholds);
205        }
206
207        Ok(())
208    }
209
210    fn update_price_history(&mut self, high: f64, low: f64) {
211        // Remove oldest prices if at capacity
212        if self.state.highs.len() >= self.state.config.period {
213            self.state.highs.pop_front();
214            self.state.lows.pop_front();
215        }
216
217        // Add new prices
218        self.state.highs.push_back(high);
219        self.state.lows.push_back(low);
220
221        // Update highest/lowest values
222        self.update_extremes();
223
224        // Check if we have sufficient data
225        self.state.has_sufficient_data = self.state.highs.len() >= self.state.config.period;
226    }
227
228    fn update_extremes(&mut self) {
229        if self.state.highs.is_empty() {
230            return;
231        }
232
233        // Find highest high and lowest low in the current period
234        self.state.highest_high = self.state.highs.iter().fold(f64::NEG_INFINITY, |acc, &x| acc.max(x));
235        self.state.lowest_low = self.state.lows.iter().fold(f64::INFINITY, |acc, &x| acc.min(x));
236    }
237
238    fn calculate_williams_r_value(&self, close: f64) -> Result<f64, WilliamsRError> {
239        if !self.state.has_sufficient_data {
240            return Ok(-50.0); // Default middle value
241        }
242
243        let price_range = self.state.highest_high - self.state.lowest_low;
244
245        if price_range == 0.0 {
246            // All prices are the same - return middle value
247            return Ok(-50.0);
248        }
249
250        // Williams %R formula: (Highest High - Close) / (Highest High - Lowest Low) × -100
251        let williams_r = ((self.state.highest_high - close) / price_range) * -100.0;
252
253        if !williams_r.is_finite() {
254            return Err(WilliamsRError::DivisionByZero);
255        }
256
257        // Clamp to valid range (0 to -100)
258        Ok(williams_r.clamp(-100.0, 0.0))
259    }
260
261    fn determine_market_condition(&self, williams_r: f64) -> WilliamsRMarketCondition {
262        if !self.state.has_sufficient_data {
263            WilliamsRMarketCondition::Insufficient
264        } else if williams_r >= self.state.config.extreme_overbought {
265            WilliamsRMarketCondition::ExtremeOverbought
266        } else if williams_r >= self.state.config.overbought {
267            WilliamsRMarketCondition::Overbought
268        } else if williams_r <= self.state.config.extreme_oversold {
269            WilliamsRMarketCondition::ExtremeOversold
270        } else if williams_r <= self.state.config.oversold {
271            WilliamsRMarketCondition::Oversold
272        } else {
273            WilliamsRMarketCondition::Normal
274        }
275    }
276}
277
278impl Default for WilliamsR {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284/// Convenience function to calculate Williams %R for HLC data without maintaining state
285pub fn calculate_williams_r_simple(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Result<Vec<f64>, WilliamsRError> {
286    let len = highs.len();
287    if len != lows.len() || len != closes.len() {
288        return Err(WilliamsRError::InvalidInput("All price arrays must have same length".to_string()));
289    }
290
291    if len == 0 {
292        return Ok(Vec::new());
293    }
294
295    let mut williams_r_calculator = WilliamsR::with_period(period)?;
296    let mut results = Vec::with_capacity(len);
297
298    for i in 0..len {
299        let input = WilliamsRInput {
300            high: highs[i],
301            low: lows[i],
302            close: closes[i],
303        };
304        let output = williams_r_calculator.calculate(input)?;
305        results.push(output.williams_r);
306    }
307
308    Ok(results)
309}