{"spec_id":"indicator-bollinger","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nindicator-bollinger: Bollinger Bands Indicator Chart\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-17\n\"\"\"\n# ruff: noqa: F403, F405\n\"\"\"anyplot.ai\nindicator-bollinger: Bollinger Bands Indicator Chart\nLibrary: letsplot | Python 3.13\nQuality: pending | Updated: 2026-05-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nBRAND = \"#009E73\"  # Okabe-Ito position 1\nBAND_COLOR = \"#4467A3\"  # Okabe-Ito position 3\n\n# Data - Generate synthetic stock price data with Bollinger Bands\nnp.random.seed(42)\nn_periods = 120\n\n# Generate price data with trend and volatility\ndates = pd.date_range(start=\"2024-01-01\", periods=n_periods, freq=\"B\")\nreturns = np.random.normal(0.0005, 0.018, n_periods)\n# Add some volatility clustering\nvolatility_multiplier = np.where((np.arange(n_periods) > 40) & (np.arange(n_periods) < 70), 1.8, 1.0)\nreturns = returns * volatility_multiplier\nclose = 100 * np.exp(np.cumsum(returns))\n\n# Calculate Bollinger Bands (20-period SMA with 2 standard deviations)\nwindow = 20\nsma = pd.Series(close).rolling(window=window).mean().values\nstd = pd.Series(close).rolling(window=window).std().values\nupper_band = sma + 2 * std\nlower_band = sma - 2 * std\n\ndf = pd.DataFrame({\"date\": dates, \"close\": close, \"sma\": sma, \"upper_band\": upper_band, \"lower_band\": lower_band})\n\n# Remove NaN values from rolling calculations\ndf = df.dropna().reset_index(drop=True)\n\n# Create plot with legend using color aesthetic\nplot = (\n    ggplot(df)\n    # Bollinger Bands fill area (between upper and lower bands)\n    + geom_ribbon(\n        aes(x=\"date\", ymin=\"lower_band\", ymax=\"upper_band\", fill=\"Bollinger Bands\"), fill=BAND_COLOR, alpha=0.2\n    )\n    # Lower band line\n    + geom_line(aes(x=\"date\", y=\"lower_band\", color=\"Bollinger Bands\"), color=BAND_COLOR, size=1.0, alpha=0.7)\n    # Upper band line\n    + geom_line(aes(x=\"date\", y=\"upper_band\", color=\"Bollinger Bands\"), color=BAND_COLOR, size=1.0, alpha=0.7)\n    # Middle band (SMA) - dashed line\n    + geom_line(aes(x=\"date\", y=\"sma\", color=\"20-SMA\"), color=BAND_COLOR, size=1.2, linetype=\"dashed\", alpha=0.9)\n    # Close price line - prominent\n    + geom_line(aes(x=\"date\", y=\"close\", color=\"Close Price\"), color=BRAND, size=1.8)\n    # Labels\n    + labs(title=\"indicator-bollinger · letsplot · anyplot.ai\", x=\"Date\", y=\"Price (USD)\", color=\"\", fill=\"\")\n    # Theme and styling\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_line(color=INK, size=0.15),\n        panel_grid_minor=element_line(color=INK, size=0.1),\n        axis_title=element_text(size=20, color=INK),\n        axis_text=element_text(size=16, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n        plot_title=element_text(size=24, color=INK),\n    )\n    + ggsize(1600, 900)\n)\n\n# Save PNG (scale=3 gives 4800x2700)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\n\n# Save HTML for interactivity\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}