{"spec_id":"indicator-bollinger","library":"makie","language":"julia","code":"# anyplot.ai\n# indicator-bollinger: Bollinger Bands Indicator Chart\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 95/100 | Created: 2026-09-02\n\nusing CairoMakie\nusing Colors\nusing Random\nusing Statistics\nusing Dates\n\nRandom.seed!(42)\n\n# --- Theme tokens -------------------------------------------------------\nTHEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nPAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nINK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nINK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint categorical palette — first series ALWAYS brand green\nIMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green (price line)\n    colorant\"#C475FD\",  # 2 — lavender (band envelope + band-width panel)\n    colorant\"#4467A3\",  # 3 — blue\n    colorant\"#BD8233\",  # 4 — ochre\n    colorant\"#AE3030\",  # 5 — matte red\n    colorant\"#2ABCCD\",  # 6 — cyan\n    colorant\"#954477\",  # 7 — rose\n    colorant\"#99B314\",  # 8 — lime\n]\n\n# --- Data -----------------------------------------------------------------\n# Daily closing prices for a 120-trading-day window, with an engineered\n# volatility squeeze followed by a breakout to demonstrate band behavior.\nn_days = 120\nwindow = 20\n\ntrading_dates = Date[]\ncurrent_date = Date(2024, 1, 2)\nwhile length(trading_dates) < n_days\n    if Dates.dayofweek(current_date) <= 5\n        push!(trading_dates, current_date)\n    end\n    global current_date += Day(1)\nend\n\ncalm_returns = randn(45) .* 2.2\nsqueeze_returns = randn(35) .* 0.6\nbreakout_returns = randn(n_days - 80) .* 2.8 .+ 0.9\ndaily_returns = vcat(calm_returns, squeeze_returns, breakout_returns)\nclose_price = 150.0 .+ cumsum(daily_returns)\n\ntrading_day = 1:n_days\nband_range = window:n_days\nsma = [mean(close_price[(i - window + 1):i]) for i in band_range]\nband_std = [std(close_price[(i - window + 1):i]) for i in band_range]\nupper_band = sma .+ 2 .* band_std\nlower_band = sma .- 2 .* band_std\n\n# --- Plot -------------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax_price = Axis(\n    fig[1, 1];\n    title              = \"indicator-bollinger · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    ylabel             = \"Price (USD)\",\n    ylabelsize         = 14,\n    ylabelcolor        = INK,\n    yticklabelsize     = 12,\n    yticklabelcolor    = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    xticksvisible      = false,\n    xticklabelsvisible = false,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = false,\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    yminorgridvisible  = false,\n)\n\n# Linked sub-panel: band width (upper - lower) makes the volatility\n# squeeze/breakout pattern explicit instead of leaving it implicit in the\n# envelope's visual width alone.\nax_width = Axis(\n    fig[2, 1];\n    xlabel            = \"Date\",\n    ylabel            = \"Band width\",\n    xlabelsize        = 14,\n    ylabelsize        = 14,\n    xlabelcolor       = INK,\n    ylabelcolor       = INK,\n    xticklabelsize    = 12,\n    yticklabelsize    = 12,\n    xticklabelcolor   = INK_SOFT,\n    yticklabelcolor   = INK_SOFT,\n    xtickcolor        = INK_SOFT,\n    ytickcolor        = INK_SOFT,\n    backgroundcolor   = PAGE_BG,\n    topspinevisible   = false,\n    rightspinevisible = false,\n    leftspinecolor    = INK_SOFT,\n    bottomspinecolor  = INK_SOFT,\n    xgridvisible      = false,\n    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    yminorgridvisible = false,\n)\n\nlinkxaxes!(ax_price, ax_width)\n\ntick_positions = 1:15:n_days\nax_width.xticks = (tick_positions, Dates.format.(trading_dates[tick_positions], \"u d\"))\n\nband_plot = band!(\n    ax_price, band_range, lower_band, upper_band;\n    color = (IMPRINT_PALETTE[2], 0.18),\n)\nlines!(ax_price, band_range, upper_band; color = IMPRINT_PALETTE[2], linewidth = 1.5)\nlines!(ax_price, band_range, lower_band; color = IMPRINT_PALETTE[2], linewidth = 1.5)\nsma_line = lines!(ax_price, band_range, sma; color = INK, linestyle = :dash, linewidth = 2)\nclose_line = lines!(\n    ax_price, trading_day, close_price;\n    color = IMPRINT_PALETTE[1], linewidth = 3,\n)\n\nband_width = upper_band .- lower_band\nlines!(ax_width, band_range, band_width; color = IMPRINT_PALETTE[2], linewidth = 2)\n\nLegend(\n    fig[1:2, 2],\n    [close_line, sma_line, band_plot],\n    [\"Close price\", \"20-day SMA\", \"Bands (±2σ)\"];\n    framevisible   = false,\n    labelcolor     = INK,\n    backgroundcolor = :transparent,\n)\nrowsize!(fig.layout, 1, Relative(0.68))\nrowsize!(fig.layout, 2, Relative(0.32))\ncolsize!(fig.layout, 1, Relative(0.86))\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}