Relative Strength Index (RSI)

The Relative Strength Index (RSI) is a momentum oscillator that measures the speed and change of price movements. It is widely used in technical analysis to identify overbought or oversold conditions in a market.

The RSI is most typically used on a 14-day timeframe, measured on a scale from 0 to 100, with high and low levels marked at 70 and 30, respectively. Short or longer timeframes are used for alternately shorter or longer outlooks. High and low levels—80 and 20, or 90 and 10—occur less frequently but indicate stronger momentum. The relative strength index was developed by J. Welles Wilder and published in a 1978 book, New Concepts in Technical Trading

What It Measures

The RSI measures the magnitude of recent price changes to evaluate overbought or oversold conditions in the price of a stock or other asset.

When to Use

Interpretation

Default Usage

use rust_ti::momentum_indicators::bulk::relative_strength_index;
use rust_ti::ConstantModelType::SmoothedMovingAverage;

pub fn main() {
    // fetch the data in your preferred way
    // let close = vec![...];  // closing prices
    
    let rsi = relative_strength_index(&close, SmoothedMovingAverage, 14);
    println!("{:?}", rsi);
}
import pytechnicalindicators as pti

rsi = pti.momentum_indicators.bulk.relative_strength_index(close, constant_model_type="simple_moving_average", period=14)
import init, { momentum_bulk_relativeStrengthIndex, ConstantModelType } 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 rsi = momentum_bulk_relativeStrengthIndex(prices, ConstantModelType.SmoothedMovingAverage, 14);
console.log(rsi);

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::relative_strength_index;
use rust_ti::chart_trends::{peaks, valleys};
use rust_ti::ConstantModelType;

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 prices = vec![...];

    // 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(&prices, 20, 5).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();
    let buy_points = valleys(&prices, 20, 5).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();

    // Define the ranges for optimization
    let max_period = 20;
    let min_period = 0;
    let min_oversold = 0;
    let max_oversold = 50;
    let min_overbought = 50;
    let max_overbought = 100;

    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_model = ConstantModelType::SimpleMovingAverage;
    let mut best_oversold = 0;
    let mut best_overbought = 0;
    let mut best_rsis = vec![];

    for oversold in min_oversold..=max_oversold {
        for overbought in min_overbought..=max_overbought {
            for &ma_type in &[ConstantModelType::SimpleMovingAverage, ConstantModelType::ExponentialMovingAverage, ConstantModelType::SmoothedMovingAverage] {
                for period in min_period..=max_period {
                    let indicators = relative_strength_index(&close, ma_type, 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 == rsi, rate positively
                                rating.push(1.0);
                                matched_sell.push(price_location);
                            } else if buy_points.contains(&price_location) {
                                // If buy point == rsi, 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 == rsi, 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;
                            best_overbought = overbought;
                            best_rsis = indicators.clone();
                        }

                }
            }
        }
    }

    println!("Best RSI parameters found:");
    println!("Period: {}", best_period);
    println!("Model: {:?}", best_model);
    println!("Oversold threshold: {}", best_oversold);
    println!("Overbought threshold: {}", best_overbought);
    println!("Rating: {}", best_rating);
    println!("Best RSI values: {:?}", best_rsis);

}

Optimization Output

Below is an example output from the optimization code above run on a year of S&P data.

Best RSI parameters found:
Period: 6
Model: ExponentialMovingAverage
Oversold threshold: 33
Overbought threshold: 72
Rating: 0.3061068702290076
Best RSI values: [46.05572331735997, 79.50202806065019, 66.12192049272042, ...]

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 RSI has considerably more buy/sell signals compared to the default RSI parameters. In order to determine whether these additional signals are beneficial, a trading simulation was conducted using both the optimized and default RSI values.

They both started with an intial capital of $1000 and invested 20% of the remaining capital on each trade. Each trade was executed when the RSI crossed the overbought or oversold thresholds, with long positions opened when the RSI dipped below the oversold level and closed when it rose above the overbought level, and short positions opened when the RSI rose above the overbought level and closed when it fell below the oversold level.

The results have been put into the tables below. The optimized RSI strategy yielded a profit of $25.42, wita a $196.28 open position. This outperformed the default RSI strategy which resulted in a loss of $40.01 (with a $180.81 open position).

Optimized trading simulation

Initial Investment
$1000.00
Final Capital
$1025.42
Total P&L
$25.42
Open Position
  • SideLONG
  • Shares0.0348
  • Entry$5955.25
  • Value$196.28

Default trading simulation

Initial Investment
$1000.00
Final Capital
$959.99
Total P&L
$-40.01
Open Position
  • SideLONG
  • Shares0.0321
  • Entry$6075.11
  • Value$180.81

Trading simulation code

For those you want to run their own simulation to compare results

fn simulate_trading(best_indicator: &[f64], best_period: usize, close: &[f64], best_oversold: usize, best_overbought: usize) {
    // --- NEW TRADING SIMULATION CODE ---

    println!("
--- Trading Simulation ---");

    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", "RSI", "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 rsi_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 rsi_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)", rsi_val, current_price, long_pos.shares, capital, profit);
            } else {
                open_long = Some(long_pos); // Put it back if not selling
            }
        } else if rsi_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)", rsi_val, current_price, shares_bought, capital, "-");
        }

        // --- Handle Short Position ---
        if let Some(short_pos) = open_short.take() {
            if rsi_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)", rsi_val, current_price, short_pos.shares, capital, profit);
            } else {
                open_short = Some(short_pos); // Put it back if not covering
            }
        } else if rsi_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)", rsi_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_rsis, best_period, &close, best_oversold, best_overbought);
    
    // Compare with default parameters
    let default_rsis = relative_strength_index(&close, ConstantModelType::SmoothedMovingAverage, 14);
    simulate_trading(&default_rsis, 14, &close, 30, 70);
}