The simulation below runs the Avellaneda-Stoikov model in real time. A stream of random buy and sell orders arrives and the market maker fills them, building up inventory. Watch the bid and ask lines shift as the inventory grows.
<!-- Avellaneda-Stoikov market-making demo: live bid/ask with inventory skew -->
<div class="controls">
<label>Risk aversion γ: <strong id="gamma-val">0.1</strong>
<input type="range" id="gamma" min="0.01" max="0.5" step="0.01" value="0.1">
</label>
<label>Spread: <strong id="spread-val">0.10</strong>
<input type="range" id="spread" min="0.02" max="0.40" step="0.02" value="0.10">
</label>
<button id="reset-btn" type="button">Reset</button>
</div>
<canvas id="chart" width="560" height="230"></canvas>
<div class="stats">
<span>Inventory: <b id="inv-val">0</b></span>
<span>Bid: <b id="bid-val">-</b></span>
<span>Ask: <b id="ask-val">-</b></span>
<span>P&L: <b id="pnl-val">0.00</b></span>
</div>
<div class="status" id="status"></div>
/* Base styles for the market-making demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.controls {
display: flex; gap: .6rem; align-items: center; flex-wrap: wrap;
margin-bottom: .5rem;
}
label {
display: flex; align-items: center; gap: .35rem;
font-size: .82rem; color: #444;
}
input[type=range] { width: 80px; cursor: pointer; }
button {
font: 600 13px system-ui; padding: .35rem .75rem;
border: 1px solid #1d3557; background: #fff; color: #1d3557;
border-radius: 7px; cursor: pointer;
}
button:hover { background: #f0f4f8; }
canvas { display: block; border-radius: 8px; background: #f7f9fc;
border: 1px solid #dde3ea; max-width: 100%; }
.stats {
display: flex; gap: 1rem; flex-wrap: wrap;
margin-top: .45rem; font-size: .84rem;
}
.stats b { color: #1d3557; }
/* Status message colors: green for buy fill, red for sell fill */
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; margin-top: .3rem; }
.status.buy { color: #0a7d33; }
.status.sell { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Avellaneda-Stoikov (2008): set bid/ask so that the reservation price
// r = S - q * gamma * sigma^2 * (T - t) leans against inventory q.
// Half-spread is fixed by the slider; the model centers it on r, not S.
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Simulation parameters
const SIGMA = 1.0; // mid-price volatility (annualised units, scaled for demo)
const T = 1.0; // total horizon (normalised)
const HISTORY = 80; // number of ticks shown on the chart
let gamma, halfSpread, inventory, pnl, mid, timeLeft;
let midHistory, bidHistory, askHistory;
let tickTimer;
function getParams() {
gamma = parseFloat(document.getElementById('gamma').value);
halfSpread = parseFloat(document.getElementById('spread').value) / 2;
}
// Reservation price: skewed mid that leans against inventory
function reservation(q, t) {
return mid - q * gamma * SIGMA * SIGMA * t;
}
// Optimal bid and ask centered on the reservation price
function quotes(q, t) {
const r = reservation(q, t);
return { bid: r - halfSpread, ask: r + halfSpread };
}
function reset() {
clearInterval(tickTimer);
getParams();
inventory = 0;
pnl = 0;
mid = 100;
timeLeft = T;
midHistory = [mid];
bidHistory = [];
askHistory = [];
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
update();
tickTimer = setInterval(tick, 400);
}
// Advance the simulation by one time step
function tick() {
if (timeLeft <= 0) { clearInterval(tickTimer); return; }
// random walk for the mid price
mid += (Math.random() - 0.5) * SIGMA * 0.4;
const dt = 0.02;
timeLeft = Math.max(0, timeLeft - dt);
getParams();
const { bid, ask } = quotes(inventory, timeLeft);
// Poisson-style order arrivals (simplified to uniform random)
const r = Math.random();
let side = '';
if (r < 0.28) {
// a buyer hits the ask: maker sells at ask, inventory decreases
pnl += ask - mid;
inventory -= 1;
side = 'sell';
} else if (r < 0.56) {
// a seller hits the bid: maker buys at bid, inventory increases
pnl += mid - bid;
inventory += 1;
side = 'buy';
}
midHistory.push(mid);
bidHistory.push(bid);
askHistory.push(ask);
if (midHistory.length > HISTORY) { midHistory.shift(); bidHistory.shift(); askHistory.shift(); }
update();
const statusEl = document.getElementById('status');
if (side === 'sell') {
statusEl.textContent = i18nFmt('Sold at %price% (ask hit)', { price: ask.toFixed(2) });
statusEl.className = 'status sell';
} else if (side === 'buy') {
statusEl.textContent = i18nFmt('Bought at %price% (bid hit)', { price: bid.toFixed(2) });
statusEl.className = 'status buy';
}
}
function update() {
getParams();
const { bid, ask } = quotes(inventory, timeLeft);
document.getElementById('inv-val').textContent = inventory;
document.getElementById('bid-val').textContent = bid.toFixed(2);
document.getElementById('ask-val').textContent = ask.toFixed(2);
document.getElementById('pnl-val').textContent = pnl.toFixed(2);
document.getElementById('gamma-val').textContent = gamma.toFixed(2);
document.getElementById('spread-val').textContent = (halfSpread * 2).toFixed(2);
draw();
}
// Render the price-history chart on the canvas
function draw() {
ctx.clearRect(0, 0, W, H);
if (midHistory.length < 2) return;
const PAD = { t: 16, b: 24, l: 44, r: 16 };
const all = [...midHistory, ...bidHistory, ...askHistory];
const lo = Math.min(...all) - 0.5;
const hi = Math.max(...all) + 0.5;
const scX = x => PAD.l + (x / (HISTORY - 1)) * (W - PAD.l - PAD.r);
const scY = y => PAD.t + (1 - (y - lo) / (hi - lo)) * (H - PAD.t - PAD.b);
// horizontal grid lines and price labels
ctx.strokeStyle = '#e0e6ed'; ctx.lineWidth = 0.7;
for (let i = 0; i <= 4; i++) {
const y = PAD.t + i * (H - PAD.t - PAD.b) / 4;
ctx.beginPath(); ctx.moveTo(PAD.l, y); ctx.lineTo(W - PAD.r, y); ctx.stroke();
const val = hi - i * (hi - lo) / 4;
ctx.fillStyle = '#888'; ctx.font = '10px system-ui';
ctx.textAlign = 'right';
ctx.fillText(val.toFixed(1), PAD.l - 3, y + 3);
}
// draw mid, bid and ask time series
function line(data, color, dash) {
if (data.length < 2) return;
ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 1.8;
ctx.setLineDash(dash || []);
const offset = HISTORY - data.length;
data.forEach((v, i) => {
const x = scX(offset + i), y = scY(v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke(); ctx.setLineDash([]);
}
line(midHistory, '#5a7088');
line(bidHistory, '#0a7d33', [4, 3]);
line(askHistory, '#c92f3c', [4, 3]);
// legend box (top-right)
const lx = W - PAD.r - 5, ly = PAD.t + 4;
const legends = [
{ label: 'Mid', color: '#5a7088', dash: false },
{ label: 'Bid', color: '#0a7d33', dash: true },
{ label: 'Ask', color: '#c92f3c', dash: true },
];
legends.forEach((lg, i) => {
const y = ly + i * 16;
ctx.strokeStyle = lg.color; ctx.lineWidth = 1.8;
ctx.setLineDash(lg.dash ? [4, 3] : []);
ctx.beginPath(); ctx.moveTo(lx - 24, y); ctx.lineTo(lx - 10, y); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#444'; ctx.font = '10px system-ui'; ctx.textAlign = 'left';
ctx.fillText(lg.label, lx - 8, y + 3);
});
}
document.getElementById('gamma').addEventListener('input', update);
document.getElementById('spread').addEventListener('input', update);
document.getElementById('reset-btn').addEventListener('click', reset);
reset();
<!-- Avellaneda-Stoikov market-making demo: live bid/ask with inventory skew -->
<div class="controls">
<label>Risk aversion γ: <strong id="gamma-val">0.1</strong>
<input type="range" id="gamma" min="0.01" max="0.5" step="0.01" value="0.1">
</label>
<label>Spread: <strong id="spread-val">0.10</strong>
<input type="range" id="spread" min="0.02" max="0.40" step="0.02" value="0.10">
</label>
<button id="reset-btn" type="button">Reset</button>
</div>
<canvas id="chart" width="560" height="230"></canvas>
<div class="stats">
<span>Inventory: <b id="inv-val">0</b></span>
<span>Bid: <b id="bid-val">-</b></span>
<span>Ask: <b id="ask-val">-</b></span>
<span>P&L: <b id="pnl-val">0.00</b></span>
</div>
<div class="status" id="status"></div>
/* Base styles for the market-making demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.controls {
display: flex; gap: .6rem; align-items: center; flex-wrap: wrap;
margin-bottom: .5rem;
}
label {
display: flex; align-items: center; gap: .35rem;
font-size: .82rem; color: #444;
}
input[type=range] { width: 80px; cursor: pointer; }
button {
font: 600 13px system-ui; padding: .35rem .75rem;
border: 1px solid #1d3557; background: #fff; color: #1d3557;
border-radius: 7px; cursor: pointer;
}
button:hover { background: #f0f4f8; }
canvas { display: block; border-radius: 8px; background: #f7f9fc;
border: 1px solid #dde3ea; max-width: 100%; }
.stats {
display: flex; gap: 1rem; flex-wrap: wrap;
margin-top: .45rem; font-size: .84rem;
}
.stats b { color: #1d3557; }
/* Status message colors: green for buy fill, red for sell fill */
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; margin-top: .3rem; }
.status.buy { color: #0a7d33; }
.status.sell { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Avellaneda-Stoikov (2008): set bid/ask so that the reservation price
// r = S - q * gamma * sigma^2 * (T - t) leans against inventory q.
// Half-spread is fixed by the slider; the model centers it on r, not S.
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Simulation parameters
const SIGMA = 1.0; // mid-price volatility (annualised units, scaled for demo)
const T = 1.0; // total horizon (normalised)
const HISTORY = 80; // number of ticks shown on the chart
let gamma, halfSpread, inventory, pnl, mid, timeLeft;
let midHistory, bidHistory, askHistory;
let tickTimer;
function getParams() {
gamma = parseFloat(document.getElementById('gamma').value);
halfSpread = parseFloat(document.getElementById('spread').value) / 2;
}
// Reservation price: skewed mid that leans against inventory
function reservation(q, t) {
return mid - q * gamma * SIGMA * SIGMA * t;
}
// Optimal bid and ask centered on the reservation price
function quotes(q, t) {
const r = reservation(q, t);
return { bid: r - halfSpread, ask: r + halfSpread };
}
function reset() {
clearInterval(tickTimer);
getParams();
inventory = 0;
pnl = 0;
mid = 100;
timeLeft = T;
midHistory = [mid];
bidHistory = [];
askHistory = [];
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
update();
tickTimer = setInterval(tick, 400);
}
// Advance the simulation by one time step
function tick() {
if (timeLeft <= 0) { clearInterval(tickTimer); return; }
// random walk for the mid price
mid += (Math.random() - 0.5) * SIGMA * 0.4;
const dt = 0.02;
timeLeft = Math.max(0, timeLeft - dt);
getParams();
const { bid, ask } = quotes(inventory, timeLeft);
// Poisson-style order arrivals (simplified to uniform random)
const r = Math.random();
let side = '';
if (r < 0.28) {
// a buyer hits the ask: maker sells at ask, inventory decreases
pnl += ask - mid;
inventory -= 1;
side = 'sell';
} else if (r < 0.56) {
// a seller hits the bid: maker buys at bid, inventory increases
pnl += mid - bid;
inventory += 1;
side = 'buy';
}
midHistory.push(mid);
bidHistory.push(bid);
askHistory.push(ask);
if (midHistory.length > HISTORY) { midHistory.shift(); bidHistory.shift(); askHistory.shift(); }
update();
const statusEl = document.getElementById('status');
if (side === 'sell') {
statusEl.textContent = i18nFmt('Sold at %price% (ask hit)', { price: ask.toFixed(2) });
statusEl.className = 'status sell';
} else if (side === 'buy') {
statusEl.textContent = i18nFmt('Bought at %price% (bid hit)', { price: bid.toFixed(2) });
statusEl.className = 'status buy';
}
}
function update() {
getParams();
const { bid, ask } = quotes(inventory, timeLeft);
document.getElementById('inv-val').textContent = inventory;
document.getElementById('bid-val').textContent = bid.toFixed(2);
document.getElementById('ask-val').textContent = ask.toFixed(2);
document.getElementById('pnl-val').textContent = pnl.toFixed(2);
document.getElementById('gamma-val').textContent = gamma.toFixed(2);
document.getElementById('spread-val').textContent = (halfSpread * 2).toFixed(2);
draw();
}
// Render the price-history chart on the canvas
function draw() {
ctx.clearRect(0, 0, W, H);
if (midHistory.length < 2) return;
const PAD = { t: 16, b: 24, l: 44, r: 16 };
const all = [...midHistory, ...bidHistory, ...askHistory];
const lo = Math.min(...all) - 0.5;
const hi = Math.max(...all) + 0.5;
const scX = x => PAD.l + (x / (HISTORY - 1)) * (W - PAD.l - PAD.r);
const scY = y => PAD.t + (1 - (y - lo) / (hi - lo)) * (H - PAD.t - PAD.b);
// horizontal grid lines and price labels
ctx.strokeStyle = '#e0e6ed'; ctx.lineWidth = 0.7;
for (let i = 0; i <= 4; i++) {
const y = PAD.t + i * (H - PAD.t - PAD.b) / 4;
ctx.beginPath(); ctx.moveTo(PAD.l, y); ctx.lineTo(W - PAD.r, y); ctx.stroke();
const val = hi - i * (hi - lo) / 4;
ctx.fillStyle = '#888'; ctx.font = '10px system-ui';
ctx.textAlign = 'right';
ctx.fillText(val.toFixed(1), PAD.l - 3, y + 3);
}
// draw mid, bid and ask time series
function line(data, color, dash) {
if (data.length < 2) return;
ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 1.8;
ctx.setLineDash(dash || []);
const offset = HISTORY - data.length;
data.forEach((v, i) => {
const x = scX(offset + i), y = scY(v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke(); ctx.setLineDash([]);
}
line(midHistory, '#5a7088');
line(bidHistory, '#0a7d33', [4, 3]);
line(askHistory, '#c92f3c', [4, 3]);
// legend box (top-right)
const lx = W - PAD.r - 5, ly = PAD.t + 4;
const legends = [
{ label: 'Mid', color: '#5a7088', dash: false },
{ label: 'Bid', color: '#0a7d33', dash: true },
{ label: 'Ask', color: '#c92f3c', dash: true },
];
legends.forEach((lg, i) => {
const y = ly + i * 16;
ctx.strokeStyle = lg.color; ctx.lineWidth = 1.8;
ctx.setLineDash(lg.dash ? [4, 3] : []);
ctx.beginPath(); ctx.moveTo(lx - 24, y); ctx.lineTo(lx - 10, y); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#444'; ctx.font = '10px system-ui'; ctx.textAlign = 'left';
ctx.fillText(lg.label, lx - 8, y + 3);
});
}
document.getElementById('gamma').addEventListener('input', update);
document.getElementById('spread').addEventListener('input', update);
document.getElementById('reset-btn').addEventListener('click', reset);
reset();
Comments
Loading comments...