{"spec_id":"indicator-ichimoku","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nindicator-ichimoku: Ichimoku Cloud Technical Indicator Chart\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\nimport re\nimport sys\nfrom datetime import datetime, timedelta\n\n\n# Script is named pygal.py — remove its directory from sys.path so the real package resolves\nsys.path = [p for p in sys.path if p != os.path.dirname(os.path.abspath(__file__))]\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens — Imprint palette (data colors theme-independent, chrome theme-adaptive)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — semantic exception applies: green up / red down is universal chart convention\nBULL_CLR = \"#009E73\"  # Imprint position 1: bullish candles (semantic green)\nBEAR_CLR = \"#AE3030\"  # Imprint position 5: bearish candles (semantic red anchor)\nTENKAN_CLR = \"#C475FD\"  # Imprint position 2: Tenkan-sen (conversion line)\nKIJUN_CLR = \"#4467A3\"  # Imprint position 3: Kijun-sen (base line)\nSPAN_A_CLR = \"#99B314\"  # Imprint position 8: Senkou Span A\nSPAN_B_CLR = \"#BD8233\"  # Imprint position 4: Senkou Span B\nCHIKOU_CLR = \"#2ABCCD\"  # Imprint position 6: Chikou Span\nCLOUD_BULL = \"#009E73\"  # Kumo fill when Span A > Span B (bullish)\nCLOUD_BEAR = \"#AE3030\"  # Kumo fill when Span B > Span A (bearish)\nCLOUD_OPA = \"0.25\" if THEME == \"light\" else \"0.35\"\n\n# Data: 180 trading days of synthetic OHLC stock prices (weekends skipped)\nnp.random.seed(42)\nn_days = 180\n\nstart_date = datetime(2024, 1, 2)\ndates = []\ncur = start_date\nfor _ in range(n_days):\n    while cur.weekday() >= 5:\n        cur += timedelta(days=1)\n    dates.append(cur)\n    cur += timedelta(days=1)\n\nbase_price = 175.0\nreturns = np.random.randn(n_days) * 1.8\nprice_series = base_price * np.cumprod(1 + returns / 100)\n\nohlc = []\nfor i, close in enumerate(price_series):\n    volatility = abs(np.random.randn()) * 1.5 + 0.3\n    intraday_range = close * volatility / 100\n    open_price = base_price if i == 0 else ohlc[-1][\"close\"]\n    high = max(open_price, close) + np.random.rand() * intraday_range\n    low = min(open_price, close) - np.random.rand() * intraday_range\n    ohlc.append({\"open\": open_price, \"high\": high, \"low\": low, \"close\": close})\n\nhighs = np.array([d[\"high\"] for d in ohlc])\nlows = np.array([d[\"low\"] for d in ohlc])\ncloses = np.array([d[\"close\"] for d in ohlc])\n\n# Ichimoku indicators — standard parameters (9, 26, 52)\nTENKAN_P, KIJUN_P, SENKOU_B_P, SHIFT = 9, 26, 52, 26\n\ntenkan_sen = np.full(n_days, np.nan)\nkijun_sen = np.full(n_days, np.nan)\nsenkou_a = np.full(n_days + SHIFT, np.nan)\nsenkou_b = np.full(n_days + SHIFT, np.nan)\nchikou_span = np.full(n_days, np.nan)\n\nfor i in range(n_days):\n    if i >= TENKAN_P - 1:\n        tenkan_sen[i] = (highs[i - TENKAN_P + 1 : i + 1].max() + lows[i - TENKAN_P + 1 : i + 1].min()) / 2\n    if i >= KIJUN_P - 1:\n        kijun_sen[i] = (highs[i - KIJUN_P + 1 : i + 1].max() + lows[i - KIJUN_P + 1 : i + 1].min()) / 2\n    if i >= KIJUN_P - 1:\n        senkou_a[i + SHIFT] = (tenkan_sen[i] + kijun_sen[i]) / 2\n    if i >= SENKOU_B_P - 1:\n        senkou_b[i + SHIFT] = (highs[i - SENKOU_B_P + 1 : i + 1].max() + lows[i - SENKOU_B_P + 1 : i + 1].min()) / 2\n    if i >= SHIFT:\n        chikou_span[i - SHIFT] = closes[i]\n\n# Display range: days 60-180 (120 candles + 26 cloud projection)\nVIEW_START, VIEW_END = 60, n_days\ntotal_x = VIEW_END - VIEW_START + SHIFT\n\n# Candlestick segments — wicks and bodies split by direction\nbull_wicks, bear_wicks = [], []\nbull_bodies, bear_bodies = [], []\n\nfor i in range(VIEW_START, VIEW_END):\n    x = i - VIEW_START + 1\n    c = ohlc[i]\n    wick = [(x, c[\"low\"]), (x, c[\"high\"]), None]\n    body = [(x, c[\"open\"]), (x, c[\"close\"]), None]\n    if c[\"close\"] >= c[\"open\"]:\n        bull_wicks.extend(wick)\n        bull_bodies.extend(body)\n    else:\n        bear_wicks.extend(wick)\n        bear_bodies.extend(body)\n\n# Ichimoku line data points\ntenkan_pts = [\n    (i - VIEW_START + 1, float(tenkan_sen[i])) for i in range(VIEW_START, VIEW_END) if not np.isnan(tenkan_sen[i])\n]\nkijun_pts = [\n    (i - VIEW_START + 1, float(kijun_sen[i])) for i in range(VIEW_START, VIEW_END) if not np.isnan(kijun_sen[i])\n]\n\nspan_a_pts, span_b_pts = [], []\nfor i in range(VIEW_START, VIEW_END + SHIFT):\n    x = i - VIEW_START + 1\n    if i < len(senkou_a) and not np.isnan(senkou_a[i]):\n        span_a_pts.append((x, float(senkou_a[i])))\n    if i < len(senkou_b) and not np.isnan(senkou_b[i]):\n        span_b_pts.append((x, float(senkou_b[i])))\n\nchikou_pts = [\n    (i - VIEW_START + 1, float(chikou_span[i])) for i in range(VIEW_START, VIEW_END) if not np.isnan(chikou_span[i])\n]\n\n# Price range for y-axis\nall_p = [d[\"high\"] for d in ohlc[VIEW_START:VIEW_END]] + [d[\"low\"] for d in ohlc[VIEW_START:VIEW_END]]\nall_p += [v for v in senkou_a[VIEW_START : VIEW_END + SHIFT] if not np.isnan(v)]\nall_p += [v for v in senkou_b[VIEW_START : VIEW_END + SHIFT] if not np.isnan(v)]\np_min, p_max = min(all_p), max(all_p)\np_pad = (p_max - p_min) * 0.06\n\n# Pygal style — theme-adaptive chrome, Imprint palette\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(\n        BULL_CLR,\n        BEAR_CLR,\n        BULL_CLR,\n        BEAR_CLR,\n        TENKAN_CLR,\n        KIJUN_CLR,\n        SPAN_A_CLR,\n        SPAN_B_CLR,\n        CHIKOU_CLR,\n        INK_MUTED,\n    ),\n    stroke_width=2.5,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n)\n\ntitle = \"indicator-ichimoku · python · pygal · anyplot.ai\"\ndate_map = {i - VIEW_START + 1: dates[i] for i in range(VIEW_START, min(VIEW_END, len(dates)))}\n\nchart = pygal.XY(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    title=title,\n    x_title=\"Date\",\n    y_title=\"Price ($)\",\n    show_dots=False,\n    show_x_guides=False,\n    show_y_guides=True,\n    allow_interruptions=True,\n    range=(p_min - p_pad, p_max + p_pad),\n    xrange=(0, total_x + 1),\n    legend_box_size=22,\n    margin=80,\n    spacing=20,\n    tooltip_border_radius=8,\n    truncate_legend=-1,\n    value_formatter=lambda x: f\"${x:.2f}\",\n    legend_at_bottom=True,\n)\n\nchart.x_labels = list(range(1, total_x + 1, 20))\nchart.x_value_formatter = lambda x: date_map[int(round(x))].strftime(\"%b %d\") if int(round(x)) in date_map else \"\"\n\nWICK_W, BODY_W, LINE_W, CHIKOU_W, REF_W = 12, 24, 5, 6, 2\n\n# Wicks — title=None hides from legend; series still rendered as serie-0 / serie-1\nchart.add(None, bull_wicks, stroke=True, show_dots=False, stroke_style={\"width\": WICK_W, \"linecap\": \"butt\"})\nchart.add(None, bear_wicks, stroke=True, show_dots=False, stroke_style={\"width\": WICK_W, \"linecap\": \"butt\"})\n\n# Candle bodies\nchart.add(\n    \"Bullish Candle\", bull_bodies, stroke=True, show_dots=False, stroke_style={\"width\": BODY_W, \"linecap\": \"butt\"}\n)\nchart.add(\n    \"Bearish Candle\", bear_bodies, stroke=True, show_dots=False, stroke_style={\"width\": BODY_W, \"linecap\": \"butt\"}\n)\n\n# Ichimoku lines\nchart.add(\n    \"Tenkan-sen (9)\", tenkan_pts, stroke=True, show_dots=False, stroke_style={\"width\": LINE_W, \"linecap\": \"round\"}\n)\nchart.add(\"Kijun-sen (26)\", kijun_pts, stroke=True, show_dots=False, stroke_style={\"width\": LINE_W, \"linecap\": \"round\"})\nchart.add(\"Senkou Span A\", span_a_pts, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"linecap\": \"round\"})\nchart.add(\"Senkou Span B\", span_b_pts, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"linecap\": \"round\"})\nchart.add(\n    \"Chikou Span\",\n    chikou_pts,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": CHIKOU_W, \"linecap\": \"round\", \"dasharray\": \"12,6\"},\n)\n\n# Reference line: close price at start of view window — focal point for price progress\nref_price = ohlc[VIEW_START][\"close\"]\nchart.add(\n    f\"Ref. Close (${ref_price:.0f})\",\n    [(1, ref_price), (total_x, ref_price)],\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": REF_W, \"linecap\": \"butt\", \"dasharray\": \"6,8\"},\n)\n\n# Render SVG; post-process stroke widths (cairosvg ignores pygal's JS-based styling)\nsvg = chart.render(is_unicode=True)\n\nstroke_specs = [\n    (WICK_W, \"butt\"),\n    (WICK_W, \"butt\"),  # series 0-1: wicks\n    (BODY_W, \"butt\"),\n    (BODY_W, \"butt\"),  # series 2-3: bodies\n    (LINE_W, \"round\"),\n    (LINE_W, \"round\"),  # series 4-5: tenkan, kijun\n    (3, \"round\"),\n    (3, \"round\"),  # series 6-7: span a, span b\n    (CHIKOU_W, \"round\"),  # series 8: chikou\n    (REF_W, \"butt\"),  # series 9: reference line\n]\nfor sid, (w, cap) in enumerate(stroke_specs):\n    svg = re.sub(\n        rf'(class=\"series serie-{sid} color-{sid}\"[^>]*>.*?)(class=\"line reactive nofill\")',\n        rf'\\1\\2 style=\"stroke-width:{w};stroke-linecap:{cap}\"',\n        svg,\n        count=1,\n        flags=re.DOTALL,\n    )\n\n# Inject Kumo cloud polygons — transform data coords → SVG pixel space\nbg_rects = re.findall(r'<rect[^>]*width=\"([^\"]*)\"[^>]*height=\"([^\"]*)\"[^>]*class=\"background\"', svg)\nif len(bg_rects) >= 2:\n    inner_w, inner_h = float(bg_rects[1][0]), float(bg_rects[1][1])\n    x_range_d = total_x + 1\n    y_lo = p_min - p_pad\n    y_range_d = (p_max + p_pad) - y_lo\n\n    span_a_dict = dict(span_a_pts)\n    span_b_dict = dict(span_b_pts)\n    common_x = sorted(set(span_a_dict) & set(span_b_dict))\n\n    polys = []\n    for k in range(len(common_x) - 1):\n        x1, x2 = common_x[k], common_x[k + 1]\n        if x2 - x1 > 2:\n            continue\n        a1, b1 = span_a_dict[x1], span_b_dict[x1]\n        a2, b2 = span_a_dict[x2], span_b_dict[x2]\n        fill = CLOUD_BULL if (a1 + a2) >= (b1 + b2) else CLOUD_BEAR\n        pts = [(x1, a1), (x2, a2), (x2, b2), (x1, b1)]\n        coords = \" \".join(\n            f\"{x / x_range_d * inner_w:.1f},{inner_h - (y - y_lo) / y_range_d * inner_h:.1f}\" for x, y in pts\n        )\n        polys.append(f'<polygon points=\"{coords}\" fill=\"{fill}\" fill-opacity=\"{CLOUD_OPA}\" stroke=\"none\" />')\n\n    if polys:\n        svg = svg.replace('<g class=\"series serie-0', \"\\n\".join(polys) + '\\n<g class=\"series serie-0')\n\n# Save PNG and interactive HTML\ncairosvg.svg2png(bytestring=svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(svg)\n"}