Williams %R
The Williams %R, also known as the Williams Percent Range, is a momentum indicator that measures overbought and oversold levels. Developed by Larry Williams, this oscillator is similar to the Stochastic Oscillator but uses a different scale and calculation method.
Unlike most oscillators that range from 0 to 100, Williams %R ranges from 0 to -100, with readings between 0 and -20 considered overbought and readings between -80 and -100 considered oversold.
What It Measures
Williams %R measures where the current closing price stands in relation to the highest high over a given lookback period. The indicator reflects the level of the close relative to the highest high for the look-back period, helping traders identify potential reversal points.
When to Use
- Identify Overbought/Oversold Conditions: Values between 0 and -20 typically indicate that an asset is overbought, while values between -80 and -100 suggest it is oversold.
- Confirm Trend Reversals: Williams %R can help identify potential reversals when it diverges from price action or crosses key threshold levels.
- Generate Entry/Exit Signals: Traders often use the crossing of the -50 level or exits from overbought/oversold zones as trading signals.
Interpretation
- High values (e.g., -20 to 0): Overbought conditions, potential sell signal
- Low values (e.g., -100 to -80): Oversold conditions, potential buy signal
- -50 level: Often used as a centerline to confirm trend direction
Default Usage
use rust_ti::momentum_indicators::bulk::williams_percent_r;
pub fn main() {
// fetch the data in your preferred way
// let close = vec![...]; // closing prices
// let high = vec![...]; // high prices
// let low = vec![...]; // low prices
let williams_percent_r = williams_percent_r(&high, &low, &close, 10);
println!("{:?}", williams_percent_r);
}
import pytechnicalindicators as pti
# fetch the data in your preferred way
# close = [...] # closing prices
# high = [...] # high prices
# low = [...] # low prices
williams_percent_r = pti.momentum_indicators.bulk.williams_percent_r(high, low, close, period=10)
print(williams_percent_r)
// WASM import
import init, { momentum_bulk_williamsPercentR } from 'https://cdn.jsdelivr.net/npm/ti-engine@latest/dist/web/ti_engine.js';
await init();
// fetch the data in your preferred way
// const close = [...]; // closing prices
// const high = [...]; // high prices
// const low = [...]; // low prices
const williamsPercentRSeries = momentum_bulk_williamsPercentR(high, low, close, 10);
console.log(williamsPercentRSeries);
Optimization
The best way to determine what the best parameters for your indicator are is to build a simple optimization loop that tests all possible parameter combinations between a defined min and max value, and rate the output.
Below is an example of how to do this in Rust.
use rust_ti::momentum_indicators::bulk::{williams_percent_r};
use rust_ti::chart_trends::{peaks, valleys};
fn proximity_rating(fuzzed_location: &usize, price_location: &usize) -> f64 {
1.0 / (*fuzzed_location as f64 - *price_location as f64).abs()
}
pub fn main() {
// fetch the data in your preferred way
// let close = vec![...]; // closing prices
let indicator_loop = Instant::now();
// get buy and sell points, in an ideal world we would buy at the lowest point in the dip and sell at the highest point in the peak
// In the course of a 20-day period (1 month of trading days), we want to find the highest peak and lowest valley within 5 days of each other
let sell_points = peaks(&close, 20, 5).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();
let buy_points = valleys(&close, 20, 5).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();
// Define the ranges for optimization
let max_period = 20;
let min_period = 2;
let min_oversold = 50;
let max_oversold = 100;
let min_overbought = 0;
let max_overbought = 50;
let fuzz_parameter = 5; // Allowable distance from buy/sell points
// Store the best parameters found
let mut best_rating = 0.0;
let mut best_period = 0;
let mut best_oversold = 0;
let mut best_overbought = 0;
let mut best_indicators = vec![];
for oversold in min_oversold..=max_oversold {
let oversold = oversold as f64 * -1.0; // Williams %R is negative
for overbought in min_overbought..=max_overbought {
let overbought = overbought as f64 * -1.0; // Williams %R is negative
for period in min_period..=max_period {
let indicators = williams_percent_r(&high, &low, &close, period);
let mut rating = vec![];
let mut matched_sell = vec![];
let mut matched_buy = vec![];
for i in 0..indicators.len() {
let price_location = i + period;
if indicators[i] > overbought as f64 {
if sell_points.contains(&price_location) {
// If sell point == Williams %R, rate positively
rating.push(1.0);
matched_sell.push(price_location);
} else if buy_points.contains(&price_location) {
// If buy point == Williams %R, rate negatively
rating.push(-1.0);
} else {
let mut found_sell = false;
for fuzzed_location in (price_location - fuzz_parameter)..=(price_location + fuzz_parameter) {
// It's ok if we count multiple times for fuzzed locations as we reduce the rating
// based off of distance from the actual sell point which will impact the final rating
if sell_points.contains(&fuzzed_location) {
rating.push(proximity_rating(&fuzzed_location, &price_location));
matched_sell.push(fuzzed_location);
found_sell = true;
}
if buy_points.contains(&fuzzed_location) {
// Note the `-` here to penalize for selling instead of buying
if !matched_sell.contains(&fuzzed_location) {
rating.push(-proximity_rating(&fuzzed_location, &price_location));
}
}
}
if !found_sell {
rating.push(0.0);
}
}
} else if indicators[i] < oversold as f64 {
if buy_points.contains(&price_location) {
// If buy point == Williams %R, rate positively
rating.push(1.0);
matched_buy.push(price_location);
} else if sell_points.contains(&price_location) {
rating.push(-1.0);
} else {
let mut found_buy = false;
for fuzzed_location in (price_location - fuzz_parameter)..=(price_location + fuzz_parameter) {
// It's ok if we count multiple times for fuzzed locations as we reduce the rating
// based off of distance from the actual sell point which will impact the final rating
if buy_points.contains(&fuzzed_location) {
rating.push(proximity_rating(&fuzzed_location, &price_location));
matched_buy.push(fuzzed_location);
found_buy = true;
}
if sell_points.contains(&fuzzed_location) {
// Note the `-` here to penalize for buying instead of selling
if !matched_buy.contains(&fuzzed_location) {
rating.push(-proximity_rating(&fuzzed_location, &price_location));
}
}
}
if !found_buy {
rating.push(0.0);
}
}
}
}
// Look for any missed buy/sell points and penalize
for missed_sell in sell_points.iter() {
if !matched_sell.contains(missed_sell) {
rating.push(-1.0);
}
}
for missed_buy in buy_points.iter() {
if !matched_buy.contains(missed_buy) {
rating.push(-1.0);
}
}
let total_rating: f64 = rating.iter().sum::<f64>() / (rating.len() as f64);
if total_rating > best_rating {
best_rating = total_rating;
best_period = period;
// best_model = ma_type;
best_oversold = (oversold * -1.0) as usize;
best_overbought = (overbought * -1.0) as usize;
best_indicators = indicators.clone();
}
}
}
}
println!(
"Indicators optimization loop took {} ms to run",
indicator_loop.elapsed().as_millis()
);
println!("
Best Williams %R parameters found:");
println!("Period: {}", best_period);
println!("Oversold threshold: {}", best_oversold as f64 * -1.0);
println!("Overbought threshold: {}", best_overbought as f64 * -1.0);
println!("Rating: {}", best_rating);
println!("Best Williams %R values: {:?}", best_indicators);
}
Optimization Output
Below is an example output from the optimization code above run on a year of S&P data.
Period: 13
Oversold threshold: -58.0
Overbought threshold: -9.0
Rating: 0.3703933747412007
Best Williams %R values: [-36.785046728971935, -33.246105919003476, -88.27855320426245, ...]
Interactive Chart
To better illustrate how the indicator performs with different parameters, an interactive chart is provided below comparing default parameters (blue) with optimized parameters (green).
Analysis
The optimized Williams %R parameters generate more nuanced trading signals compared to the default settings. A trading simulation was conducted to evaluate the effectiveness of both parameter sets. Both strategies started with an initial capital of $1000 and invested 20% of the remaining capital on each trade.
Long positions were opened when Williams %R fell below the oversold level and closed when it rose above the overbought level. Short positions were opened when Williams %R rose above the overbought level and closed when it fell below the oversold level.
The results are shown in the tables below. The optimized Williams %R strategy yielded a profit of $4.03, with a $192.18 open position. This outperformed the default Williams %R strategy which resulted in a profit of $0.64 (with a $191.53 open position).
Optimized trading simulation
- SideLONG
- Shares0.0341
- Entry$5955.25
- Value$192.18
Default trading simulation
- SideLONG
- Shares0.034
- Entry$5955.25
- Value$191.53
Trading simulation code
For those you want to run their own simulation to compare results
use rust_ti::momentum_indicators::bulk::{williams_percent_r};
fn simulate_trading(best_indicator: &[f64], best_period: usize, close: &[f64], best_oversold: usize, best_overbought: usize) {
// --- TRADING SIMULATION CODE ---
println!("
--- Trading Simulation ---");
let best_oversold = best_oversold as f64 * -1.0; // Convert back to negative
let best_overbought = best_overbought as f64 * -1.0; // Convert back to negative
let initial_capital = 1000.0;
let mut capital = initial_capital;
let investment_pct = 0.20;
struct Position {
entry_price: f64,
shares: f64,
}
let mut open_long: Option<Position> = None;
let mut open_short: Option<Position> = None;
// Print table header
println!("{:<5} | {:<19} | {:<10} | {:<10} | {:<12} | {:<15} | {:<10}",
"Day", "Event", "WR", "Price", "Shares", "Capital", "P/L");
println!("{}", "-".repeat(95));
for i in 0..best_indicator.len() {
let price_index = i + best_period;
if price_index >= close.len() { break; }
let wr_val = best_indicator[i];
let current_price = close[price_index];
let day = price_index;
// --- Handle Long Position ---
if let Some(long_pos) = open_long.take() {
if wr_val > best_overbought as f64 {
let sale_value = long_pos.shares * current_price;
let profit = sale_value - (long_pos.shares * long_pos.entry_price);
capital += sale_value;
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
day, "Sell (Close Long)", wr_val, current_price, long_pos.shares, capital, profit);
} else {
open_long = Some(long_pos); // Put it back if not selling
}
} else if wr_val < best_oversold as f64 && open_short.is_none() { // Don't buy if short is open
let investment = capital * investment_pct;
let shares_bought = investment / current_price;
open_long = Some(Position { entry_price: current_price, shares: shares_bought });
capital -= investment;
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
day, "Buy (Open Long)", wr_val, current_price, shares_bought, capital, "-");
}
// --- Handle Short Position ---
if let Some(short_pos) = open_short.take() {
if wr_val < best_oversold as f64 {
let cost_to_cover = short_pos.shares * current_price;
let profit = (short_pos.shares * short_pos.entry_price) - cost_to_cover;
capital += profit; // Add profit to capital
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
day, "Cover (Close Short)", wr_val, current_price, short_pos.shares, capital, profit);
} else {
open_short = Some(short_pos); // Put it back if not covering
}
} else if wr_val > best_overbought as f64 && open_long.is_none() { // Don't short if long is open
let short_value = capital * investment_pct;
let shares_shorted = short_value / current_price;
open_short = Some(Position { entry_price: current_price, shares: shares_shorted });
// Capital doesn't change when opening a short, it's held as collateral
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
day, "Short (Open Short)", wr_val, current_price, shares_shorted, capital, "-");
}
}
println!("
--- Final Results ---");
if let Some(pos) = open_long {
println!("Simulation ended with an OPEN LONG position:");
println!(" - Shares: {:.4}", pos.shares);
println!(" - Entry Price: ${:.2}", pos.entry_price);
let last_price = close.last().unwrap_or(&0.0);
let current_value = pos.shares * last_price;
capital += current_value;
println!(" - Position value at last price (${:.2}): ${:.2}", last_price, current_value);
}
if let Some(pos) = open_short {
println!("Simulation ended with an OPEN SHORT position:");
println!(" - Shares: {:.4}", pos.shares);
println!(" - Entry Price: ${:.2}", pos.entry_price);
let last_price = close.last().unwrap_or(&0.0);
let cost_to_cover = pos.shares * last_price;
let pnl = (pos.shares * pos.entry_price) - cost_to_cover;
capital += pnl;
println!(" - Unrealized P/L at last price (${:.2}): ${:.2}", last_price, pnl);
}
let final_pnl = capital - initial_capital;
println!("
Initial Capital: ${:.2}", initial_capital);
println!("Final Capital: ${:.2}", capital);
println!("Total P/L: ${:.2}", final_pnl);
}
fn main() {
// Fetch data and perform optimization as shown in the optimization code above
simulate_trading(&best_williams_percent_r, best_period, &close, best_oversold, best_overbought);
println!("
Default Williams %R values for comparison:");
let default_wr = williams_percent_r(&high, &low, &close, 10);
println!("{:?}", default_wr);
simulate_trading(&default_wr, 10, &close, 80, 20);
}