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