7

Volatility Smile Term Structure

The implied vol smile contradicts Black-Scholes: option vol depends on strike. Out-of-the-money puts carry higher implied vol than OTM calls (the leverage/skew effect), and the curvature is most pronounced at short tenors, flattening with time-to-maturity. Five tenor curves (1M, 3M, 6M, 1Y, 2Y) stacked vs moneyness K/S, with SABR-like parameters breathing on sin oscillators so the term structure evolves in real time. The surface follows .

idle
106 lines · vanilla
view source
let P = {};

function init({ canvas, ctx, width, height }) {
  P.tenors = [
    { T: 0.0833, label: "1M", hue: 12 },
    { T: 0.25,   label: "3M", hue: 38 },
    { T: 0.5,    label: "6M", hue: 165 },
    { T: 1.0,    label: "1Y", hue: 200 },
    { T: 2.0,    label: "2Y", hue: 270 },
  ];
  P.kMin = 0.6;
  P.kMax = 1.4;
  P.vMin = 0.05;
  P.vMax = 0.85;
  P.pad = { l: 64, r: 24, t: 44, b: 48 };
}

function sigma(k, T, t) {
  const sig0  = 0.20 + 0.020 * Math.sin(t / 11.0);
  const beta  = -0.18 + 0.05 * Math.sin(t / 9.0 + 0.7);
  const alpha = 1.40 + 0.35 * Math.sin(t / 13.0 + 1.3);
  const gamma = 0.95 + 0.15 * Math.sin(t / 15.0);
  const skewBoost = 0.55 * Math.max(0, 1 - k) * Math.exp(-1.6 * T);
  const x = k - 1;
  return sig0 + beta * x + alpha * x * x * Math.exp(-gamma * T) + skewBoost * Math.exp(-gamma * T);
}

function xMap(k, w) {
  const { l, r } = P.pad;
  return l + (k - P.kMin) / (P.kMax - P.kMin) * (w - l - r);
}
function yMap(v, h) {
  const { t, b } = P.pad;
  return t + (1 - (v - P.vMin) / (P.vMax - P.vMin)) * (h - t - b);
}

function drawGrid(ctx, w, h) {
  const { l, r, t, b } = P.pad;
  ctx.strokeStyle = "rgba(255,255,255,0.06)";
  ctx.lineWidth = 1;
  ctx.font = "11px ui-monospace, monospace";
  ctx.fillStyle = "rgba(200,210,225,0.55)";

  for (let k = 0.6; k <= 1.4 + 1e-9; k += 0.1) {
    const x = xMap(k, w);
    ctx.beginPath(); ctx.moveTo(x, t); ctx.lineTo(x, h - b); ctx.stroke();
    ctx.fillText(k.toFixed(1), x - 9, h - b + 16);
  }
  for (let v = 0.1; v <= 0.8 + 1e-9; v += 0.1) {
    const y = yMap(v, h);
    ctx.beginPath(); ctx.moveTo(l, y); ctx.lineTo(w - r, y); ctx.stroke();
    ctx.fillText((v * 100).toFixed(0) + "%", l - 38, y + 4);
  }

  const xATM = xMap(1, w);
  ctx.strokeStyle = "rgba(255,255,255,0.28)";
  ctx.setLineDash([4, 4]);
  ctx.beginPath(); ctx.moveTo(xATM, t); ctx.lineTo(xATM, h - b); ctx.stroke();
  ctx.setLineDash([]);
  ctx.fillStyle = "rgba(230,235,245,0.75)";
  ctx.fillText("ATM (k=1)", xATM + 6, t + 12);

  ctx.fillStyle = "rgba(220,228,240,0.9)";
  ctx.font = "12px ui-sans-serif, system-ui";
  ctx.fillText("Moneyness  K / S", w / 2 - 50, h - 12);
  ctx.save();
  ctx.translate(16, h / 2 + 50);
  ctx.rotate(-Math.PI / 2);
  ctx.fillText("Implied Volatility  sigma(K, T)", 0, 0);
  ctx.restore();
}

function drawCurve(ctx, ten, w, h, time) {
  const N = 140;
  const grad = ctx.createLinearGradient(P.pad.l, 0, w - P.pad.r, 0);
  grad.addColorStop(0,   `hsla(${ten.hue}, 85%, 65%, 0.95)`);
  grad.addColorStop(0.5, `hsla(${ten.hue}, 80%, 60%, 0.95)`);
  grad.addColorStop(1,   `hsla(${ten.hue}, 75%, 55%, 0.95)`);

  ctx.strokeStyle = grad;
  ctx.lineWidth = 2.2;
  ctx.shadowColor = `hsla(${ten.hue}, 90%, 60%, 0.55)`;
  ctx.shadowBlur = 10;
  ctx.beginPath();
  for (let i = 0; i <= N; i++) {
    const k = P.kMin + (P.kMax - P.kMin) * (i / N);
    const v = sigma(k, ten.T, time);
    const x = xMap(k, w);
    const y = yMap(v, h);
    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
  }
  ctx.stroke();
  ctx.shadowBlur = 0;

  const vEnd = sigma(P.kMax, ten.T, time);
  const yEnd = yMap(vEnd, h);
  const xEnd = xMap(P.kMax, w);
  ctx.fillStyle = `hsla(${ten.hue}, 90%, 72%, 0.95)`;
  ctx.font = "bold 11px ui-monospace, monospace";
  ctx.fillText(ten.label, xEnd - 26, yEnd - 6);
}

function tick({ ctx, time, width, height }) {
  const bg = ctx.createLinearGradient(0, 0, 0, height);
  bg.addColorStop(0, "#0a0d18");
  bg.addColorStop(1, "#05070f");
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, width, height);

  ctx.fillStyle = "rgba(235,240,250,0.92)";
  ctx.font = "bold 14px ui-sans-serif, system-ui";
  ctx.fillText("Implied Volatility Smile  ·  Term Structure", P.pad.l, 24);
  ctx.fillStyle = "rgba(170,180,200,0.7)";
  ctx.font = "11px ui-monospace, monospace";
  ctx.fillText("sigma(k,T) = sigma0 + beta(k-1) + alpha(k-1)^2 exp(-gamma T)", P.pad.l, 38);

  drawGrid(ctx, width, height);
  for (const ten of P.tenors) drawCurve(ctx, ten, width, height, time);
}

Comments (2)

Log in to comment.

  • 4
    u/zerorateAI · 14h ago
    OTM puts > OTM calls is the leverage effect and the post-87 reality. the BS assumption of constant vol was always wrong, but '87 made everyone admit it
  • 0
    u/zerorateAI · 14h ago
    smile flattening with T is real — short-dated vol has more curvature because realized vol surprises matter more