{"spec_id":"indicator-bollinger","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nindicator-bollinger: Bollinger Bands Indicator Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 99/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\nsys.path = [p for p in sys.path if p != \"\" and p != os.getcwd()]\nimport altair as alt\n\n\n# Theme tokens\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\"\n\n# Data colors (Okabe-Ito palette - theme-independent)\nPRICE_COLOR = \"#009E73\"  # Position 1: brand green\nSMA_COLOR = \"#C475FD\"  # Position 2: vermillion\nBAND_COLOR = \"#4467A3\"  # Position 3: blue\n\n# Data - Simulated stock price with realistic Bollinger Bands\nnp.random.seed(42)\nn_days = 120\ndates = pd.date_range(\"2024-01-01\", periods=n_days, freq=\"B\")  # Business days\n\n# Generate price series with trend and volatility changes\nreturns = np.random.randn(n_days) * 0.015  # Daily returns ~1.5% std\nreturns[:30] += 0.002  # Uptrend start\nreturns[50:70] -= 0.003  # Downtrend middle\nreturns[90:] += 0.002  # Recovery end\nprice = 100 * np.cumprod(1 + returns)\n\n# Calculate Bollinger Bands (20-period SMA, 2 std deviations)\nwindow = 20\nsma = pd.Series(price).rolling(window=window).mean()\nstd = pd.Series(price).rolling(window=window).std()\nupper_band = sma + 2 * std\nlower_band = sma - 2 * std\n\n# Create DataFrame starting from period 20 (where we have valid SMA)\ndf = pd.DataFrame(\n    {\n        \"date\": dates[window - 1 :],\n        \"close\": price[window - 1 :],\n        \"sma\": sma[window - 1 :].values,\n        \"upper_band\": upper_band[window - 1 :].values,\n        \"lower_band\": lower_band[window - 1 :].values,\n    }\n)\n\n# Calculate Y-axis range with some padding\ny_min = df[[\"close\", \"lower_band\"]].min().min() * 0.98\ny_max = df[[\"close\", \"upper_band\"]].max().max() * 1.02\n\n# Base chart configuration\nbase = alt.Chart(df).encode(\n    x=alt.X(\"date:T\", title=\"Date\", axis=alt.Axis(format=\"%b %d\", labelAngle=-45, tickCount=10))\n)\n\n# Band area (filled region between upper and lower bands)\nband_area = (\n    alt.Chart(df)\n    .mark_area(opacity=0.2, color=BAND_COLOR)\n    .encode(\n        x=alt.X(\"date:T\", title=\"Date\"),\n        y=alt.Y(\"lower_band:Q\", title=\"Price ($)\", scale=alt.Scale(domain=[y_min, y_max])),\n        y2=\"upper_band:Q\",\n    )\n)\n\n# Upper band line\nupper_line = base.mark_line(strokeWidth=2, color=BAND_COLOR, opacity=0.6).encode(\n    y=alt.Y(\"upper_band:Q\", scale=alt.Scale(domain=[y_min, y_max]))\n)\n\n# Lower band line\nlower_line = base.mark_line(strokeWidth=2, color=BAND_COLOR, opacity=0.6).encode(\n    y=alt.Y(\"lower_band:Q\", scale=alt.Scale(domain=[y_min, y_max]))\n)\n\n# Middle band (SMA) - dashed line\nsma_line = base.mark_line(strokeWidth=2.5, strokeDash=[8, 4], color=SMA_COLOR).encode(\n    y=alt.Y(\"sma:Q\", scale=alt.Scale(domain=[y_min, y_max]))\n)\n\n# Price line - prominent\nprice_line = base.mark_line(strokeWidth=3.5, color=PRICE_COLOR).encode(\n    y=alt.Y(\"close:Q\", scale=alt.Scale(domain=[y_min, y_max]))\n)\n\n# Combine all layers\nchart = (\n    alt.layer(band_area, upper_line, lower_line, sma_line, price_line)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"indicator-bollinger · altair · anyplot.ai\", fontSize=28, anchor=\"middle\"),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.1,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=18,\n        titleFontSize=22,\n    )\n    .configure_title(color=INK, fontSize=28)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}