{"spec_id":"indicator-bollinger","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// indicator-bollinger: Bollinger Bands Indicator Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny LCG so the random walk is reproducible without a browser RNG.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 16807) % 2147483647;\n  return (seed - 1) / 2147483646;\n}\nfunction randNormal() {\n  const u1 = Math.max(lcg(), 1e-9);\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst period = 20;\nconst rawDays = 139; // trimmed to 120 trading days once the SMA window fills\n\nconst closes = [180];\nfor (let i = 1; i < rawDays; i++) {\n  closes.push(closes[i - 1] + randNormal() * 1.5);\n}\n\nconst timestamps = [];\nlet cursor = Date.UTC(2024, 0, 2);\nfor (let i = 0; i < rawDays; i++) {\n  const day = new Date(cursor).getUTCDay();\n  if (day === 6) cursor += 2 * 86400000;\n  else if (day === 0) cursor += 86400000;\n  timestamps.push(cursor);\n  cursor += 86400000;\n}\n\nconst closeData = [];\nconst smaData = [];\nconst upperData = [];\nconst lowerData = [];\nfor (let i = period - 1; i < rawDays; i++) {\n  const window_ = closes.slice(i - period + 1, i + 1);\n  const mean = window_.reduce((a, b) => a + b, 0) / period;\n  const variance = window_.reduce((a, b) => a + (b - mean) ** 2, 0) / period;\n  const std = Math.sqrt(variance);\n  const x = timestamps[i];\n  closeData.push([x, Math.round(closes[i] * 100) / 100]);\n  smaData.push([x, Math.round(mean * 100) / 100]);\n  upperData.push([x, Math.round((mean + 2 * std) * 100) / 100]);\n  lowerData.push([x, Math.round((mean - 2 * std) * 100) / 100]);\n}\n\n// Area series default to a zero threshold, which would force the y-axis down\n// to $0 and waste most of the canvas on empty space. Pin explicit bounds from\n// the actual data range instead — standard practice for price charts.\nconst yMin = Math.min(...lowerData.map((d) => d[1]));\nconst yMax = Math.max(...upperData.map((d) => d[1]));\nconst yPad = (yMax - yMin) * 0.1;\n\n// Find the single most extreme band touch (closest approach of price to\n// either envelope edge) to call out as an overbought/oversold focal point.\nlet touchIdx = 0;\nlet touchType = \"upper\";\nlet minGap = Infinity;\nfor (let i = 0; i < closeData.length; i++) {\n  const gapUpper = upperData[i][1] - closeData[i][1];\n  const gapLower = closeData[i][1] - lowerData[i][1];\n  if (gapUpper < minGap) {\n    minGap = gapUpper;\n    touchIdx = i;\n    touchType = \"upper\";\n  }\n  if (gapLower < minGap) {\n    minGap = gapLower;\n    touchIdx = i;\n    touchType = \"lower\";\n  }\n}\nconst touchPoint = closeData[touchIdx];\nconst touchLabel = touchType === \"upper\" ? \"Overbought touch\" : \"Oversold touch\";\n\n// --- Chart -------------------------------------------------------------------\n// Bollinger fill without the arearange module: an upper \"area\" (tinted, fills\n// toward the axis floor) drawn under a lower \"area\" filled in the page\n// background color, which masks everything below the lower band — leaving\n// only the band between the two lines visibly tinted.\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"indicator-bollinger · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"20-day SMA · ±2σ bands\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    type: \"datetime\",\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Price (USD)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" }, format: \"${value}\" },\n    min: Math.floor(yMin - yPad),\n    max: Math.ceil(yMax + yPad),\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: { shared: true, valueDecimals: 2, valuePrefix: \"$\" },\n  plotOptions: {\n    series: { animation: false, marker: { enabled: false } },\n  },\n  series: [\n    {\n      name: \"Bollinger Bands (±2σ)\",\n      type: \"area\",\n      data: upperData,\n      color: t.palette[1],\n      fillOpacity: 0.15,\n      lineWidth: 1,\n    },\n    {\n      name: \"Lower Band (-2σ)\",\n      type: \"area\",\n      data: lowerData,\n      color: t.palette[1],\n      fillColor: t.pageBg,\n      fillOpacity: 1,\n      lineWidth: 1,\n      showInLegend: false,\n    },\n    {\n      name: \"SMA (20)\",\n      type: \"line\",\n      data: smaData,\n      color: t.ink,\n      dashStyle: \"ShortDash\",\n      lineWidth: 2,\n    },\n    {\n      name: \"Close\",\n      type: \"line\",\n      data: closeData,\n      color: t.palette[0],\n      lineWidth: 3,\n    },\n    {\n      name: touchLabel,\n      type: \"scatter\",\n      data: [touchPoint],\n      color: t.amber,\n      marker: {\n        enabled: true,\n        symbol: \"diamond\",\n        radius: 8,\n        lineWidth: 2,\n        lineColor: t.ink,\n      },\n      dataLabels: {\n        enabled: true,\n        format: touchLabel,\n        y: touchType === \"upper\" ? -16 : 22,\n        style: { color: t.ink, fontSize: \"13px\", fontWeight: \"600\", textOutline: \"none\" },\n      },\n      showInLegend: false,\n      enableMouseTracking: false,\n    },\n  ],\n});\n"}