Scriptone Scriptone

[Python Stock Analysis] Calculating and Visualizing Ichimoku Cloud with Polars

blog-image

Overview

In the previous article, we explained how to understand price volatility ranges using Bollinger Bands. This time, we’ll cover the Ichimoku Cloud, a technical indicator developed in Japan, including how to calculate it with Polars and visualize it with Plotly.

The Ichimoku Cloud consists of five components: the Conversion Line, Base Line, Leading Span A & B, and Lagging Span. It’s a powerful indicator that allows you to analyze multiple timeframes simultaneously.

Motivation

The Ichimoku Cloud (一目均衡表, Ichimoku Kinko Hyo) is a technical indicator developed by Goichi Hosoda in Japan in 1936. It’s also known internationally as “Ichimoku Cloud” and is used by traders outside Japan. The main features of the Ichimoku Cloud are:

  • Simultaneous multi-timeframe analysis: Can grasp short-term (9 days), medium-term (26 days), and long-term (52 days) trends at once
  • Visual support and resistance through the cloud: The “cloud” formed between Leading Span A and B functions as future support or resistance lines
  • Understanding trend direction and strength: Can determine trend strength and turning points from the relationship between price and cloud, crosses of Conversion and Base lines, etc.
  • Visually intuitive: The cloud is clearly displayed on the chart, making trend direction easy to see at a glance

As in previous articles, we’ll use modern Python libraries such as Polars, Plotly, marimo, and yfinance-pl for analysis.

Libraries Used

The libraries are as follows:

LibraryRelated LibraryDescription
polarspandasA library for handling dataframes at high speed
numpy-Numerical computing library, used for array manipulation and differential calculation (np.diff()) to detect cloud bullish/bearish crossover points
plotlymatplotlibVisualization library that allows users to interactively manipulate graphs
marimojupyterA notebook that runs in py format and allows interactive code execution
yfinance-plyfinanceA Python library wrapping yfinance-rs that can handle Yahoo Finance information with Polars

Notebook and Code

The HTML version of the marimo notebook is available here. We also link to the Python code on GitHub.

Explanation

What is Ichimoku Cloud

The Ichimoku Cloud is a technical indicator composed of the following five components:

  1. Conversion Line (Tenkan-sen): Average of the highest high and lowest low over the past 9 days
  2. Base Line (Kijun-sen): Average of the highest high and lowest low over the past 26 days
  3. Leading Span A (Senkou Span A): (Conversion Line + Base Line) / 2, shifted 26 days into the future
  4. Leading Span B (Senkou Span B): Average of the highest high and lowest low over the past 52 days, shifted 26 days into the future
  5. Lagging Span (Chikou Span): Today’s closing price, shifted 26 days into the past

The area between Leading Span A and Leading Span B is called the “cloud” (Kumo), which functions as support or resistance lines.

Future Cloud: Since the leading spans are shifted 26 days into the future, the last 26 days of data correspond to future dates. By displaying this future portion, you can anticipate future support and resistance. This is one of the greatest features of the Ichimoku Cloud.

How to Calculate Ichimoku Cloud

We’ll calculate each component of the Ichimoku Cloud using Polars. Use rolling_max() and rolling_min() to get the highest/lowest values within a period, and shift() to move the time axis.

import polars as pl
from typing import TypedDict


class IchimokuValues(TypedDict):
    conversion_line: pl.Series
    base_line: pl.Series
    leading_span1: pl.Series
    leading_span2: pl.Series
    lagging_span: pl.Series


