The simulator below prices a European call option using the Black-Scholes formula and computes its Greeks. Drag the stock price slider to simulate the underlying drifting through the day.
<!-- Option Greeks & Delta-Hedging demo -->
<div class="controls">
<label>Stock price $ <span id="s-val">100.00</span></label>
<input type="range" id="s-slider" min="60" max="140" step="0.5" value="100">
<label>Implied vol <span id="vol-val">20</span>%</label>
<input type="range" id="vol-slider" min="5" max="80" step="1" value="20">
<label>Days to expiry <span id="t-val">30</span></label>
<input type="range" id="t-slider" min="1" max="90" step="1" value="30">
</div>
<table id="greeks-table">
<thead>
<tr>
<th>Greek</th>
<th>Value</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr><td>Price (C)</td><td id="c-price">—</td><td id="c-price-desc">—</td></tr>
<tr><td>Delta (Δ)</td><td id="c-delta">—</td><td id="c-delta-desc">—</td></tr>
<tr><td>Gamma (Γ)</td><td id="c-gamma">—</td><td id="c-gamma-desc">—</td></tr>
<tr><td>Vega (𝒱)</td><td id="c-vega">—</td><td id="c-vega-desc">—</td></tr>
</tbody>
</table>
<div class="hedge-row">
<span>Current hedge:</span>
<span id="hedge-shares">—</span>
<span>shares held</span>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-rebalance" type="button">Re-hedge now</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* base reset */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; align-items: center; margin-bottom: .8rem; }
.controls label { white-space: nowrap; font-weight: 600; font-size: .85rem; }
input[type=range] { width: 100%; }
table { width: 100%; border-collapse: collapse; margin-bottom: .7rem; }
th { background: #1d3557; color: #fff; padding: 5px 8px; text-align: left; font-size: .82rem; }
td { padding: 4px 8px; border-bottom: 1px solid #dee2e6; font-size: .85rem; }
td:first-child { font-weight: 700; font-family: ui-monospace, monospace; }
tr:nth-child(even) td { background: #f4f7fa; }
.hedge-row { font-size: .9rem; margin-bottom: .5rem; }
.hedge-row span:nth-child(2) { font-weight: 700; font-size: 1rem; margin: 0 .3rem; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.gain { color: #0a7d33; }
.status.loss { color: #c92f3c; }
.status.neutral { color: #555; }
.btns { display: flex; gap: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Black-Scholes European call pricer + Greeks calculator.
// Model: log-normal stock, constant vol & rate, no dividends.
// Abramowitz & Stegun approximation for the standard normal CDF.
function normCDF(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const poly = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const pdf = Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
const result = 1 - pdf * poly;
return x >= 0 ? result : 1 - result;
}
// Standard normal PDF.
function normPDF(x) { return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI); }
// Compute price, delta, gamma and vega for a European call.
// Inputs: S=stock price, K=strike, r=risk-free rate, T=time(years), sig=volatility
function bsGreeks(S, K, r, T, sig) {
if (T <= 0) {
const intrinsic = Math.max(S - K, 0);
return { price: intrinsic, delta: S > K ? 1 : 0, gamma: 0, vega: 0, d1: 0, d2: 0 };
}
const sqrtT = Math.sqrt(T);
const d1 = (Math.log(S / K) + (r + 0.5 * sig * sig) * T) / (sig * sqrtT);
const d2 = d1 - sig * sqrtT;
const price = S * normCDF(d1) - K * Math.exp(-r * T) * normCDF(d2);
const delta = normCDF(d1);
const gamma = normPDF(d1) / (S * sig * sqrtT);
const vega = S * normPDF(d1) * sqrtT / 100; // per 1% move in vol
return { price, delta, gamma, vega, d1, d2 };
}
// Fixed parameters: strike = $100, risk-free rate = 5%
const K = 100, r = 0.05;
// Hedge state: prevS = last rebalanced stock price, hedgeShares = current hedge ratio
let prevS = null, prevDelta = null, hedgeShares = null, hedgeValue = null, pnl = 0;
const slider = document.getElementById('s-slider');
const volSlider = document.getElementById('vol-slider');
const tSlider = document.getElementById('t-slider');
const sValEl = document.getElementById('s-val');
const volValEl = document.getElementById('vol-val');
const tValEl = document.getElementById('t-val');
function getParams() {
return {
S: parseFloat(slider.value),
sig: parseFloat(volSlider.value) / 100,
T: parseInt(tSlider.value) / 365
};
}
function update() {
const { S, sig, T } = getParams();
sValEl.textContent = S.toFixed(2);
volValEl.textContent = (sig * 100).toFixed(0);
tValEl.textContent = Math.round(T * 365);
const g = bsGreeks(S, K, r, T, sig);
document.getElementById('c-price').textContent = '
Watch what happens: as the stock moves, delta changes (gamma is at work), and the hedge — the number of shares you must hold to stay neutral — must be rebalanced. The P&L column shows how much you gain or lose between rebalances; without hedging, every tick is an unmanaged exposure.
+ g.price.toFixed(3);
document.getElementById('c-delta').textContent = g.delta.toFixed(4);
document.getElementById('c-gamma').textContent = g.gamma.toFixed(5);
document.getElementById('c-vega').textContent = '
Watch what happens: as the stock moves, delta changes (gamma is at work), and the hedge — the number of shares you must hold to stay neutral — must be rebalanced. The P&L column shows how much you gain or lose between rebalances; without hedging, every tick is an unmanaged exposure.
+ g.vega.toFixed(4);
document.getElementById('c-price-desc').textContent = i18nFmt('Fair value of the call with strike $%K%', { K: K });
document.getElementById('c-delta-desc').textContent = i18nFmt('Hold %shares% shares to be neutral right now', { shares: g.delta.toFixed(2) });
document.getElementById('c-gamma-desc').textContent = i18nFmt('Delta drifts by %gamma% per $1 move', { gamma: g.gamma.toFixed(5) });
document.getElementById('c-vega-desc').textContent = i18nFmt('Cost per 1% change in implied vol', {});
// Show currently held shares if hedged, else show current delta as suggestion
if (hedgeShares !== null) {
document.getElementById('hedge-shares').textContent = hedgeShares.toFixed(4);
} else {
document.getElementById('hedge-shares').textContent = g.delta.toFixed(4);
}
// Compute net P&L since last rebalance: option gain + hedge gain
const statusEl = document.getElementById('status');
if (hedgeShares !== null && prevS !== null && S !== prevS) {
const optionPnL = g.price - bsGreeks(prevS, K, r, T, sig).price;
const hedgePnL = hedgeShares * (S - prevS);
const netPnL = optionPnL + hedgePnL;
const sign = netPnL >= 0 ? '+' : '';
statusEl.textContent = i18nFmt('Net P&L since last rebalance: %net%', { net: sign + netPnL.toFixed(4) });
statusEl.className = 'status ' + (Math.abs(netPnL) < 0.005 ? 'neutral' : netPnL > 0 ? 'gain' : 'loss');
} else if (hedgeShares === null) {
statusEl.textContent = 'Press Re-hedge to lock a hedge, then move the price.';
statusEl.className = 'status neutral';
}
}
function rebalance() {
const { S, sig, T } = getParams();
const g = bsGreeks(S, K, r, T, sig);
prevS = S;
prevDelta = g.delta;
hedgeShares = g.delta;
document.getElementById('hedge-shares').textContent = hedgeShares.toFixed(4);
const statusEl = document.getElementById('status');
statusEl.textContent = i18nFmt('Hedged at delta = %delta%. Now move the stock price.', { delta: g.delta.toFixed(4) });
statusEl.className = 'status neutral';
}
function reset() {
prevS = null; prevDelta = null; hedgeShares = null; hedgeValue = null; pnl = 0;
slider.value = 100; volSlider.value = 20; tSlider.value = 30;
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
update();
}
slider.addEventListener('input', update);
volSlider.addEventListener('input', update);
tSlider.addEventListener('input', update);
document.getElementById('btn-rebalance').addEventListener('click', rebalance);
document.getElementById('btn-reset').addEventListener('click', reset);
update();
<!-- Option Greeks & Delta-Hedging demo -->
<div class="controls">
<label>Stock price $ <span id="s-val">100.00</span></label>
<input type="range" id="s-slider" min="60" max="140" step="0.5" value="100">
<label>Implied vol <span id="vol-val">20</span>%</label>
<input type="range" id="vol-slider" min="5" max="80" step="1" value="20">
<label>Days to expiry <span id="t-val">30</span></label>
<input type="range" id="t-slider" min="1" max="90" step="1" value="30">
</div>
<table id="greeks-table">
<thead>
<tr>
<th>Greek</th>
<th>Value</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr><td>Price (C)</td><td id="c-price">—</td><td id="c-price-desc">—</td></tr>
<tr><td>Delta (Δ)</td><td id="c-delta">—</td><td id="c-delta-desc">—</td></tr>
<tr><td>Gamma (Γ)</td><td id="c-gamma">—</td><td id="c-gamma-desc">—</td></tr>
<tr><td>Vega (𝒱)</td><td id="c-vega">—</td><td id="c-vega-desc">—</td></tr>
</tbody>
</table>
<div class="hedge-row">
<span>Current hedge:</span>
<span id="hedge-shares">—</span>
<span>shares held</span>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-rebalance" type="button">Re-hedge now</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* base reset */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; align-items: center; margin-bottom: .8rem; }
.controls label { white-space: nowrap; font-weight: 600; font-size: .85rem; }
input[type=range] { width: 100%; }
table { width: 100%; border-collapse: collapse; margin-bottom: .7rem; }
th { background: #1d3557; color: #fff; padding: 5px 8px; text-align: left; font-size: .82rem; }
td { padding: 4px 8px; border-bottom: 1px solid #dee2e6; font-size: .85rem; }
td:first-child { font-weight: 700; font-family: ui-monospace, monospace; }
tr:nth-child(even) td { background: #f4f7fa; }
.hedge-row { font-size: .9rem; margin-bottom: .5rem; }
.hedge-row span:nth-child(2) { font-weight: 700; font-size: 1rem; margin: 0 .3rem; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.gain { color: #0a7d33; }
.status.loss { color: #c92f3c; }
.status.neutral { color: #555; }
.btns { display: flex; gap: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Black-Scholes European call pricer + Greeks calculator.
// Model: log-normal stock, constant vol & rate, no dividends.
// Abramowitz & Stegun approximation for the standard normal CDF.
function normCDF(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const poly = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const pdf = Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
const result = 1 - pdf * poly;
return x >= 0 ? result : 1 - result;
}
// Standard normal PDF.
function normPDF(x) { return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI); }
// Compute price, delta, gamma and vega for a European call.
// Inputs: S=stock price, K=strike, r=risk-free rate, T=time(years), sig=volatility
function bsGreeks(S, K, r, T, sig) {
if (T <= 0) {
const intrinsic = Math.max(S - K, 0);
return { price: intrinsic, delta: S > K ? 1 : 0, gamma: 0, vega: 0, d1: 0, d2: 0 };
}
const sqrtT = Math.sqrt(T);
const d1 = (Math.log(S / K) + (r + 0.5 * sig * sig) * T) / (sig * sqrtT);
const d2 = d1 - sig * sqrtT;
const price = S * normCDF(d1) - K * Math.exp(-r * T) * normCDF(d2);
const delta = normCDF(d1);
const gamma = normPDF(d1) / (S * sig * sqrtT);
const vega = S * normPDF(d1) * sqrtT / 100; // per 1% move in vol
return { price, delta, gamma, vega, d1, d2 };
}
// Fixed parameters: strike = $100, risk-free rate = 5%
const K = 100, r = 0.05;
// Hedge state: prevS = last rebalanced stock price, hedgeShares = current hedge ratio
let prevS = null, prevDelta = null, hedgeShares = null, hedgeValue = null, pnl = 0;
const slider = document.getElementById('s-slider');
const volSlider = document.getElementById('vol-slider');
const tSlider = document.getElementById('t-slider');
const sValEl = document.getElementById('s-val');
const volValEl = document.getElementById('vol-val');
const tValEl = document.getElementById('t-val');
function getParams() {
return {
S: parseFloat(slider.value),
sig: parseFloat(volSlider.value) / 100,
T: parseInt(tSlider.value) / 365
};
}
function update() {
const { S, sig, T } = getParams();
sValEl.textContent = S.toFixed(2);
volValEl.textContent = (sig * 100).toFixed(0);
tValEl.textContent = Math.round(T * 365);
const g = bsGreeks(S, K, r, T, sig);
document.getElementById('c-price').textContent = '
Watch what happens: as the stock moves, delta changes (gamma is at work), and the hedge — the number of shares you must hold to stay neutral — must be rebalanced. The P&L column shows how much you gain or lose between rebalances; without hedging, every tick is an unmanaged exposure.
+ g.price.toFixed(3);
document.getElementById('c-delta').textContent = g.delta.toFixed(4);
document.getElementById('c-gamma').textContent = g.gamma.toFixed(5);
document.getElementById('c-vega').textContent = '
Watch what happens: as the stock moves, delta changes (gamma is at work), and the hedge — the number of shares you must hold to stay neutral — must be rebalanced. The P&L column shows how much you gain or lose between rebalances; without hedging, every tick is an unmanaged exposure.
+ g.vega.toFixed(4);
document.getElementById('c-price-desc').textContent = i18nFmt('Fair value of the call with strike $%K%', { K: K });
document.getElementById('c-delta-desc').textContent = i18nFmt('Hold %shares% shares to be neutral right now', { shares: g.delta.toFixed(2) });
document.getElementById('c-gamma-desc').textContent = i18nFmt('Delta drifts by %gamma% per $1 move', { gamma: g.gamma.toFixed(5) });
document.getElementById('c-vega-desc').textContent = i18nFmt('Cost per 1% change in implied vol', {});
// Show currently held shares if hedged, else show current delta as suggestion
if (hedgeShares !== null) {
document.getElementById('hedge-shares').textContent = hedgeShares.toFixed(4);
} else {
document.getElementById('hedge-shares').textContent = g.delta.toFixed(4);
}
// Compute net P&L since last rebalance: option gain + hedge gain
const statusEl = document.getElementById('status');
if (hedgeShares !== null && prevS !== null && S !== prevS) {
const optionPnL = g.price - bsGreeks(prevS, K, r, T, sig).price;
const hedgePnL = hedgeShares * (S - prevS);
const netPnL = optionPnL + hedgePnL;
const sign = netPnL >= 0 ? '+' : '';
statusEl.textContent = i18nFmt('Net P&L since last rebalance: %net%', { net: sign + netPnL.toFixed(4) });
statusEl.className = 'status ' + (Math.abs(netPnL) < 0.005 ? 'neutral' : netPnL > 0 ? 'gain' : 'loss');
} else if (hedgeShares === null) {
statusEl.textContent = 'Press Re-hedge to lock a hedge, then move the price.';
statusEl.className = 'status neutral';
}
}
function rebalance() {
const { S, sig, T } = getParams();
const g = bsGreeks(S, K, r, T, sig);
prevS = S;
prevDelta = g.delta;
hedgeShares = g.delta;
document.getElementById('hedge-shares').textContent = hedgeShares.toFixed(4);
const statusEl = document.getElementById('status');
statusEl.textContent = i18nFmt('Hedged at delta = %delta%. Now move the stock price.', { delta: g.delta.toFixed(4) });
statusEl.className = 'status neutral';
}
function reset() {
prevS = null; prevDelta = null; hedgeShares = null; hedgeValue = null; pnl = 0;
slider.value = 100; volSlider.value = 20; tSlider.value = 30;
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
update();
}
slider.addEventListener('input', update);
volSlider.addEventListener('input', update);
tSlider.addEventListener('input', update);
document.getElementById('btn-rebalance').addEventListener('click', rebalance);
document.getElementById('btn-reset').addEventListener('click', reset);
update();
Comments
Loading comments...