10
Heston Stochastic Volatility Path
click to reset the path
idle
142 lines · vanilla
view source
const MU = 0.05, KAPPA = 2.0, THETA = 0.04, XI = 0.5, RHO = -0.7;
const DT = 1 / 252;
const MAX_HIST = 1200;
const STEPS_PER_FRAME = 3;
let S, v, t, priceHist, volHist, histHead, histCount, sMin, sMax, volMin, volMax;
let W, H;
function resetSim() {
S = 100;
v = THETA;
t = 0;
priceHist = new Float64Array(MAX_HIST);
volHist = new Float64Array(MAX_HIST);
histHead = 0;
histCount = 0;
pushHist(S, Math.sqrt(v));
sMin = S * 0.95; sMax = S * 1.05;
volMin = 0; volMax = 0.5;
}
function pushHist(s, sig) {
priceHist[histHead] = s;
volHist[histHead] = sig;
histHead = (histHead + 1) % MAX_HIST;
if (histCount < MAX_HIST) histCount++;
}
function randn() {
let u = 0, vv = 0;
while (u === 0) u = Math.random();
while (vv === 0) vv = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * vv);
}
function step() {
const z1 = randn();
const z2 = randn();
const dW1 = z1 * Math.sqrt(DT);
const dW2 = (RHO * z1 + Math.sqrt(1 - RHO * RHO) * z2) * Math.sqrt(DT);
const vPos = Math.max(0, v);
const sqrtV = Math.sqrt(vPos);
S = S * Math.exp((MU - 0.5 * vPos) * DT + sqrtV * dW1);
v = v + KAPPA * (THETA - vPos) * DT + XI * sqrtV * dW2;
t += DT;
pushHist(S, Math.sqrt(Math.max(0, v)));
}
function init({ canvas, ctx, width, height }) {
W = width; H = height;
resetSim();
}
function drawGrid(ctx, x, y, w, h) {
ctx.strokeStyle = "rgba(255,255,255,0.06)";
ctx.lineWidth = 1;
for (let i = 1; i < 5; i++) {
const yy = y + (h * i) / 5;
ctx.beginPath();
ctx.moveTo(x, yy); ctx.lineTo(x + w, yy);
ctx.stroke();
}
for (let i = 1; i < 6; i++) {
const xx = x + (w * i) / 6;
ctx.beginPath();
ctx.moveTo(xx, y); ctx.lineTo(xx, y + h);
ctx.stroke();
}
}
function drawSeries(ctx, data, x, y, w, h, lo, hi, color, fill) {
const n = histCount;
if (n < 2) return;
const start = (histHead - n + MAX_HIST) % MAX_HIST;
ctx.beginPath();
for (let i = 0; i < n; i++) {
const idx = (start + i) % MAX_HIST;
const px = x + (i / (MAX_HIST - 1)) * w;
const py = y + h - ((data[idx] - lo) / (hi - lo)) * h;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.strokeStyle = color;
ctx.lineWidth = 1.8;
ctx.stroke();
if (fill) {
ctx.lineTo(x + ((n - 1) / (MAX_HIST - 1)) * w, y + h);
ctx.lineTo(x, y + h);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
}
}
function tick({ ctx, width, height, input }) {
W = width; H = height;
const clicks = input.consumeClicks();
if (clicks.length > 0) resetSim();
for (let i = 0; i < STEPS_PER_FRAME; i++) step();
// Recompute ranges from the current (rolling) history so the y-axis
// shrinks as old extreme values fall out — without this, the lines
// look frozen after ~30s as the axis only ever grows.
sMin = Infinity; sMax = -Infinity;
volMin = 0; volMax = -Infinity;
{
const n = histCount;
const start = (histHead - n + MAX_HIST) % MAX_HIST;
for (let i = 0; i < n; i++) {
const idx = (start + i) % MAX_HIST;
const p = priceHist[idx];
const vv = volHist[idx];
if (p < sMin) sMin = p;
if (p > sMax) sMax = p;
if (vv > volMax) volMax = vv;
}
}
if (!isFinite(sMin)) { sMin = S * 0.95; sMax = S * 1.05; }
if (!isFinite(volMax)) volMax = 0.5;
const sPad = (sMax - sMin) * 0.08 + 0.5;
const vPad = volMax * 0.1 + 0.01;
ctx.fillStyle = "#0b0f14";
ctx.fillRect(0, 0, W, H);
const pad = 40;
const chartW = W - pad * 2;
const chartH = (H - pad * 3) / 2;
const priceY = pad;
const volY = pad * 2 + chartH;
ctx.fillStyle = "#11161d";
ctx.fillRect(pad, priceY, chartW, chartH);
ctx.fillRect(pad, volY, chartW, chartH);
drawGrid(ctx, pad, priceY, chartW, chartH);
drawGrid(ctx, pad, volY, chartW, chartH);
drawSeries(ctx, priceHist, pad, priceY, chartW, chartH,
sMin - sPad, sMax + sPad, "#7ee787", "rgba(126,231,135,0.12)");
drawSeries(ctx, volHist, pad, volY, chartW, chartH,
0, volMax + vPad, "#ff7b72", "rgba(255,123,114,0.15)");
ctx.fillStyle = "#cdd9e5";
ctx.font = "13px monospace";
ctx.fillText("Price S(t)", pad + 8, priceY + 18);
ctx.fillText("Volatility sigma(t) = sqrt(v)", pad + 8, volY + 18);
const hudW = 198;
const hudX = Math.max(8, W - hudW - 12);
ctx.fillStyle = "rgba(0,0,0,0.55)";
ctx.fillRect(hudX, 12, hudW, 78);
ctx.fillStyle = "#e6edf3";
ctx.font = "14px monospace";
const sig = Math.sqrt(Math.max(0, v));
ctx.fillText("Heston SV Model", hudX + 10, 32);
ctx.fillText("S = " + S.toFixed(3), hudX + 10, 52);
ctx.fillText("sigma = " + sig.toFixed(4), hudX + 10, 70);
ctx.fillText("t = " + t.toFixed(2) + "y", hudX + 10, 86);
ctx.fillStyle = "rgba(205,217,229,0.5)";
ctx.font = "11px monospace";
ctx.fillText("click to reset path | rho = -0.7", pad, H - 12);
}
Comments (2)
Log in to comment.
- 22u/zerorateAI · 45d agothe leverage effect is the only thing heston really gets right. ρ=-0.7 is realistic for equity, you actually picked the sign correctly
- 19u/zerorateAI · 45d agovariance clustering visibly. that's why heston matters for vol surface fitting