Stochastic RSI (STOCH-RSI)

Stochastic RSI (STOCH-RSI)

Function Syntax

=STOCH_RSI(data, period, smoothing)  
  • data (array):
    Range of columns containing the date, Open, high, Low, close, volume data.
  • period (number):
    The number of periods (days) used to calculate the RSI.
  • smoothing(number): The number of periods used to smooth the %K values into the %D signal line.

Returns:
A three-column array of dates and their corresponding %K and %D values.

Google Sheets Output Example

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

stoch-rsi return array
STOCH-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):

atr.gs
/**

 * Calculates the Stochastic RSI for a given dataset and period.

 *

 * @param {array} data - A range containing date and close prices.

 * @param {number} period - The number of periods for calculating the Stochastic RSI.

 * @param {number} smoothing - The number of periods for the SMA

 * @returns {array} - A 2D array with headers: Date, %K, %D.

 * @customfunction

 */

  

function STOCH_RSI(data, period,smoothing) {

  // Preprocess data using getData

  // Check if the period is greater than the length of the data

  if (period > data.length) {

    throw new Error("Error: The specified period exceeds the length of the data.");

  }

  

  // Calculate RSI values

  const rsiValues = RSI(data, period).slice(1); // Remove headers

  

  // Extract RSI values for %K calculation

  const dates = rsiValues.map(row => row[0])

  const rsiOnly = rsiValues.map(row => row[1]);

  

  // Calculate %K for each period using RSI values

  const stochRSIValues = [];

  for (let i = period - 1; i < rsiOnly.length; i++) {

    const periodHigh = Math.max(...rsiOnly.slice(i - period + 1, i + 1));

    const periodLow = Math.min(...rsiOnly.slice(i - period + 1, i + 1));

    const currentRSI = rsiOnly[i];

  

    const rsiValue = ((currentRSI - periodLow) / (periodHigh - periodLow)) * 100;

    stochRSIValues.push([dates[i], rsiValue]); // Adjust date alignment

  }

  

  // Calculate %D (3-period SMA of %K)

  const kValues = SMA(stochRSIValues, smoothing).slice(1); // Calculate %D directly from %K and remove header

  const dValues = SMA(kValues,smoothing).slice(1);

  

  // Align %K and %D lengths

  const [alignedK, alignedD] = alignLENGTHS(kValues, dValues);

  

  // Combine %K and %D values into final output

  const stochRSI = alignedK.map((k, i) => {

    return [k[0], k[1], alignedD[i] ? alignedD[i][1] : null];

  });

  

  // Prepare the final data structure

  return [["Date", "%K", "%D"], ...stochRSI];

}

  

/**

 * 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];

}

/**

 * 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;

}

  

/**

 * Calculates the Simple Moving Average (SMA) for a given dataset and period.

 *

 * @param {Array} data A range containing date and close price data from Googlefinance(), with no header row.

 * @param {number} period The number of periods to calculate the SMA, e.g., 14 for a 14-day SMA.

 * @returns {Array<Array>} The Simple Moving Average (SMA) of the range.

 * @customfunction

 */

  

function SMA(data, period) {

  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

  

  // Check if the period is greater than the length of the data

  if (period > closePrices.length) {

    throw new Error("Error: The specified period exceeds the length of the data.");

  }

  

  // Initialize an array to store SMA values with corresponding dates

  let results = [["Date", `SMA(${period})`]]; // Include headers

  

  // Calculate SMA for the given period

  for (let i = period - 1; i < closePrices.length; i++) {

    const periodPrices = closePrices.slice(i - period + 1, i + 1); // Get the slice for the period

    const sma = periodPrices.reduce((acc, val) => acc + val, 0) / period; // Calculate average

  

    // Append the current date and SMA to the results array

    results.push([dates[i], sma]);

  }

  

  return results;

}

  

/**

 * Aligns the lengths of two arrays by trimming the longer array to match the shorter one.

 *

 * @param {Array} arr1 - The first array.

 * @param {Array} arr2 - The second array.

 * @returns {Array} - An array containing two aligned arrays.

 */

  

function alignLENGTHS(arr1, arr2) {

  const lengthDiff = arr1.length - arr2.length;

  if (lengthDiff > 0) {

    return [arr1.slice(lengthDiff), arr2];

  } else if (lengthDiff < 0) {

    return [arr1, arr2.slice(-lengthDiff)];

  }

  return [arr1, arr2];

}

  

/**

 *

 * @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];

  }))];

}
Last updated on