def get_ichimoku_values(df: pl.DataFrame) -> IchimokuValues:
    # Decimal type doesn't support rolling operations, so convert to Float64 first
    high = df["high.amount"].cast(pl.Float64)
    low = df["low.amount"].cast(pl.Float64)
    close = df["close.amount"].cast(pl.Float64)

    # Conversion Line: (Max + Min) / 2 over past 9 days
    conversion_line = (high.rolling_max(9) + low.rolling_min(9)) / 2

    # Base Line: (Max + Min) / 2 over past 26 days
    base_line = (high.rolling_max(26) + low.rolling_min(26)) / 2

    # Leading Span A: (Conversion Line + Base Line) / 2, shifted 26 days into future
    # Polars shift fills empty spaces with null by default
    leading_span1 = ((conversion_line + base_line) / 2).shift(26)

    # Leading Span B: (Max + Min) / 2 over past 52 days, shifted 26 days into future
    leading_span2 = ((high.rolling_max(52) + low.rolling_min(52)) / 2).shift(26)

    # Lagging Span: Today's closing price, shifted 26 days into past
    lagging_span = close.shift(-26)

    return {
        "conversion_line": conversion_line,
        "base_line": base_line,
        "leading_span1": leading_span1,
        "leading_span2": leading_span2,
        "lagging_span": lagging_span,
    }

There are a few points to note:

  • Decimal Type Conversion: Data retrieved with yfinance-pl is in Decimal type, but Polars’ rolling_max() and rolling_min() don’t support Decimal type, so convert with cast(pl.Float64)
  • shift() Direction: shift() moves forward in time with positive values and backward with negative values. Leading spans use shift(26) to move 26 days forward, while lagging span uses shift(-26) to move 26 days backward
  • Period Meaning: The periods of 9 days, 26 days, and 52 days are the standard settings for Ichimoku Cloud. These are based on Japanese market business days

Basic Reading of Ichimoku Cloud

The Ichimoku Cloud is a visually intuitive indicator, but there are some basic ways to read it.

The Cloud (Between Leading Span A and B)

The cloud functions as future support or resistance lines.

  • Price above the cloud: Uptrend. Cloud likely functions as support
  • Price within the cloud: Range-bound market. Direction undetermined
  • Price below the cloud: Downtrend. Cloud likely functions as resistance

The thickness of the cloud also indicates trend strength. Thicker clouds mean stronger support/resistance.

Cloud Bullish and Bearish

The color of the cloud changes based on the relationship between Leading Span A and Leading Span B.

  • Bullish (bullish cloud): Leading Span A is above Leading Span B. Suggests uptrend
  • Bearish (bearish cloud): Leading Span A is below Leading Span B. Suggests downtrend

The timing when the cloud changes from bearish to bullish, or vice versa, is an important signal of trend reversal.

Conversion and Base Line Cross

When the Conversion Line (short-term) crosses above the Base Line (medium-term), it’s a buy signal; when it crosses below, it’s a sell signal. This is similar to the golden cross/death cross concept of moving averages.

Lagging Span

The Lagging Span is an indicator for comparing the current price with the price 26 days ago. When the lagging span is above past prices, it’s considered bullish; when below, bearish.

Using the Future Cloud

The future cloud shows future support and resistance because the leading spans are shifted 26 days forward.

  • Thick future cloud: Strong support/resistance expected. Difficult to break through
  • Thin future cloud: Weak support/resistance. Price may break through easily
  • Future cloud twist: Point where bearish⇔bullish switches. Suggests potential trend reversal

The future cloud is a prediction based on current data and changes according to actual price movements. It’s important to regularly update and check the chart.

Visualization with Plotly

We’ll use Plotly to display candlesticks and the five components of Ichimoku Cloud. In particular, we’ll fill the area between Leading Span A and B to visualize the “cloud” and change colors based on bullish/bearish conditions. We’ll also display the future cloud to visualize future support and resistance.

Function to Generate Future Dates

Since the leading spans of Ichimoku Cloud are shifted 26 days into the future, we need future dates to display 26 days of future cloud. We’ll generate future dates on a business day basis using Python’s datetime.

from datetime import datetime, timedelta

