{"spec_id":"indicator-bollinger","library":"muix","language":"javascript","code":"// anyplot.ai\n// indicator-bollinger: Bollinger Bands Indicator Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { Box, Typography } from \"@mui/material\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst [BRAND, SMA_COLOR, BAND_COLOR] = t.palette;\n\nconst hexToRgba = (hex, alpha) => {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n};\nconst BAND_FILL = hexToRgba(BAND_COLOR, 0.16);\n\n// --- Data (in-memory, deterministic) ----------------------------------------\nconst SYMBOL = \"MRDN\";\nconst N = 120;\nconst WINDOW = 20;\nconst START_PRICE = 162.5;\n\n// Fixed-seed LCG (Numerical Recipes constants) — the browser has no seeded RNG.\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\nconst 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\n// Trading dates (weekdays only)\nconst dates = [];\nconst cursor = new Date(2024, 0, 2);\nwhile (dates.length < N) {\n  const day = cursor.getDay();\n  if (day !== 0 && day !== 6) dates.push(new Date(cursor));\n  cursor.setDate(cursor.getDate() + 1);\n}\n\n// Daily close, with a calm \"squeeze\" regime and a high-volatility breakout\n// regime so the bands visibly narrow and then widen (see spec \"Notes\").\nconst close = [START_PRICE];\nfor (let i = 1; i < N; i += 1) {\n  const squeeze = i >= 40 && i < 65;\n  const breakout = i >= 90;\n  const dailyVolPct = squeeze ? 0.35 : breakout ? 1.7 : 0.9;\n  const driftPct = 0.05;\n  const changePct = driftPct + dailyVolPct * gaussian();\n  close.push(Math.max(20, close[i - 1] * (1 + changePct / 100)));\n}\n\nconst round2 = (v) => Math.round(v * 100) / 100;\n\n// Rolling 20-period SMA and population std dev -> upper/lower bands.\nconst sma = [];\nconst upperBand = [];\nconst lowerBand = [];\nconst bandWidth = [];\nfor (let i = 0; i < N; i += 1) {\n  if (i < WINDOW - 1) {\n    sma.push(null);\n    upperBand.push(null);\n    lowerBand.push(null);\n    bandWidth.push(null);\n    continue;\n  }\n  const windowSlice = close.slice(i - WINDOW + 1, i + 1);\n  const mean = windowSlice.reduce((sum, v) => sum + v, 0) / WINDOW;\n  const variance =\n    windowSlice.reduce((sum, v) => sum + (v - mean) ** 2, 0) / WINDOW;\n  const stdDev = Math.sqrt(variance);\n  const upper = round2(mean + 2 * stdDev);\n  const lower = round2(mean - 2 * stdDev);\n  sma.push(round2(mean));\n  upperBand.push(upper);\n  lowerBand.push(lower);\n  bandWidth.push(round2(upper - lower));\n}\nconst closeRounded = close.map(round2);\n\n// Explicit y-axis bounds: the stacked band series' domain runs from 0 (its\n// hidden base) to the upper band, which would force the axis to include 0\n// and waste most of the canvas on empty space above/below the real range.\nconst allValues = [...closeRounded, ...sma, ...upperBand, ...lowerBand].filter(\n  (v) => v !== null,\n);\nconst dataMin = Math.min(...allValues);\nconst dataMax = Math.max(...allValues);\nconst axisPadding = (dataMax - dataMin) * 0.15;\nconst yMin = Math.floor((dataMin - axisPadding) / 5) * 5;\nconst yMax = Math.ceil((dataMax + axisPadding) / 5) * 5;\n\nconst TITLE = `${SYMBOL} · indicator-bollinger · javascript · muix · anyplot.ai`;\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const titleHeight = 64;\n\n  return (\n    <Box sx={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <Typography\n        color=\"text.primary\"\n        sx={{\n          height: titleHeight,\n          lineHeight: `${titleHeight}px`,\n          pl: 1,\n          fontSize: 22,\n          fontWeight: 600,\n        }}\n      >\n        {TITLE}\n      </Typography>\n      <LineChart\n        width={width}\n        height={height - titleHeight}\n        skipAnimation\n        margin={{ top: 72, right: 48, bottom: 76, left: 132 }}\n        xAxis={[\n          {\n            data: dates,\n            scaleType: \"time\",\n            label: \"Trading Date\",\n            labelStyle: { fontSize: 16 },\n            tickLabelStyle: { fontSize: 14 },\n            valueFormatter: (date) =>\n              date.toLocaleDateString(\"en-US\", {\n                month: \"short\",\n                day: \"numeric\",\n              }),\n          },\n        ]}\n        yAxis={[\n          {\n            label: \"Price (USD)\",\n            labelStyle: { fontSize: 16 },\n            // tickFontSize pushes the rotated axis title clear of the \"$NNN\"\n            // tick labels — the actual tick text size is set via tickLabelStyle.\n            tickFontSize: 40,\n            tickLabelStyle: { fontSize: 14 },\n            valueFormatter: (v) => `$${v.toFixed(0)}`,\n            min: yMin,\n            max: yMax,\n          },\n        ]}\n        series={[\n          {\n            id: \"bandBase\",\n            data: lowerBand,\n            area: true,\n            stack: \"band\",\n            color: BAND_COLOR,\n            showMark: false,\n            connectNulls: false,\n            curve: \"monotoneX\",\n          },\n          {\n            id: \"bandWidth\",\n            data: bandWidth,\n            area: true,\n            stack: \"band\",\n            color: BAND_COLOR,\n            showMark: false,\n            connectNulls: false,\n            curve: \"monotoneX\",\n            label: \"Bollinger Band (SMA ± 2σ)\",\n          },\n          {\n            id: \"sma\",\n            data: sma,\n            color: SMA_COLOR,\n            showMark: false,\n            connectNulls: false,\n            curve: \"monotoneX\",\n            label: \"20-Day SMA\",\n          },\n          {\n            id: \"close\",\n            data: closeRounded,\n            color: BRAND,\n            showMark: false,\n            curve: \"monotoneX\",\n            label: `${SYMBOL} Close`,\n          },\n        ]}\n        grid={{ horizontal: true }}\n        slotProps={{\n          legend: {\n            direction: \"row\",\n            position: { vertical: \"top\", horizontal: \"middle\" },\n            labelStyle: { fontSize: 14 },\n          },\n        }}\n        sx={{\n          \"& .MuiLineElement-root\": { strokeWidth: 2 },\n          \"& .MuiLineElement-series-close\": { strokeWidth: 3.5 },\n          \"& .MuiLineElement-series-sma\": {\n            strokeWidth: 2.25,\n            strokeDasharray: \"10 6\",\n          },\n          \"& .MuiLineElement-series-bandBase\": {\n            strokeWidth: 1.5,\n            strokeOpacity: 0.6,\n          },\n          \"& .MuiLineElement-series-bandWidth\": {\n            strokeWidth: 1.5,\n            strokeOpacity: 0.6,\n          },\n          \"& .MuiAreaElement-series-bandBase\": { fill: \"none\" },\n          \"& .MuiAreaElement-series-bandWidth\": { fill: BAND_FILL },\n        }}\n      />\n    </Box>\n  );\n}\n"}