{"spec_id":"indicator-ichimoku","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nindicator-ichimoku: Ichimoku Cloud Technical Indicator Chart\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — semantic exception: finance convention maps bullish→green, bearish→red\nBULLISH_COLOR = \"#009E73\"  # Imprint position 1 — semantic green = up / gain\nBEARISH_COLOR = \"#AE3030\"  # Imprint position 5 — semantic red  = down / loss\n\n# Ichimoku line colors: remaining Imprint positions (positions 1 & 5 reserved semantically above)\nLINE_COLORS = {\n    \"Tenkan-sen (9)\": \"#C475FD\",  # Imprint position 2\n    \"Kijun-sen (26)\": \"#4467A3\",  # Imprint position 3\n    \"Chikou Span\": \"#BD8233\",  # Imprint position 4\n    \"Senkou Span A\": \"#2ABCCD\",  # Imprint position 6\n    \"Senkou Span B\": \"#954477\",  # Imprint position 7\n}\n\n# Data — 200 business days of simulated OHLC with engineered trend phases\nnp.random.seed(42)\nn_days = 200\ndates = pd.date_range(\"2024-01-01\", periods=n_days, freq=\"B\")\n\nreturns = np.random.normal(0.001, 0.018, n_days)\nreturns[40:70] += 0.004  # uptrend\nreturns[100:130] -= 0.005  # downtrend\nreturns[150:180] += 0.003  # recovery\nprice = 150 * np.cumprod(1 + returns)\n\nopen_prices = np.empty(n_days)\nhigh_prices = np.empty(n_days)\nlow_prices = np.empty(n_days)\nclose_prices = price.copy()\n\nopen_prices[0] = 150.0\nfor i in range(1, n_days):\n    open_prices[i] = close_prices[i - 1] + np.random.uniform(-0.5, 0.5)\n\nfor i in range(n_days):\n    spread = np.random.uniform(1, 4)\n    high_prices[i] = max(open_prices[i], close_prices[i]) + np.random.uniform(0.5, spread)\n    low_prices[i] = min(open_prices[i], close_prices[i]) - np.random.uniform(0.5, spread)\n\ndf = pd.DataFrame(\n    {\n        \"date\": dates,\n        \"open\": np.round(open_prices, 2),\n        \"high\": np.round(high_prices, 2),\n        \"low\": np.round(low_prices, 2),\n        \"close\": np.round(close_prices, 2),\n    }\n)\n\n# Ichimoku components — standard (9, 26, 52) parameters\nperiod_9_high = df[\"high\"].rolling(9).max()\nperiod_9_low = df[\"low\"].rolling(9).min()\nperiod_26_high = df[\"high\"].rolling(26).max()\nperiod_26_low = df[\"low\"].rolling(26).min()\nperiod_52_high = df[\"high\"].rolling(52).max()\nperiod_52_low = df[\"low\"].rolling(52).min()\n\ndf[\"tenkan_sen\"] = (period_9_high + period_9_low) / 2\ndf[\"kijun_sen\"] = (period_26_high + period_26_low) / 2\n\nsenkou_a = ((df[\"tenkan_sen\"] + df[\"kijun_sen\"]) / 2).values\nsenkou_b = ((period_52_high + period_52_low) / 2).values\n\n# Senkou Spans shifted 26 periods into the future\nfuture_dates = pd.date_range(start=dates[-1] + pd.tseries.offsets.BDay(1), periods=26, freq=\"B\")\nall_dates = dates.append(future_dates)\nsenkou_dates = all_dates[26 : 26 + n_days]\n\nsenkou_df = pd.DataFrame({\"date\": senkou_dates, \"senkou_span_a\": senkou_a, \"senkou_span_b\": senkou_b}).dropna()\n\n# Chikou Span: close shifted 26 periods into the past\nchikou_df = pd.DataFrame({\"date\": dates[: n_days - 26], \"chikou_span\": close_prices[26:]})\n\ndf[\"direction\"] = np.where(df[\"close\"] >= df[\"open\"], \"Bullish\", \"Bearish\")\n\n# Display from period 52 onward (52-period lookback complete)\ndisplay_df = df.iloc[52:].copy()\n\nsenkou_df[\"cloud_type\"] = np.where(\n    senkou_df[\"senkou_span_a\"] >= senkou_df[\"senkou_span_b\"], \"Bullish Cloud\", \"Bearish Cloud\"\n)\n\n# Axis domains\nall_prices = pd.concat([display_df[[\"high\", \"low\"]].stack(), senkou_df[[\"senkou_span_a\", \"senkou_span_b\"]].stack()])\ny_min = all_prices.min() * 0.98\ny_max = all_prices.max() * 1.02\ny_range = y_max - y_min\ny_scale = alt.Scale(domain=[y_min, y_max])\nx_scale = alt.Scale(domain=[display_df[\"date\"].min(), senkou_df[\"date\"].max()])\n\nx_enc = alt.X(\"date:T\", title=\"Date\", scale=x_scale, axis=alt.Axis(format=\"%b '%y\", labelAngle=-30, tickCount=\"month\"))\n\n# Cloud — increased opacity (0.28) for clearer boundary visibility\nbullish_cloud_data = senkou_df[senkou_df[\"cloud_type\"] == \"Bullish Cloud\"]\nbearish_cloud_data = senkou_df[senkou_df[\"cloud_type\"] == \"Bearish Cloud\"]\n\ncloud_bullish = (\n    alt.Chart(bullish_cloud_data)\n    .mark_area(opacity=0.28, interpolate=\"monotone\")\n    .encode(\n        x=x_enc,\n        y=alt.Y(\"senkou_span_a:Q\", title=\"Price ($)\", scale=y_scale),\n        y2=\"senkou_span_b:Q\",\n        color=alt.value(BULLISH_COLOR),\n    )\n)\n\ncloud_bearish = (\n    alt.Chart(bearish_cloud_data)\n    .mark_area(opacity=0.28, interpolate=\"monotone\")\n    .encode(x=x_enc, y=alt.Y(\"senkou_span_a:Q\", scale=y_scale), y2=\"senkou_span_b:Q\", color=alt.value(BEARISH_COLOR))\n)\n\n# Candlesticks\ncandle_scale = alt.Scale(domain=[\"Bullish\", \"Bearish\"], range=[BULLISH_COLOR, BEARISH_COLOR])\n\nwicks = (\n    alt.Chart(display_df)\n    .mark_rule(strokeWidth=1.1)\n    .encode(\n        x=x_enc,\n        y=alt.Y(\"low:Q\", scale=y_scale),\n        y2=\"high:Q\",\n        color=alt.Color(\"direction:N\", scale=candle_scale, legend=None),\n    )\n)\n\nbodies = (\n    alt.Chart(display_df)\n    .mark_bar(size=2.5)\n    .encode(\n        x=x_enc,\n        y=alt.Y(\"open:Q\", scale=y_scale),\n        y2=\"close:Q\",\n        color=alt.Color(\"direction:N\", scale=candle_scale, legend=None),\n        tooltip=[\n            alt.Tooltip(\"date:T\", title=\"Date\", format=\"%b %d, %Y\"),\n            alt.Tooltip(\"open:Q\", title=\"Open\", format=\"$.2f\"),\n            alt.Tooltip(\"high:Q\", title=\"High\", format=\"$.2f\"),\n            alt.Tooltip(\"low:Q\", title=\"Low\", format=\"$.2f\"),\n            alt.Tooltip(\"close:Q\", title=\"Close\", format=\"$.2f\"),\n        ],\n    )\n)\n\n# Indicator lines — long-format for auto-legend\nchikou_display = chikou_df[chikou_df[\"date\"] >= display_df[\"date\"].min()].copy()\n\ncomp_names = list(LINE_COLORS.keys())\ncomp_colors = list(LINE_COLORS.values())\n\nlines_tenkan = (\n    display_df[[\"date\", \"tenkan_sen\"]].rename(columns={\"tenkan_sen\": \"value\"}).assign(component=\"Tenkan-sen (9)\")\n)\nlines_kijun = (\n    display_df[[\"date\", \"kijun_sen\"]].rename(columns={\"kijun_sen\": \"value\"}).assign(component=\"Kijun-sen (26)\")\n)\nlines_chikou = (\n    chikou_display[[\"date\", \"chikou_span\"]].rename(columns={\"chikou_span\": \"value\"}).assign(component=\"Chikou Span\")\n)\nlines_span_a = (\n    senkou_df[[\"date\", \"senkou_span_a\"]].rename(columns={\"senkou_span_a\": \"value\"}).assign(component=\"Senkou Span A\")\n)\nlines_span_b = (\n    senkou_df[[\"date\", \"senkou_span_b\"]].rename(columns={\"senkou_span_b\": \"value\"}).assign(component=\"Senkou Span B\")\n)\n\nindicator_df = pd.concat([lines_tenkan, lines_kijun, lines_chikou, lines_span_a, lines_span_b], ignore_index=True)\n\nindicator_color_scale = alt.Scale(domain=comp_names, range=comp_colors)\nindicator_dash_scale = alt.Scale(domain=comp_names, range=[[1, 0], [8, 4], [4, 3], [1, 0], [1, 0]])\n\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"date\"], empty=False)\n\nindicator_lines = (\n    alt.Chart(indicator_df)\n    .mark_line(strokeWidth=2.0, interpolate=\"monotone\")\n    .encode(\n        x=x_enc,\n        y=alt.Y(\"value:Q\", scale=y_scale),\n        color=alt.Color(\n            \"component:N\",\n            scale=indicator_color_scale,\n            legend=alt.Legend(\n                title=\"Ichimoku Components\",\n                titleFontSize=12,\n                titleFontWeight=\"bold\",\n                labelFontSize=11,\n                orient=\"none\",\n                legendX=318,\n                legendY=5,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n                padding=8,\n                cornerRadius=4,\n                symbolStrokeWidth=2.5,\n                symbolSize=140,\n                titlePadding=4,\n            ),\n        ),\n        strokeDash=alt.StrokeDash(\"component:N\", scale=indicator_dash_scale, legend=None),\n        opacity=alt.value(0.9),\n    )\n)\n\n# Interactive crosshair\ncrosshair_rule = (\n    alt.Chart(indicator_df)\n    .mark_rule(color=INK_MUTED, strokeWidth=0.8, strokeDash=[3, 3])\n    .encode(x=\"date:T\", opacity=alt.condition(nearest, alt.value(0.7), alt.value(0)))\n    .add_params(nearest)\n)\n\ncrosshair_dots = (\n    alt.Chart(indicator_df)\n    .mark_point(size=60, filled=True)\n    .encode(\n        x=\"date:T\",\n        y=alt.Y(\"value:Q\", scale=y_scale),\n        color=alt.Color(\"component:N\", scale=indicator_color_scale, legend=None),\n        opacity=alt.condition(nearest, alt.value(1), alt.value(0)),\n        tooltip=[\n            alt.Tooltip(\"date:T\", title=\"Date\", format=\"%b %d, %Y\"),\n            alt.Tooltip(\"component:N\", title=\"Line\"),\n            alt.Tooltip(\"value:Q\", title=\"Price\", format=\"$.2f\"),\n        ],\n    )\n)\n\n# TK crossover annotations — positioned with clearance above/below the candles\ntk_diff = display_df[\"tenkan_sen\"] - display_df[\"kijun_sen\"]\nbull_cross_idx = []\nbear_cross_idx = []\nfor i in range(1, len(tk_diff)):\n    idx = tk_diff.index[i]\n    idx_prev = tk_diff.index[i - 1]\n    if tk_diff.loc[idx_prev] < 0 and tk_diff.loc[idx] >= 0:\n        bull_cross_idx.append(idx)\n    elif tk_diff.loc[idx_prev] >= 0 and tk_diff.loc[idx] < 0:\n        bear_cross_idx.append(idx)\n\nannotation_layers = []\nif bull_cross_idx:\n    cx = bull_cross_idx[0]\n    bull_df = pd.DataFrame(\n        {\n            \"date\": [display_df.loc[cx, \"date\"]],\n            \"price\": [display_df.loc[cx, \"high\"] + y_range * 0.055],\n            \"label\": [\"▲ TK Cross\"],\n        }\n    )\n    annotation_layers.append(\n        alt.Chart(bull_df)\n        .mark_text(align=\"center\", fontSize=11, fontWeight=\"bold\")\n        .encode(x=x_enc, y=alt.Y(\"price:Q\", scale=y_scale), text=\"label:N\", color=alt.value(BULLISH_COLOR))\n    )\n\nif bear_cross_idx:\n    bx = bear_cross_idx[0]\n    bear_df = pd.DataFrame(\n        {\n            \"date\": [display_df.loc[bx, \"date\"]],\n            \"price\": [display_df.loc[bx, \"low\"] - y_range * 0.055],\n            \"label\": [\"▼ TK Cross\"],\n        }\n    )\n    annotation_layers.append(\n        alt.Chart(bear_df)\n        .mark_text(align=\"center\", fontSize=11, fontWeight=\"bold\")\n        .encode(x=x_enc, y=alt.Y(\"price:Q\", scale=y_scale), text=\"label:N\", color=alt.value(BEARISH_COLOR))\n    )\n\n# Title — length 49 chars, below 67-char baseline, no scaling needed\ntitle_str = \"indicator-ichimoku · python · altair · anyplot.ai\"\nn_title = len(title_str)\nratio = 67 / n_title if n_title > 67 else 1.0\ntitle_fontsize = max(11, round(16 * ratio))\n\n# Compose all layers\nall_layers = [cloud_bullish, cloud_bearish, wicks, bodies, indicator_lines, crosshair_rule, crosshair_dots]\nall_layers.extend(annotation_layers)\n\nchart = (\n    alt.layer(*all_layers)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            title_str,\n            fontSize=title_fontsize,\n            anchor=\"middle\",\n            font=\"sans-serif\",\n            color=INK,\n            subtitle=\"Ichimoku Kinko Hyo (9, 26, 52) — Tenkan / Kijun / Kumo / Chikou\",\n            subtitleFontSize=11,\n            subtitleColor=INK_MUTED,\n            subtitlePadding=4,\n        ),\n    )\n    .resolve_scale(color=\"independent\", strokeDash=\"independent\")\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0.5)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        gridOpacity=0.12,\n        gridColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save PNG — pad to exact 3200 × 1800 target\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save HTML (interactive)\nchart.save(f\"plot-{THEME}.html\")\n"}