{"spec_id":"indicator-bollinger","library":"echarts","language":"javascript","code":"// anyplot.ai\n// indicator-bollinger: Bollinger Bands Indicator Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Aurora Biotech (ABTX) daily close over 120 trading days, with a 20-period\n// SMA and +-2 standard-deviation Bollinger Bands.\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return function next() {\n    state = (1103515245 * state + 12345) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(20240102);\nfunction gaussian() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst NUM_DAYS = 120;\nconst PERIOD = 20;\nconst NUM_STD_DEV = 2;\nconst MONTH_NAMES = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n];\n\nconst dates = [];\nconst closes = [];\nlet cursor = new Date(Date.UTC(2024, 0, 2));\nlet price = 84;\nwhile (dates.length < NUM_DAYS) {\n  const day = cursor.getUTCDay();\n  if (day !== 0 && day !== 6) {\n    dates.push(`${MONTH_NAMES[cursor.getUTCMonth()]} ${cursor.getUTCDate()}`);\n    price += gaussian() * 1.1 + 0.05;\n    closes.push(Math.max(price, 5));\n  }\n  cursor.setUTCDate(cursor.getUTCDate() + 1);\n}\n\nconst sma = new Array(NUM_DAYS).fill(null);\nconst upperBand = new Array(NUM_DAYS).fill(null);\nconst lowerBand = new Array(NUM_DAYS).fill(null);\nconst bandSpread = new Array(NUM_DAYS).fill(null);\nfor (let i = PERIOD - 1; i < NUM_DAYS; i += 1) {\n  const window = closes.slice(i - PERIOD + 1, i + 1);\n  const mean = window.reduce((sum, v) => sum + v, 0) / PERIOD;\n  const variance = window.reduce((sum, v) => sum + (v - mean) ** 2, 0) / PERIOD;\n  const stdDev = Math.sqrt(variance);\n  sma[i] = mean;\n  upperBand[i] = mean + NUM_STD_DEV * stdDev;\n  lowerBand[i] = mean - NUM_STD_DEV * stdDev;\n  bandSpread[i] = upperBand[i] - lowerBand[i];\n}\n\nconst bandColor = t.palette[1];\n\n// Y-axis auto-scale: pad a few dollars beyond the close/band extremes so the\n// squeeze/expansion pattern fills the canvas instead of sitting in a $0-$100 strip.\nconst spanValues = [];\nfor (let i = 0; i < NUM_DAYS; i += 1) {\n  spanValues.push(closes[i]);\n  if (upperBand[i] !== null) spanValues.push(upperBand[i]);\n  if (lowerBand[i] !== null) spanValues.push(lowerBand[i]);\n}\nconst dataMin = Math.min(...spanValues);\nconst dataMax = Math.max(...spanValues);\nconst yPad = (dataMax - dataMin) * 0.1;\nconst yMin = Math.floor(dataMin - yPad);\nconst yMax = Math.ceil(dataMax + yPad);\n\n// Locate the tightest squeeze (minimum band spread) to annotate the key\n// Bollinger pattern instead of leaving the story purely implicit in color.\n// Search starts one full period after warm-up so the low-variance edge right\n// at the SMA's first valid bar isn't mistaken for a genuine volatility squeeze.\nlet squeezeIndex = -1;\nlet minSpread = Infinity;\nfor (let i = PERIOD * 2; i < NUM_DAYS; i += 1) {\n  if (bandSpread[i] !== null && bandSpread[i] < minSpread) {\n    minSpread = bandSpread[i];\n    squeezeIndex = i;\n  }\n}\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"indicator-bollinger · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  legend: {\n    data: [\"Close\", \"SMA (20)\", \"Upper Band\", \"Lower Band\"],\n    top: 60,\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  grid: { left: 90, right: 60, top: 140, bottom: 80 },\n  tooltip: {\n    trigger: \"axis\",\n    formatter: (params) =>\n      params\n        .filter((p) => !p.seriesName.includes(\"band fill\"))\n        .map(\n          (p) =>\n            `${p.marker} ${p.seriesName}: $${p.value == null ? \"-\" : p.value.toFixed(2)}`,\n        )\n        .join(\"<br/>\"),\n  },\n  xAxis: {\n    type: \"category\",\n    data: dates,\n    boundaryGap: false,\n    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 9 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Price (USD)\",\n    nameTextStyle: { color: t.ink, fontSize: 14 },\n    min: yMin,\n    max: yMax,\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: \"${value}\" },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Lower Band (band fill)\",\n      type: \"line\",\n      data: lowerBand,\n      stack: \"band\",\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      areaStyle: { opacity: 0 },\n      z: 1,\n    },\n    {\n      name: \"Band Spread (band fill)\",\n      type: \"line\",\n      data: bandSpread,\n      stack: \"band\",\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      areaStyle: { color: bandColor, opacity: 0.15 },\n      z: 1,\n    },\n    {\n      name: \"Upper Band\",\n      type: \"line\",\n      data: upperBand,\n      symbol: \"none\",\n      itemStyle: { color: bandColor },\n      lineStyle: {\n        color: bandColor,\n        width: 1.5,\n        type: \"dashed\",\n        opacity: 0.75,\n      },\n      z: 2,\n    },\n    {\n      name: \"Lower Band\",\n      type: \"line\",\n      data: lowerBand,\n      symbol: \"none\",\n      itemStyle: { color: bandColor },\n      lineStyle: {\n        color: bandColor,\n        width: 1.5,\n        type: \"dashed\",\n        opacity: 0.75,\n      },\n      z: 2,\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1 },\n        label: {\n          formatter: \"Tightest squeeze\",\n          color: t.inkSoft,\n          fontSize: 12,\n          position: \"insideEndTop\",\n        },\n        data: [{ xAxis: dates[squeezeIndex] }],\n      },\n    },\n    {\n      name: \"SMA (20)\",\n      type: \"line\",\n      data: sma,\n      symbol: \"none\",\n      itemStyle: { color: t.ink },\n      lineStyle: { color: t.ink, width: 2, type: \"dotted\" },\n      z: 3,\n    },\n    {\n      name: \"Close\",\n      type: \"line\",\n      data: closes,\n      symbol: \"none\",\n      itemStyle: { color: t.palette[0] },\n      lineStyle: { color: t.palette[0], width: 3 },\n      z: 4,\n    },\n  ],\n});\n"}