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

Interpretation

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

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

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);
}