{"spec_id":"indicator-rsi","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// indicator-rsi: RSI Technical Indicator Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst LOOKBACK = 14;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny fixed-seed LCG — the browser has no seeded RNG.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst periods = 120;\nconst closes = [182.4];\nfor (let i = 1; i < periods; i++) {\n  const momentumSwing = Math.sin(i / 8) * 1.3;\n  const noise = (rand() - 0.5) * 2.4;\n  closes.push(Math.max(5, closes[i - 1] + momentumSwing + noise));\n}\n\n// Wilder's smoothed RSI — the standard 14-period lookback.\nfunction computeRSI(prices, lookback) {\n  const rsi = new Array(prices.length).fill(null);\n  let avgGain = 0;\n  let avgLoss = 0;\n  for (let i = 1; i <= lookback; i++) {\n    const change = prices[i] - prices[i - 1];\n    avgGain += Math.max(change, 0);\n    avgLoss += Math.max(-change, 0);\n  }\n  avgGain /= lookback;\n  avgLoss /= lookback;\n  rsi[lookback] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);\n  for (let i = lookback + 1; i < prices.length; i++) {\n    const change = prices[i] - prices[i - 1];\n    const gain = Math.max(change, 0);\n    const loss = Math.max(-change, 0);\n    avgGain = (avgGain * (lookback - 1) + gain) / lookback;\n    avgLoss = (avgLoss * (lookback - 1) + loss) / lookback;\n    rsi[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);\n  }\n  return rsi;\n}\n\nconst rsiByDay = computeRSI(closes, LOOKBACK);\n\nconst tradingDates = [];\nconst start = new Date(\"2024-01-02T00:00:00Z\");\nfor (let i = 0; i < periods; i++) {\n  const d = new Date(start);\n  d.setUTCDate(d.getUTCDate() + i);\n  tradingDates.push(d.toISOString().slice(0, 10));\n}\n\n// Only periods with a full 14-day lookback carry an RSI value.\nconst labels = tradingDates.slice(LOOKBACK);\nconst rsiSeries = rsiByDay.slice(LOOKBACK);\nconst constantLine = (value) => labels.map(() => value);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Zone shading plugin (core Chart.js plugin API, no external dependency) --\nconst rsiZonesPlugin = {\n  id: \"rsiZones\",\n  beforeDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const yScale = scales.y;\n    const shadeZone = (from, to, color) => {\n      const yTop = yScale.getPixelForValue(to);\n      const yBottom = yScale.getPixelForValue(from);\n      ctx.save();\n      ctx.fillStyle = color;\n      ctx.fillRect(chartArea.left, yTop, chartArea.right - chartArea.left, yBottom - yTop);\n      ctx.restore();\n    };\n    shadeZone(70, 100, \"rgba(174, 48, 48, 0.12)\"); // overbought — matte red tint\n    shadeZone(0, 30, \"rgba(0, 158, 115, 0.10)\"); // oversold — brand green tint\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    labels,\n    datasets: [\n      {\n        label: `RSI (${LOOKBACK})`,\n        data: rsiSeries,\n        borderColor: t.palette[0],\n        backgroundColor: t.palette[0],\n        borderWidth: 3,\n        pointRadius: 0,\n        tension: 0.15,\n        fill: false,\n      },\n      {\n        label: \"Overbought (70)\",\n        data: constantLine(70),\n        borderColor: \"#AE3030\",\n        borderWidth: 1.5,\n        borderDash: [8, 5],\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        label: \"Oversold (30)\",\n        data: constantLine(30),\n        borderColor: t.palette[0],\n        borderWidth: 1.5,\n        borderDash: [8, 5],\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        label: \"Centerline (50)\",\n        data: constantLine(50),\n        borderColor: t.inkSoft,\n        borderWidth: 1,\n        borderDash: [2, 4],\n        pointRadius: 0,\n        fill: false,\n      },\n    ],\n  },\n  plugins: [rsiZonesPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 8, right: 16, bottom: 4, left: 4 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"Daily RSI (14) · indicator-rsi · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          filter: (item) => item.text !== \"Centerline (50)\",\n          usePointStyle: true,\n          boxWidth: 10,\n        },\n      },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 13 }, maxTicksLimit: 10, autoSkip: true },\n        grid: { display: false },\n        title: { display: true, text: \"Trading Date\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        min: 0,\n        max: 100,\n        ticks: { color: t.inkSoft, font: { size: 13 }, stepSize: 10 },\n        grid: { color: t.grid },\n        title: { display: true, text: \"RSI\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}