Ichimoku Cloud Technical Indicator Chart — Altair

Displays the Ichimoku Kinko Hyo ("one glance equilibrium chart") overlay on a candlestick price chart. The indicator plots five lines — Tenkan-sen (conversion), Kijun-sen (base), Senkou Span A, Senkou Span B, and Chikou Span (lagging) — with the area between Senkou Span A and B filled as the "Kumo" (cloud). The cloud color changes depending on which span is on top, providing an at-a-glance view of trend direction, momentum, and support/resistance zones.

Ichimoku Cloud Technical Indicator Chart rendered with Altair

Python source (Altair)

""" anyplot.ai
indicator-ichimoku: Ichimoku Cloud Technical Indicator Chart
Library: altair 6.2.1 | Python 3.13.13
Quality: 91/100 | Updated: 2026-06-08
"""

import os

import altair as alt
import numpy as np
import pandas as pd
from PIL import Image


# Theme
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — semantic exception: finance convention maps bullish→green, bearish→red
BULLISH_COLOR = "#009E73"  # Imprint position 1 — semantic green = up / gain
BEARISH_COLOR = "#AE3030"  # Imprint position 5 — semantic red  = down / loss

# Ichimoku line colors: remaining Imprint positions (positions 1 & 5 reserved semantically above)
LINE_COLORS = {
    "Tenkan-sen (9)": "#C475FD",  # Imprint position 2
    "Kijun-sen (26)": "#4467A3",  # Imprint position 3
    "Chikou Span": "#BD8233",  # Imprint position 4
    "Senkou Span A": "#2ABCCD",  # Imprint position 6
    "Senkou Span B": "#954477",  # Imprint position 7
}

# Data — 200 business days of simulated OHLC with engineered trend phases
np.random.seed(42)
n_days = 200
dates = pd.date_range("2024-01-01", periods=n_days, freq="B")

returns = np.random.normal(0.001, 0.018, n_days)
returns[40:70] += 0.004  # uptrend
returns[100:130] -= 0.005  # downtrend
returns[150:180] += 0.003  # recovery
price = 150 * np.cumprod(1 + returns)

open_prices = np.empty(n_days)
high_prices = np.empty(n_days)
low_prices = np.empty(n_days)
close_prices = price.copy()

open_prices[0] = 150.0
for i in range(1, n_days):
    open_prices[i] = close_prices[i - 1] + np.random.uniform(-0.5, 0.5)

for i in range(n_days):
    spread = np.random.uniform(1, 4)
    high_prices[i] = max(open_prices[i], close_prices[i]) + np.random.uniform(0.5, spread)
    low_prices[i] = min(open_prices[i], close_prices[i]) - np.random.uniform(0.5, spread)

df = pd.DataFrame(
    {
        "date": dates,
        "open": np.round(open_prices, 2),
        "high": np.round(high_prices, 2),
        "low": np.round(low_prices, 2),
        "close": np.round(close_prices, 2),
    }
)

# Ichimoku components — standard (9, 26, 52) parameters
period_9_high = df["high"].rolling(9).max()
period_9_low = df["low"].rolling(9).min()
period_26_high = df["high"].rolling(26).max()
period_26_low = df["low"].rolling(26).min()
period_52_high = df["high"].rolling(52).max()
period_52_low = df["low"].rolling(52).min()

df["tenkan_sen"] = (period_9_high + period_9_low) / 2
df["kijun_sen"] = (period_26_high + period_26_low) / 2

senkou_a = ((df["tenkan_sen"] + df["kijun_sen"]) / 2).values
senkou_b = ((period_52_high + period_52_low) / 2).values

# Senkou Spans shifted 26 periods into the future
future_dates = pd.date_range(start=dates[-1] + pd.tseries.offsets.BDay(1), periods=26, freq="B")
all_dates = dates.append(future_dates)
senkou_dates = all_dates[26 : 26 + n_days]

senkou_df = pd.DataFrame({"date": senkou_dates, "senkou_span_a": senkou_a, "senkou_span_b": senkou_b}).dropna()

# Chikou Span: close shifted 26 periods into the past
chikou_df = pd.DataFrame({"date": dates[: n_days - 26], "chikou_span": close_prices[26:]})

df["direction"] = np.where(df["close"] >= df["open"], "Bullish", "Bearish")

# Display from period 52 onward (52-period lookback complete)
display_df = df.iloc[52:].copy()

senkou_df["cloud_type"] = np.where(
    senkou_df["senkou_span_a"] >= senkou_df["senkou_span_b"], "Bullish Cloud", "Bearish Cloud"
)

# Axis domains
all_prices = pd.concat([display_df[["high", "low"]].stack(), senkou_df[["senkou_span_a", "senkou_span_b"]].stack()])
y_min = all_prices.min() * 0.98
y_max = all_prices.max() * 1.02
y_range = y_max - y_min
y_scale = alt.Scale(domain=[y_min, y_max])
x_scale = alt.Scale(domain=[display_df["date"].min(), senkou_df["date"].max()])