def generate_future_business_days(last_date: str, n_days: int = 26) -> list[str]:
    """
    Generate future business days (weekdays) from the last date

    Args:
        last_date: Last data date (string format: "YYYY-MM-DD")
        n_days: Number of business days to generate (default 26 days)

    Returns:
        List of future business days (string format: "YYYY-MM-DD")
    """
    # Convert last date to date object
    current = datetime.strptime(last_date, "%Y-%m-%d")
    future_dates = []

    # Advance one day at a time until required business days are collected
    while len(future_dates) < n_days:
        current += timedelta(days=1)
        # Add only business days (Mon-Fri) (weekday(): Mon=0, Fri=4)
        if current.weekday() < 5:
            future_dates.append(current.strftime("%Y-%m-%d"))

    return future_dates

Key points of this function:

  • Python’s datetime: Convert string to date with datetime.strptime(), advance one day at a time with timedelta(days=1)
  • Business day filtering: Get weekday with weekday() (Mon=0, Tue=1, …, Fri=4, Sat=5, Sun=6) and add only values less than 5 (Mon-Fri)
  • Loop until required days: Skip Sat/Sun and loop until 26 business days are collected

Function to Color-code Cloud by Bullish/Bearish

First, define a function that divides the cloud into segments and color-codes them.

import numpy as np
import plotly.graph_objects as go

def create_cloud_segments(dates, span1, span2):
    """
    Divide Ichimoku Cloud into segments color-coded by bullish/bearish
    """
    # Convert Polars Series to NumPy array
    if hasattr(span1, "to_numpy"):
        span1 = span1.to_numpy()
    if hasattr(span2, "to_numpy"):
        span2 = span2.to_numpy()

    # Bullish judgment (Leading Span A > Leading Span B)
    is_bullish = np.nan_to_num(span1 > span2, nan=False)

    # Detect crossover points (where bearish⇔bullish switches)
    crossovers = np.where(np.diff(is_bullish.astype(int)) != 0)[0] + 1

    # Create segment boundaries
    boundaries = np.concatenate([[0], crossovers, [len(dates)]])

    traces = []

    # Process each segment
    for i in range(len(boundaries) - 1):
        start_idx = boundaries[i]
        end_idx = boundaries[i + 1]

        segment_dates = dates[start_idx:end_idx]
        segment_span1 = span1[start_idx:end_idx]
        segment_span2 = span2[start_idx:end_idx]

        # Skip segments with only NaN
        if np.all(np.isnan(segment_span1)) or np.all(np.isnan(segment_span2)):
            continue

        # Determine bullish/bearish of segment and set color
        segment_is_bullish = is_bullish[start_idx]
        fillcolor = (
            "rgba(135, 206, 250, 0.3)"  # Bullish: Light blue
            if segment_is_bullish
            else "rgba(255, 165, 0, 0.3)"  # Bearish: Orange
        )

        # Leading Span A line (no fill)
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span1,
                mode="lines",
                line=dict(width=0.5, color="green"),
                showlegend=False,
                hoverinfo="skip",
            )
        )

        # Leading Span B line (fill between Leading Span A)
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span2,
                mode="lines",
                line=dict(width=0.5, color="orange"),
                fill="tonexty",  # Fill between previous trace (Leading Span A)
                fillcolor=fillcolor,
                showlegend=False,
                hoverinfo="skip",
            )
        )

    return traces

Key points of this function:

  • Crossover detection with np.diff(): Automatically detect points where bearish/bullish switches
  • Segment division: Divide cloud at crossover points and set color for each segment
  • Color distinction: Light blue for bullish, orange for bearish for visual distinction
  • fill="tonexty": Fill between Leading Span A and B to express the cloud

Creating Cloud Segments Including Future Cloud

Extend the create_cloud_segments() function to display the future cloud.

