{"spec_id":"indicator-ichimoku","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nindicator-ichimoku: Ichimoku Cloud Technical Indicator Chart\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Prevent this script (bokeh.py) from shadowing the installed bokeh package when\n# Python adds its own directory to sys.path[0] on direct invocation.\nsys.path = [p for p in sys.path if os.path.abspath(p or os.getcwd()) != os.path.dirname(os.path.abspath(__file__))]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BoxAnnotation, ColumnDataSource, HoverTool, Label, Legend, NumeralTickFormatter, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette\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 assignments for Ichimoku components\nCOLOR_BULL_CANDLE = \"#009E73\"  # Imprint green — bullish (up) candles\nCOLOR_BEAR_CANDLE = \"#AE3030\"  # Imprint matte red — bearish (down) candles\nCOLOR_TENKAN = \"#C475FD\"  # Imprint lavender — Tenkan-sen (9-period)\nCOLOR_KIJUN = \"#4467A3\"  # Imprint blue — Kijun-sen (26-period)\nCOLOR_CHIKOU = \"#BD8233\"  # Imprint ochre — Chikou Span (lagging)\nCOLOR_CLOUD_BULL = \"#009E73\"  # Imprint green — bullish cloud (Span A >= Span B)\nCOLOR_CLOUD_BEAR = \"#AE3030\"  # Imprint matte red — bearish cloud (Span B > Span A)\n\n# Data — 200 trading days of OHLC with trending phases for Ichimoku analysis\nnp.random.seed(42)\nn_days = 200\nstart_price = 180.0\ndates = pd.date_range(start=\"2024-01-02\", periods=n_days, freq=\"B\")\n\nreturns = np.random.randn(n_days) * 0.014\nreturns[:50] += 0.002\nreturns[50:100] -= 0.003\nreturns[100:150] += 0.004\nreturns[150:] -= 0.001\nprices = start_price * 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 = prices.copy()\n\nopen_prices[0] = start_price\nfor i in range(1, n_days):\n    open_prices[i] = close_prices[i - 1]\n\nfor i in range(n_days):\n    daily_range = abs(np.random.randn()) * 0.01 * close_prices[i]\n    high_prices[i] = max(open_prices[i], close_prices[i]) + daily_range\n    low_prices[i] = min(open_prices[i], close_prices[i]) - daily_range\n\n# Ichimoku components using standard parameters (9, 26, 52)\nohlc_df = pd.DataFrame({\"high\": high_prices, \"low\": low_prices, \"close\": close_prices})\n\ntenkan_sen = ((ohlc_df[\"high\"].rolling(9).max() + ohlc_df[\"low\"].rolling(9).min()) / 2).values\nkijun_sen = ((ohlc_df[\"high\"].rolling(26).max() + ohlc_df[\"low\"].rolling(26).min()) / 2).values\n\nsenkou_a_unshifted = (tenkan_sen + kijun_sen) / 2\nsenkou_b_unshifted = ((ohlc_df[\"high\"].rolling(52).max() + ohlc_df[\"low\"].rolling(52).min()) / 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.union(future_dates)\n\nsenkou_span_a = np.full(n_days + 26, np.nan)\nsenkou_span_b = np.full(n_days + 26, np.nan)\nsenkou_span_a[26 : n_days + 26] = senkou_a_unshifted\nsenkou_span_b[26 : n_days + 26] = senkou_b_unshifted\n\n# Chikou Span: close shifted 26 periods into the past\nchikou_span = np.full(n_days, np.nan)\nchikou_span[: n_days - 26] = close_prices[26:]\n\ndf = pd.DataFrame(\n    {\n        \"date\": dates,\n        \"open\": open_prices,\n        \"high\": high_prices,\n        \"low\": low_prices,\n        \"close\": close_prices,\n        \"tenkan\": tenkan_sen,\n        \"kijun\": kijun_sen,\n        \"chikou\": chikou_span,\n    }\n)\ndf[\"bullish\"] = df[\"close\"] >= df[\"open\"]\ndf[\"date_str\"] = df[\"date\"].dt.strftime(\"%b %d, %Y\")\n\ncloud_df = pd.DataFrame({\"date\": all_dates, \"span_a\": senkou_span_a, \"span_b\": senkou_span_b})\ncloud_df = cloud_df.dropna().reset_index(drop=True)\ncloud_df[\"bullish_cloud\"] = cloud_df[\"span_a\"] >= cloud_df[\"span_b\"]\n\nbullish_df = df[df[\"bullish\"]].copy()\nbearish_df = df[~df[\"bullish\"]].copy()\nsource_bull = ColumnDataSource(bullish_df)\nsource_bear = ColumnDataSource(bearish_df)\n\n# Figure — 3200×1800, toolbar off for PNG, min_border reserves room for large labels\np = figure(\n    width=3200,\n    height=1800,\n    x_axis_type=\"datetime\",\n    title=\"indicator-ichimoku · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Date\",\n    y_axis_label=\"Price ($)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\nx_pad = pd.Timedelta(days=3)\np.x_range = Range1d(start=dates[51] - x_pad, end=all_dates[-1] + x_pad)\n\n# Kumo (cloud) — filled area between Senkou Span A and B, alpha raised for thin regions\nbull_cloud = cloud_df[cloud_df[\"bullish_cloud\"]].copy()\nbear_cloud = cloud_df[~cloud_df[\"bullish_cloud\"]].copy()\n\nif len(bull_cloud) > 0:\n    r_cloud_bull = p.varea(\n        x=\"date\",\n        y1=\"span_a\",\n        y2=\"span_b\",\n        source=ColumnDataSource(bull_cloud),\n        fill_color=COLOR_CLOUD_BULL,\n        fill_alpha=0.28,\n    )\n\nif len(bear_cloud) > 0:\n    r_cloud_bear = p.varea(\n        x=\"date\",\n        y1=\"span_a\",\n        y2=\"span_b\",\n        source=ColumnDataSource(bear_cloud),\n        fill_color=COLOR_CLOUD_BEAR,\n        fill_alpha=0.28,\n    )\n\n# Cloud boundary lines\nr_span_a = p.line(\n    x=\"date\",\n    y=\"value\",\n    source=ColumnDataSource(cloud_df[[\"date\", \"span_a\"]].rename(columns={\"span_a\": \"value\"})),\n    line_color=COLOR_CLOUD_BULL,\n    line_width=2.5,\n    line_alpha=0.85,\n)\nr_span_b = p.line(\n    x=\"date\",\n    y=\"value\",\n    source=ColumnDataSource(cloud_df[[\"date\", \"span_b\"]].rename(columns={\"span_b\": \"value\"})),\n    line_color=COLOR_CLOUD_BEAR,\n    line_width=2.5,\n    line_alpha=0.85,\n)\n\n# Candlestick wicks and bodies\ncandle_width = 0.6 * 24 * 60 * 60 * 1000\np.segment(x0=\"date\", y0=\"high\", x1=\"date\", y1=\"low\", source=source_bull, color=COLOR_BULL_CANDLE, line_width=2)\np.segment(x0=\"date\", y0=\"high\", x1=\"date\", y1=\"low\", source=source_bear, color=COLOR_BEAR_CANDLE, line_width=2)\n\nbull_bars = p.vbar(\n    x=\"date\",\n    top=\"close\",\n    bottom=\"open\",\n    width=candle_width,\n    source=source_bull,\n    fill_color=COLOR_BULL_CANDLE,\n    line_color=COLOR_BULL_CANDLE,\n)\nbear_bars = p.vbar(\n    x=\"date\",\n    top=\"open\",\n    bottom=\"close\",\n    width=candle_width,\n    source=source_bear,\n    fill_color=COLOR_BEAR_CANDLE,\n    line_color=COLOR_BEAR_CANDLE,\n)\n\n# Ichimoku lines — Tenkan and Kijun with strong visual weight\nline_df = df.dropna(subset=[\"tenkan\", \"kijun\"]).copy()\nline_source = ColumnDataSource(line_df)\n\nr_tenkan = p.line(x=\"date\", y=\"tenkan\", source=line_source, line_color=COLOR_TENKAN, line_width=3.5)\nr_kijun = p.line(x=\"date\", y=\"kijun\", source=line_source, line_color=COLOR_KIJUN, line_width=3.5)\n\n# Chikou Span — wider line improves visibility in dense candlestick areas\nchikou_df = df.dropna(subset=[\"chikou\"]).copy()\nr_chikou = p.line(\n    x=\"date\",\n    y=\"chikou\",\n    source=ColumnDataSource(chikou_df),\n    line_color=COLOR_CHIKOU,\n    line_width=3.5,\n    line_dash=\"dashed\",\n)\n\n# TK Cross BoxAnnotation — highlights first bullish Tenkan/Kijun crossover\nvalid = line_df.dropna(subset=[\"tenkan\", \"kijun\"]).copy()\nvalid[\"tk_diff\"] = valid[\"tenkan\"] - valid[\"kijun\"]\nvalid[\"cross\"] = np.sign(valid[\"tk_diff\"]).diff()\nbullish_crosses = valid[valid[\"cross\"] == 2.0]\n\nif len(bullish_crosses) > 0:\n    cross_idx = bullish_crosses.index[0]\n    cross_date = valid.loc[cross_idx, \"date\"]\n    cross_price = valid.loc[cross_idx, \"tenkan\"]\n    p.add_layout(\n        BoxAnnotation(\n            left=cross_date - pd.Timedelta(days=8),\n            right=cross_date + pd.Timedelta(days=8),\n            fill_alpha=0.10,\n            fill_color=COLOR_TENKAN,\n            line_color=COLOR_TENKAN,\n            line_alpha=0.4,\n            line_width=2,\n        )\n    )\n    p.add_layout(\n        Label(\n            x=cross_date,\n            y=cross_price + 3,\n            text=\"TK Cross\",\n            text_font_size=\"26pt\",\n            text_color=COLOR_TENKAN,\n            text_font_style=\"bold\",\n            text_alpha=0.9,\n        )\n    )\n\n# Legend — theme-adaptive, placed in right panel\nlegend_items = [\n    (\"Tenkan-sen (9)\", [r_tenkan]),\n    (\"Kijun-sen (26)\", [r_kijun]),\n    (\"Chikou Span\", [r_chikou]),\n    (\"Senkou A\", [r_span_a]),\n    (\"Senkou B\", [r_span_b]),\n    (\"Kumo (bullish)\", [r_cloud_bull]),\n    (\"Kumo (bearish)\", [r_cloud_bear]),\n]\nlegend = Legend(items=legend_items, location=\"top_left\")\nlegend.label_text_font_size = \"28pt\"\nlegend.label_text_color = INK_SOFT\nlegend.glyph_height = 30\nlegend.glyph_width = 40\nlegend.spacing = 12\nlegend.padding = 15\nlegend.background_fill_color = ELEVATED_BG\nlegend.background_fill_alpha = 0.92\nlegend.border_line_color = INK_SOFT\nlegend.border_line_width = 1\np.add_layout(legend, \"right\")\n\n# Hover tooltip for candlesticks\np.add_tools(\n    HoverTool(\n        renderers=[bull_bars, bear_bars],\n        tooltips=\"\"\"\n        <div style=\"font-size:16px; padding:8px;\">\n            <strong>@date_str</strong><br/>\n            Open: @open{$0.00}<br/>\n            High: @high{$0.00}<br/>\n            Low: @low{$0.00}<br/>\n            Close: @close{$0.00}\n        </div>\n        \"\"\",\n        mode=\"vline\",\n    )\n)\n\n# Text sizing for 3200×1800\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"normal\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\np.yaxis.formatter = NumeralTickFormatter(format=\"$0\")\n\n# Grid — y-axis only, subtle dashed lines\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\np.ygrid.grid_line_width = 1\np.ygrid.grid_line_dash = [4, 4]\n\n# Axis chrome — theme-adaptive\np.outline_line_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.axis_line_width = 1\np.yaxis.axis_line_width = 1\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Save HTML (interactive artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with Selenium — avoids export_png chromedriver snap issues\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\n\n# Chrome headless has ~139 px of browser chrome overhead; resize so the\n# viewport (window.innerHeight) is exactly H, not H minus that overhead.\nvh = driver.execute_script(\"return window.innerHeight\")\nif vh != H:\n    driver.set_window_size(W, H + (H - vh))\n\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}