x_enc = alt.X("date:T", title="Date", scale=x_scale, axis=alt.Axis(format="%b '%y", labelAngle=-30, tickCount="month"))

# Cloud — increased opacity (0.28) for clearer boundary visibility
bullish_cloud_data = senkou_df[senkou_df["cloud_type"] == "Bullish Cloud"]
bearish_cloud_data = senkou_df[senkou_df["cloud_type"] == "Bearish Cloud"]

cloud_bullish = (
    alt.Chart(bullish_cloud_data)
    .mark_area(opacity=0.28, interpolate="monotone")
    .encode(
        x=x_enc,
        y=alt.Y("senkou_span_a:Q", title="Price ($)", scale=y_scale),
        y2="senkou_span_b:Q",
        color=alt.value(BULLISH_COLOR),
    )
)

cloud_bearish = (
    alt.Chart(bearish_cloud_data)
    .mark_area(opacity=0.28, interpolate="monotone")
    .encode(x=x_enc, y=alt.Y("senkou_span_a:Q", scale=y_scale), y2="senkou_span_b:Q", color=alt.value(BEARISH_COLOR))
)

# Candlesticks
candle_scale = alt.Scale(domain=["Bullish", "Bearish"], range=[BULLISH_COLOR, BEARISH_COLOR])

wicks = (
    alt.Chart(display_df)
    .mark_rule(strokeWidth=1.1)
    .encode(
        x=x_enc,
        y=alt.Y("low:Q", scale=y_scale),
        y2="high:Q",
        color=alt.Color("direction:N", scale=candle_scale, legend=None),
    )
)

bodies = (
    alt.Chart(display_df)
    .mark_bar(size=2.5)
    .encode(
        x=x_enc,
        y=alt.Y("open:Q", scale=y_scale),
        y2="close:Q",
        color=alt.Color("direction:N", scale=candle_scale, legend=None),
        tooltip=[
            alt.Tooltip("date:T", title="Date", format="%b %d, %Y"),
            alt.Tooltip("open:Q", title="Open", format="$.2f"),
            alt.Tooltip("high:Q", title="High", format="$.2f"),
            alt.Tooltip("low:Q", title="Low", format="$.2f"),
            alt.Tooltip("close:Q", title="Close", format="$.2f"),
        ],
    )
)

# Indicator lines — long-format for auto-legend
chikou_display = chikou_df[chikou_df["date"] >= display_df["date"].min()].copy()

comp_names = list(LINE_COLORS.keys())
comp_colors = list(LINE_COLORS.values())

lines_tenkan = (
    display_df[["date", "tenkan_sen"]].rename(columns={"tenkan_sen": "value"}).assign(component="Tenkan-sen (9)")
)
lines_kijun = (
    display_df[["date", "kijun_sen"]].rename(columns={"kijun_sen": "value"}).assign(component="Kijun-sen (26)")
)
lines_chikou = (
    chikou_display[["date", "chikou_span"]].rename(columns={"chikou_span": "value"}).assign(component="Chikou Span")
)
lines_span_a = (
    senkou_df[["date", "senkou_span_a"]].rename(columns={"senkou_span_a": "value"}).assign(component="Senkou Span A")
)
lines_span_b = (
    senkou_df[["date", "senkou_span_b"]].rename(columns={"senkou_span_b": "value"}).assign(component="Senkou Span B")
)

indicator_df = pd.concat([lines_tenkan, lines_kijun, lines_chikou, lines_span_a, lines_span_b], ignore_index=True)

indicator_color_scale = alt.Scale(domain=comp_names, range=comp_colors)
indicator_dash_scale = alt.Scale(domain=comp_names, range=[[1, 0], [8, 4], [4, 3], [1, 0], [1, 0]])

nearest = alt.selection_point(nearest=True, on="pointerover", fields=["date"], empty=False)

indicator_lines = (
    alt.Chart(indicator_df)
    .mark_line(strokeWidth=2.0, interpolate="monotone")
    .encode(
        x=x_enc,
        y=alt.Y("value:Q", scale=y_scale),
        color=alt.Color(
            "component:N",
            scale=indicator_color_scale,
            legend=alt.Legend(
                title="Ichimoku Components",
                titleFontSize=12,
                titleFontWeight="bold",
                labelFontSize=11,
                orient="none",
                legendX=318,
                legendY=5,
                fillColor=ELEVATED_BG,
                strokeColor=INK_SOFT,
                labelColor=INK_SOFT,
                titleColor=INK,
                padding=8,
                cornerRadius=4,
                symbolStrokeWidth=2.5,
                symbolSize=140,
                titlePadding=4,
            ),
        ),
        strokeDash=alt.StrokeDash("component:N", scale=indicator_dash_scale, legend=None),
        opacity=alt.value(0.9),
    )
)