def create_cloud_segments_with_future(dates, span1, span2, num_future_days=26):
    """
    Divide Ichimoku Cloud (including future cloud) into segments color-coded by bullish/bearish
    """
    # Generate future dates
    last_date = dates[-1]
    future_dates = generate_future_business_days(last_date, num_future_days)

    # Combine dates (past + future)
    all_dates = dates + future_dates

    # Convert Polars Series to NumPy array
    if hasattr(span1, "to_numpy"):
        span1_values = span1.to_numpy()
    else:
        span1_values = span1
    if hasattr(span2, "to_numpy"):
        span2_values = span2.to_numpy()
    else:
        span2_values = span2

    # Extend leading span arrays (fill first 26 with NaN)
    span1_extended = np.concatenate([[np.nan] * num_future_days, span1_values])
    span2_extended = np.concatenate([[np.nan] * num_future_days, span2_values])

    # Separate past and future portions
    past_dates = dates
    future_span1 = span1_extended[-num_future_days:]
    future_span2 = span2_extended[-num_future_days:]
    past_span1 = span1_extended[:-num_future_days]
    past_span2 = span2_extended[:-num_future_days]

    # Bullish judgment for past portion
    is_bullish = np.nan_to_num(past_span1 > past_span2, nan=False)

    # Detect crossover points
    crossovers = np.where(np.diff(is_bullish.astype(int)) != 0)[0] + 1

    # Create segment boundaries
    boundaries = np.concatenate([[0], crossovers, [len(past_dates)]])

    traces = []
    last_segment_color = None

    # Process each segment of past portion
    for i in range(len(boundaries) - 1):
        start_idx = boundaries[i]
        end_idx = boundaries[i + 1]

        segment_dates = past_dates[start_idx:end_idx]
        segment_span1 = past_span1[start_idx:end_idx]
        segment_span2 = past_span2[start_idx:end_idx]

        if np.all(np.isnan(segment_span1)) or np.all(np.isnan(segment_span2)):
            continue

        # Determine bullish/bearish of segment and set color
        segment_is_bullish = is_bullish[start_idx]
        fillcolor = (
            "rgba(135, 206, 250, 0.3)"  # Bullish: Light blue
            if segment_is_bullish
            else "rgba(255, 165, 0, 0.3)"  # Bearish: Orange
        )
        last_segment_color = fillcolor

        # Leading Span A line
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span1,
                mode="lines",
                line=dict(width=0.5, color="green"),
                showlegend=False,
                hoverinfo="skip",
            )
        )

        # Leading Span B line
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span2,
                mode="lines",
                line=dict(width=0.5, color="orange"),
                fill="tonexty",
                fillcolor=fillcolor,
                showlegend=False,
                hoverinfo="skip",
            )
        )

    # Add future cloud (continue last segment color)
    if last_segment_color and not np.all(np.isnan(future_span1)):
        traces.append(
            go.Scatter(
                x=future_dates,
                y=future_span1,
                mode="lines",
                line=dict(width=0.5, color="green", dash="dot"),  # Dotted line for future
                showlegend=False,
                hoverinfo="skip",
            )
        )

        traces.append(
            go.Scatter(
                x=future_dates,
                y=future_span2,
                mode="lines",
                line=dict(width=0.5, color="orange", dash="dot"),  # Dotted line for future
                fill="tonexty",
                fillcolor=last_segment_color,  # Continue last segment color
                showlegend=False,
                hoverinfo="skip",
            )
        )

    return traces

Key points of this function:

  • Future date generation: Generate 26 business days of future dates with generate_future_business_days()
  • Array extension: Fill first 26 with np.nan using np.concatenate([[np.nan] * 26, span1_values]), then concatenate actual leading span values
  • Separate past and future: Determine bullish/bearish with past portion ([:-26]) of extended array, continue last segment color for future portion ([-26:])
  • Dotted line for future: Display future cloud with dotted line using dash="dot" to visually distinguish from past data

Creating the Chart

Next, retrieve data and create the Ichimoku Cloud chart.

import polars as pl
import yfinance_pl as yf

# Retrieve data
ticker = yf.Ticker("8381.T")
hist = ticker.history(period="6mo")

# Calculate Ichimoku Cloud
ichimoku = get_ichimoku_values(hist)

