Relative Strength Index (RSI)

Relative Strength Index (RSI)

Function Syntax

=RSI(data, period)  
  • data (array):
    Range of columns containing the date, Open, high, Low, close, volume data.
  • period (number):
    Number of periods (days) over which the RSI is calculated

Returns:
A two-column array of dates and their corresponding RSI values.

Google Sheets Output Example

Below is an example of the resulting array when applying the custom RSI() function in Google Sheets

rsi return array
RSI Return Array

Spreadsheet

Apps Scripts Code

Here’s the full Google Apps Script code.
Paste this directly into the Google Sheets Apps Script editor (Extensions → Apps Script):

rsi.gs
/**
 * Calculates the Relative Strength Index (RSI) for a given dataset and period.
 * @param {array} data - an array where the first column is the dates, and subsequent columns contain open, high, low, close, and volume.
 * @param {number} period The number of periods to calculate the RSI, e.g., 14 for a 14-day RSI.
 * @return {array} The RSI values along with corresponding dates.
 * @customfunction
 */

function RSI(data, period) {
  // Preprocess the range using getData
  const processedData = getData(data);

  // Check number of arguments
  if (arguments.length !== RSI.length) {
    throw new Error(`Wrong number of arguments. Expected ${RSI.length} arguments, but got ${arguments.length} arguments.`);
  }

  // Extract columns from the processed data
  const dates = processedData.slice(1).map(row => row[0]); // Dates are already Date objects
  const closePrices = processedData.slice(1).map(row => row[4]); // Close prices are already floats

  if (period > closePrices.length) {
    throw "Error: The specified period exceeds the length of the range.";
  }

  // Initialize arrays to store gains and losses with dates
  let gains = [["Date", "Gain"]];
  let losses = [["Date", "Loss"]];

  // Calculate price changes and store them with corresponding dates
  for (let i = 1; i < closePrices.length; i++) {
    let priceChange = closePrices[i] - closePrices[i - 1];
    let date = dates[i]; // Use the date corresponding to the current close price

    if (priceChange > 0) {
      gains.push([date, priceChange]);
      losses.push([date, 0]); // Placeholder for losses
    } else {
      gains.push([date, 0]); // Placeholder for gains
      losses.push([date, Math.abs(priceChange)]);
    }
  }

  // Calculate average gain and average loss over the specified period using RMA
  let avgGains = RMA(gains, period).slice(1); // Remove headers
  let avgLosses = RMA(losses, period).slice(1); // Remove headers


  // Calculate RSI values and add corresponding date
  let RSIs = avgGains.map((gainRow, index) => {
    let date = gainRow[0]; // Use the date from the gains array
    let avgGain = gainRow[1];
    let avgLoss = avgLosses[index][1];

    let RS = avgLoss === 0 ? 100 : avgGain / avgLoss; // Prevent division by zero
    let RSI = 100 - (100 / (1 + RS));

    return [date, RSI];
  });

  // Prepare the final RSI data structure
  return [["Date", `RSI(${period})`], ...RSIs];
}
 
/**
 *
 * @param {Array<Array>} data - A 2D array of cell values to process. The first row should contain headers.
 * @returns {Array<Array>} - A 2D array where the first row contains headers and subsequent rows contain processed data.
 *
 * @customfunction
 * 
 */

function getData(data) {
  if (!data || data.length === 0) {
    throw new Error("Input data is empty or invalid.");
  }

  // Check if the first row contains headers
  const headers = Array.isArray(data[0]) && data[0].every(item => typeof item === "string")
    ? data[0]
    : null;

  if (!headers) {
    // If no headers, assume the first column is the date column and process data
    return data.map(row => row.map((value, index) => {
      return index === 0 ? new Date(value) : parseFloat(value);
    }));
  }

  // Process rows with headers
  return [headers, ...data.slice(1).map(row => headers.map((header, index) => {
    const key = header.toLowerCase();
    return key === "date" ? new Date(row[index]) : parseFloat(row[index]) || row[index];
  }))];
}

/**
 * Calculates the Relavtive Moving Average (RMA) for a given dataset and period.
 *
 * @param {array} data - an array where the first column is the  dates, and subsequent columns for  open, high, low, close and volume.  
 * @param {number} period The number of periods to calculate the RMA, e.g., 14 for a 14-day RMA.
 * @returns {Array} the RMA values along with corresponding dates.
 * @customfunction
 */

function RMA(data, period) {
  //Check number of arguments
  if (arguments.length !== RMA.length) {
    throw new Error(`Wrong number of arguments. Expected ${RMA.length} arguments, but got ${arguments.length} arguments.`);
  }

  const processedData = getData(data);
  const dates = processedData.slice(1).map(row => row[0]); // Extract dates
  const closePrices = processedData[0].length === 2
    ? processedData.slice(1).map(row => row[1]) // For 2-column arrays, use the second column
    : processedData.slice(1).map(row => row[4]); // Default: use index 4 for close price

  // Initialize an array to store EMA values with corresponding dates
  let results = [["Date", `RMA(${period})`]]; // Include headers

  // Calculate the multiplier for RMA calculation
  const multiplier = 1 / period;

  // Calculate SMA for the first n data points
  let sum = 0;
  for (let i = 0; i < period; i++) {
    sum += closePrices[i];
  }
  let sma = sum / period;

  // The first RMA 
  results.push([dates[period - 1], sma]); 


  // Calculate RMA for the remaining data points
  for (let i = period; i < closePrices.length; i++) {
    const currentClose = closePrices[i];
    const prevRMA = results[results.length - 1][1];
    const currentRMA = (currentClose - prevRMA) * multiplier + prevRMA;

    // Append the current date and RMA to the results array
    results.push([dates[i], currentRMA]);
  }

  return results;
}
Last updated on