Ichimoku Cloud


The Ichimoku Cloud (Ichimoku Kinko Hyo) is a versatile indicator that defines support and resistance, identifies trend direction, gauges momentum, and provides trading signals. It consists of five lines: Tenkan-sen (Conversion Line), Kijun-sen (Base Line), Senkou Span A (Leading Span A), Senkou Span B (Leading Span B), and Chikou Span (Lagging Span).

ICHIMOKU

=ICHIMOKU(data, tenkanPeriod, kijunPeriod, senkouBPeriod)

Example Usage

=ICHIMOKU(A2:F500, 9, 26, 52)

Parameters

Parameter Type Description Status
data
Range
The input range of columns containing the Date, Open, High, Low, Close, and Volume data.
Required
tenkanPeriod
Number
The period for the Tenkan-sen (Conversion Line). Default is 9.
Optional
kijunPeriod
Number
The period for the Kijun-sen (Base Line). Default is 26.
Optional
senkouBPeriod
Number
The period for the Senkou Span B (Leading Span B). Default is 52.
Optional

Returns

A multi-column array containing:

  1. Date
  2. Tenkan-sen: (Highest High + Lowest Low) / 2 for the past tenkanPeriod.
  3. Kijun-sen: (Highest High + Lowest Low) / 2 for the past kijunPeriod.
  4. Senkou Span A: (Tenkan-sen + Kijun-sen) / 2, plotted 26 periods ahead.
  5. Senkou Span B: (Highest High + Lowest Low) / 2 for the past senkouBPeriod, plotted 26 periods ahead.
  6. Chikou Span: Closing price plotted 26 periods behind.
Ichimoku Cloud Formula Result in Google Sheets

Source Code

Copy the following code into your Apps Script editor (Extensions > Apps Script) to use the ICHIMOKU-CLOUD function in your spreadsheet.

ichimoku.js
/**
 * Calculates the Ichimoku Cloud.
 * A comprehensive indicator that defines support/resistance, trend, and momentum.
 *
 * @param {array} data - The input range. Must include at least 4 columns: Date, Open, High, Low.
 * @param {number} [tenkanPeriod=9] - Conversion Line period (default 9).
 * @param {number} [kijunPeriod=26] - Base Line period (default 26).
 * @param {number} [senkouBPeriod=52] - Leading Span B period (default 52).
 * @param {number} [offset=26] - Displacement (default 26).
 * @returns {array} A multi-column array with headers: Date, Tenkan-sen, Kijun-sen, Senkou Span A, Senkou Span B, Chikou Span.
 * @customfunction
 */
function ICHIMOKU(data, tenkanPeriod = 9, kijunPeriod = 26, senkouBPeriod = 52, offset = 26) {
    checkPremium();

    // Argument validation
    if (arguments.length > 5) {
        throw new Error(`Wrong number of arguments.`);
    }

    const processedData = getData(data);
    const columnCount = processedData[0].length;
    if (columnCount < 4) {
        throw new Error(`Invalid data structure. Expected at least 4 columns (Date, O, H, L).`);
    }

    const dataRows = processedData.slice(1);
    const results = [["Date", `Tenkan-sen (${tenkanPeriod})`, `Kijun-sen (${kijunPeriod})`, `Senkou Span A (${offset})`, `Senkou Span B (${senkouBPeriod}, ${offset})`, `Chikou Span (${offset})`]];

    // Buffers
    const highBuffer = [];
    const lowBuffer = [];

    // Displacement Queues
    // Senkou A/B are calculated today but plotted 'offset' periods ahead.
    // To align them to the CURRENT date (i.e. "What is the Cloud value for today?"),
    // We need the value that was calculated 'offset' periods ago.
    const spanAQueue = [];
    const spanBQueue = [];
    for (let k = 0; k < offset; k++) {
        spanAQueue.push(null);
        spanBQueue.push(null);
    }

    const maxPeriod = Math.max(tenkanPeriod, kijunPeriod, senkouBPeriod);

    for (let i = 0; i < dataRows.length; i++) {
        const row = dataRows[i];
        const date = row[0];
        const high = row[2];
        const low = row[3];
        const close = row[4];

        highBuffer.push(high);
        lowBuffer.push(low);

        if (highBuffer.length > maxPeriod) {
            highBuffer.shift();
            lowBuffer.shift();
        }

        // Helper for Max/Min
        const getMaxMin = (period) => {
            if (highBuffer.length < period) return null;
            let maxH = -Infinity;
            let minL = Infinity;
            // Scan the last 'period' entries of the buffer
            // Buffer size is at most maxPeriod.
            // We need indices [length - period] to [length - 1]
            const startIdx = highBuffer.length - period;
            for (let j = startIdx; j < highBuffer.length; j++) {
                if (highBuffer[j] > maxH) maxH = highBuffer[j];
                if (lowBuffer[j] < minL) minL = lowBuffer[j];
            }
            return (maxH + minL) / 2;
        };

        const tenkan = getMaxMin(tenkanPeriod);
        const kijun = getMaxMin(kijunPeriod);
        const senkouB_calc = getMaxMin(senkouBPeriod);

        let senkouA_calc = null;
        if (tenkan !== null && kijun !== null) {
            senkouA_calc = (tenkan + kijun) / 2;
        }

        // Queue Logic
        const currentSpanA = spanAQueue.shift();
        const currentSpanB = spanBQueue.shift();

        spanAQueue.push(senkouA_calc);
        spanBQueue.push(senkouB_calc);

        // Chikou: Strictly, Chikou at Date[i] is Close[i] plotted at i-26.
        // But in a data table, Chikou[i] usually holds Close[i].

        results.push([
            date,
            tenkan === null ? "" : tenkan,
            kijun === null ? "" : kijun,
            currentSpanA === null || currentSpanA === undefined ? "" : currentSpanA,
            currentSpanB === null || currentSpanB === undefined ? "" : currentSpanB,
            close
        ]);
    }

    // Trim Output
    // Align to the slowest component dynamically.
    // We iterate to find the first row where ALL critical indicators are valid.
    // This ensures no "ragged" starts, regardless of which period (Tenkan, Kijun, or Span B) is longest.
    let firstValidIndex = -1;
    for (let i = 1; i < results.length; i++) {
        const row = results[i];
        // Check Tenkan[1], Kijun[2], SpanA[3], SpanB[4]
        if (row[1] !== "" && row[1] !== null &&
            row[2] !== "" && row[2] !== null &&
            row[3] !== "" && row[3] !== null &&
            row[4] !== "" && row[4] !== null) {
            firstValidIndex = i;
            break;
        }
    }

    if (firstValidIndex !== -1) {
        return [results[0], ...results.slice(firstValidIndex)];
    } else {
        return [results[0]];
    }
}