# Add to DataFrame
df_plot = hist.with_columns(
    conversion_line=ichimoku["conversion_line"],
    base_line=ichimoku["base_line"],
    leading_span1=ichimoku["leading_span1"],
    leading_span2=ichimoku["leading_span2"],
    lagging_span=ichimoku["lagging_span"],
).with_columns(
    [
        pl.col("open.amount").cast(pl.Float64),
        pl.col("high.amount").cast(pl.Float64),
        pl.col("low.amount").cast(pl.Float64),
        pl.col("close.amount").cast(pl.Float64),
    ]
)

dates = df_plot["date"].to_list()

# Create chart
data = []

# Candlestick
data.append(
    go.Candlestick(
        x=dates,
        open=df_plot["open.amount"],
        high=df_plot["high.amount"],
        low=df_plot["low.amount"],
        close=df_plot["close.amount"],
        increasing_line_color="red",
        decreasing_line_color="green",
        name="Price",
    )
)

# Conversion Line
data.append(
    go.Scatter(
        x=dates,
        y=df_plot["conversion_line"],
        mode="lines",
        name="Conversion Line",
        line=dict(color="blue", width=1),
    )
)

# Base Line
data.append(
    go.Scatter(
        x=dates,
        y=df_plot["base_line"],
        mode="lines",
        name="Base Line",
        line=dict(color="red", width=1),
    )
)

# Cloud (color-coded bearish/bullish + future cloud)
cloud_traces = create_cloud_segments_with_future(
    dates,
    df_plot["leading_span1"],
    df_plot["leading_span2"],
    num_future_days=26,
)
data.extend(cloud_traces)

# Lagging Span
data.append(
    go.Scatter(
        x=dates,
        y=df_plot["lagging_span"],
        mode="lines",
        name="Lagging Span",
        line=dict(color="purple", width=1, dash="dot"),
    )
)

# Layout settings
layout = go.Layout(
    title="Stock Price and Ichimoku Cloud",
    xaxis_title="Date",
    yaxis_title="Price",
    xaxis_rangeslider_visible=False,
)

fig = go.Figure(data=data, layout=layout)
fig.show()

In this code, the create_cloud_segments_with_future() function color-codes the cloud based on bullish/bearish conditions. It displays light blue for bullish and orange for bearish, making trend changes visually easy to understand.

Key points of this code:

  • Automatic detection of cloud bearish/bullish: The create_cloud_segments_with_future() function determines the magnitude relationship between Leading Span A and B to detect crossover points
  • Color-coding by segment: Fill cloud with light blue for bullish and orange for bearish to visually express trend changes
  • Efficient rendering: Detect crossovers using NumPy’s np.diff() and render cloud with minimum number of traces
  • Color transparency: Set 30% transparency in rgba() format to make candlesticks and other lines easy to see
  • Future cloud display: Generate 26 business days of future dates with Python’s datetime and extend leading span arrays with np.concatenate() to display future cloud
  • Array extension: Map actual leading span values to future dates by filling first 26 with np.nan
  • Dotted line for future distinction: Display future cloud with dotted line using dash="dot" to clearly distinguish from past data
  • Color continuation: Apply color of last segment (bearish/bullish) of past portion to future cloud to visualize trend continuity

Summary

This time, we explained the calculation method for the five components of Ichimoku Cloud (Conversion Line, Base Line, Leading Span A & B, Lagging Span) and how to visualize them with Plotly. Using Polars’ rolling_max(), rolling_min(), and shift(), we can concisely implement the complex calculations of Ichimoku Cloud. In particular, it’s convenient that the time axis shifts of leading and lagging spans can be easily achieved with shift(). While the relationship with the previous Bollinger Bands and moving averages is low, you can grasp rough trends with Ichimoku Cloud alone by combining with simple sub-charts like volume. Fundamental analysis is helpful for the beginning and end right after trend changes, but it’s difficult to grasp with Ichimoku alone. However, the ease of understanding and depth that Ichimoku Cloud provides as a complete system is attractive, so I think it’s worth learning Ichimoku Cloud.

Reference: 『Pythonで実践する株式投資分析』(片渕彼富 (著), 山田祥寛 (監修))

comment

Loading comments...