Williams %R

Momentum indicator measuring overbought/oversold levels from -100 to 0 scale.

About the 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

Example Usage

use centaur_technical_indicators::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 centaur_technical_indicators as cti

# fetch the data in your preferred way
# close = [...]  # closing prices
# high = [...]   # high prices
# low = [...]    # low prices

williams_percent_r = cti.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/centaur-technical-indicators@latest/dist/web/centaur-technical-indicators.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

Even if the defaults have stood the test of time, tuning them to your specific asset and timeframe gives you signals that fit your market. Below shows you how to achieve this.

Optimization Code

use centaur_technical_indicators::momentum_indicators::bulk::{williams_percent_r};
use centaur_technical_indicators::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

Example output from running the optimization code above 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, ...]

Trading Simulation

In order to determine whether the optimized parameters beat the defaults, a trading simulation was run, below are the results.

Optimized Trading Simulation

Initial Investment
$1000.00
Final Capital
$1004.03
Total P&L
$4.03
Open Position
  • SideLONG
  • Shares0.0341
  • Entry$5955.25
  • Value$192.18

Default Trading Simulation

Initial Investment
$1000.00
Final Capital
$1000.64
Total P&L
$0.64
Open Position
  • SideLONG
  • Shares0.034
  • Entry$5955.25
  • Value$191.53

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).

Trading Simulation Code

For those who want to run their own simulation to compare results.

use centaur_technical_indicators::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);
}