# Interactive crosshair
crosshair_rule = (
    alt.Chart(indicator_df)
    .mark_rule(color=INK_MUTED, strokeWidth=0.8, strokeDash=[3, 3])
    .encode(x="date:T", opacity=alt.condition(nearest, alt.value(0.7), alt.value(0)))
    .add_params(nearest)
)

crosshair_dots = (
    alt.Chart(indicator_df)
    .mark_point(size=60, filled=True)
    .encode(
        x="date:T",
        y=alt.Y("value:Q", scale=y_scale),
        color=alt.Color("component:N", scale=indicator_color_scale, legend=None),
        opacity=alt.condition(nearest, alt.value(1), alt.value(0)),
        tooltip=[
            alt.Tooltip("date:T", title="Date", format="%b %d, %Y"),
            alt.Tooltip("component:N", title="Line"),
            alt.Tooltip("value:Q", title="Price", format="$.2f"),
        ],
    )
)

# TK crossover annotations — positioned with clearance above/below the candles
tk_diff = display_df["tenkan_sen"] - display_df["kijun_sen"]
bull_cross_idx = []
bear_cross_idx = []
for i in range(1, len(tk_diff)):
    idx = tk_diff.index[i]
    idx_prev = tk_diff.index[i - 1]
    if tk_diff.loc[idx_prev] < 0 and tk_diff.loc[idx] >= 0:
        bull_cross_idx.append(idx)
    elif tk_diff.loc[idx_prev] >= 0 and tk_diff.loc[idx] < 0:
        bear_cross_idx.append(idx)

annotation_layers = []
if bull_cross_idx:
    cx = bull_cross_idx[0]
    bull_df = pd.DataFrame(
        {
            "date": [display_df.loc[cx, "date"]],
            "price": [display_df.loc[cx, "high"] + y_range * 0.055],
            "label": ["▲ TK Cross"],
        }
    )
    annotation_layers.append(
        alt.Chart(bull_df)
        .mark_text(align="center", fontSize=11, fontWeight="bold")
        .encode(x=x_enc, y=alt.Y("price:Q", scale=y_scale), text="label:N", color=alt.value(BULLISH_COLOR))
    )

if bear_cross_idx:
    bx = bear_cross_idx[0]
    bear_df = pd.DataFrame(
        {
            "date": [display_df.loc[bx, "date"]],
            "price": [display_df.loc[bx, "low"] - y_range * 0.055],
            "label": ["▼ TK Cross"],
        }
    )
    annotation_layers.append(
        alt.Chart(bear_df)
        .mark_text(align="center", fontSize=11, fontWeight="bold")
        .encode(x=x_enc, y=alt.Y("price:Q", scale=y_scale), text="label:N", color=alt.value(BEARISH_COLOR))
    )

# Title — length 49 chars, below 67-char baseline, no scaling needed
title_str = "indicator-ichimoku · python · altair · anyplot.ai"
n_title = len(title_str)
ratio = 67 / n_title if n_title > 67 else 1.0
title_fontsize = max(11, round(16 * ratio))

# Compose all layers
all_layers = [cloud_bullish, cloud_bearish, wicks, bodies, indicator_lines, crosshair_rule, crosshair_dots]
all_layers.extend(annotation_layers)

chart = (
    alt.layer(*all_layers)
    .properties(
        width=620,
        height=320,
        background=PAGE_BG,
        title=alt.Title(
            title_str,
            fontSize=title_fontsize,
            anchor="middle",
            font="sans-serif",
            color=INK,
            subtitle="Ichimoku Kinko Hyo (9, 26, 52) — Tenkan / Kijun / Kumo / Chikou",
            subtitleFontSize=11,
            subtitleColor=INK_MUTED,
            subtitlePadding=4,
        ),
    )
    .resolve_scale(color="independent", strokeDash="independent")
    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0.5)
    .configure_axis(
        labelFontSize=10,
        titleFontSize=12,
        gridOpacity=0.12,
        gridColor=INK,
        domainColor=INK_SOFT,
        tickColor=INK_SOFT,
        labelColor=INK_SOFT,
        titleColor=INK,
    )
    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
)

# Save PNG — pad to exact 3200 × 1800 target
TW, TH = 3200, 1800
chart.save(f"plot-{THEME}.png", scale_factor=4.0)

_img = Image.open(f"plot-{THEME}.png").convert("RGB")
_w, _h = _img.size
if _w > TW or _h > TH:
    raise SystemExit(
        f"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. "
        f"Shrink chart .properties(width=, height=) values and re-render."
    )
if _w < TW or _h < TH:
    _canvas = Image.new("RGB", (TW, TH), PAGE_BG)
    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))
    _canvas.save(f"plot-{THEME}.png")

# Save HTML (interactive)
chart.save(f"plot-{THEME}.html")

Part of Ichimoku Cloud Technical Indicator Chart on anyplot.ai.

Other implementations