<p class="hint">A Fisher market: 3 buyers with budgets, 3 goods each with 1 unit of supply. Adjust budgets and utility weights, then click <b>Compute Equilibrium</b> to find the unique prices that clear the market (Eisenberg–Gale, 1959).</p>
<div id="controls"></div>
<button id="btn-compute" type="button">Compute Equilibrium</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<div id="results" class="results hidden"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
#controls { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: .6rem 1rem; margin-bottom: .8rem; }
.buyer-block { background: #f0f4f8; border-radius: 10px; padding: .6rem .8rem; }
.buyer-block h4 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
label { display: block; font-size: .8rem; color: #555; margin-top: .35rem; }
input[type=range] { width: 100%; accent-color: #1d3557; }
.val { font-weight: 700; color: #1d3557; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-right: .4rem; }
button.ghost { background: #fff; color: #1d3557; }
.results { margin-top: .9rem; }
.results.hidden { display: none; }
.price-row { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
.price-chip { background: #1d3557; color: #fff; border-radius: 8px; padding: .3rem .65rem; font-size: .82rem; font-weight: 700; }
table { width: 100%; border-collapse: collapse; margin-top: .4rem; font-size: .82rem; }
th { background: #dce6ee; color: #1d3557; padding: .3rem .5rem; text-align: left; }
td { padding: .28rem .5rem; border-bottom: 1px solid #eef1f4; }
tr:last-child td { border-bottom: none; }
.ok { color: #0a7d33; font-weight: 700; }
.section-label { font-weight: 700; color: #1d3557; font-size: .85rem; margin: .6rem 0 .2rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fisher market equilibrium via tatonnement (Eisenberg-Gale, 1959).
// Each buyer i has budget b_i and linear utility u_ij for good j (1 unit supply each).
// At equilibrium: every buyer spends full budget on best bang-per-buck good(s),
// and every good is exactly sold out. This is the unique optimum of the
// Eisenberg-Gale convex program: max sum_i b_i * log(utility_i).
// We solve by multiplicative tatonnement: raise p_j when over-demanded, lower when under.
const BUYERS = ['Alice', 'Bob', 'Carol'];
const GOODS = ['A', 'B', 'C'];
const N = 3, M = 3;
const DEFAULT = {
budgets: [40, 30, 30],
utils: [
[5, 3, 1], // Alice values good A most
[2, 5, 2], // Bob values good B most
[1, 2, 6] // Carol values good C most
]
};
let state = JSON.parse(JSON.stringify(DEFAULT));
// Demand: buyer i spends whole budget on good(s) with highest utility/price.
// Returns alloc[i][j] = fraction of good j allocated to buyer i.
function demand(p, budgets, utils) {
const alloc = Array.from({length: N}, () => Array(M).fill(0));
for (let i = 0; i < N; i++) {
const ratios = utils[i].map((u, j) => u / p[j]);
const maxR = Math.max(...ratios);
const best = utils[i].map((u, j) => Math.abs(ratios[j] - maxR) < maxR * 1e-9 ? u : 0);
const sumBestRatio = best.reduce((s, u, j) => s + (u > 0 ? u / p[j] : 0), 0);
for (let j = 0; j < M; j++) {
if (best[j] > 0) {
const spendJ = budgets[i] * (best[j] / p[j]) / sumBestRatio;
alloc[i][j] = spendJ / p[j];
}
}
}
return alloc;
}
function computeEquilibrium(budgets, utils) {
// Initialize prices proportional to weighted utility (good heuristic start)
const totalU = Array(M).fill(0);
for (let j = 0; j < M; j++) for (let i = 0; i < N; i++) totalU[j] += utils[i][j] * budgets[i];
const sumU = totalU.reduce((a, b) => a + b, 0);
const totalBudget = budgets.reduce((a, b) => a + b, 0);
let p = totalU.map(u => u / sumU * totalBudget);
// Tatonnement with decaying step size (converges for non-degenerate inputs)
const ITERS = 20000;
const STEP0 = 0.003;
for (let t = 0; t < ITERS; t++) {
const step = STEP0 * Math.pow(0.9999, t);
const alloc = demand(p, budgets, utils);
for (let j = 0; j < M; j++) {
const totalDemand = alloc.reduce((s, row) => s + row[j], 0);
p[j] *= Math.exp(step * (totalDemand - 1));
}
// Normalize: total market value = total budget
const mv = p.reduce((a, b) => a + b, 0);
for (let j = 0; j < M; j++) p[j] = p[j] * totalBudget / mv;
}
return { p, alloc: demand(p, budgets, utils) };
}
function buildControls() {
const cont = document.getElementById('controls');
cont.innerHTML = '';
for (let i = 0; i < N; i++) {
const div = document.createElement('div');
div.className = 'buyer-block';
let html = `<h4>${BUYERS[i]}</h4>`;
html += fmt(`<label>Budget: <span class="val" id="bval-%i%">%v%</span>
<input type="range" min="10" max="80" step="5" value="%v%"
oninput="state.budgets[%i%]=+this.value;document.getElementById('bval-%i%').textContent=this.value"></label>`, { i: i, v: state.budgets[i] });
for (let j = 0; j < M; j++) {
html += fmt(`<label>Utility for %v%: <span class="val" id="uval-%i%-%j%">%v2%</span>
<input type="range" min="1" max="9" step="1" value="%v2%"
oninput="state.utils[%i%][%j%]=+this.value;document.getElementById('uval-%i%-%j%').textContent=this.value"></label>`, { v: GOODS[j], i: i, j: j, v2: state.utils[i][j] });
}
div.innerHTML = html;
cont.appendChild(div);
}
}
function showResults(p, alloc, budgets) {
const res = document.getElementById('results');
res.classList.remove('hidden');
let html = '<div class="section-label">Equilibrium Prices</div><div class="price-row">';
for (let j = 0; j < M; j++) {
html += fmt(`<div class="price-chip">Good %v%: $%v2%</div>`, { v: GOODS[j], v2: p[j].toFixed(2) });
}
html += '</div>';
html += '<div class="section-label">How Each Buyer Spends</div>';
html += '<table><tr><th>Buyer</th><th>Budget</th>';
for (let j = 0; j < M; j++) html += fmt(`<th>Good %v% qty</th>`, { v: GOODS[j] });
html += '<th>Spent</th><th>Cleared?</th></tr>';
for (let i = 0; i < N; i++) {
const spent = alloc[i].reduce((s, q, j) => s + q * p[j], 0);
const ok = Math.abs(spent - budgets[i]) < 0.5;
html += `<tr><td>${BUYERS[i]}</td><td>${budgets[i]}</td>`;
for (let j = 0; j < M; j++) html += `<td>${alloc[i][j].toFixed(3)}</td>`;
html += `<td>${spent.toFixed(2)}</td><td class="${ok ? 'ok' : ''}">${ok ? '✓' : '!'}</td></tr>`;
}
html += '</table>';
html += '<div class="section-label">Supply Cleared?</div>';
html += '<table><tr><th>Good</th><th>Total demand</th><th>Supply</th><th>Cleared?</th></tr>';
for (let j = 0; j < M; j++) {
const td = alloc.reduce((s, row) => s + row[j], 0);
const ok = Math.abs(td - 1) < 0.02;
html += `<tr><td>${GOODS[j]}</td><td>${td.toFixed(3)}</td><td>1.000</td><td class="${ok ? 'ok' : ''}">${ok ? '✓' : '!'}</td></tr>`;
}
html += '</table>';
res.innerHTML = html;
}
document.getElementById('btn-compute').addEventListener('click', () => {
const { p, alloc } = computeEquilibrium(state.budgets, state.utils);
showResults(p, alloc, state.budgets);
console.log(`prices:`, p.map(x => +x.toFixed(4)));
const totalDemand = GOODS.map((_, j) => alloc.reduce((s, row) => s + row[j], 0));
console.log(`total demand per good:`, totalDemand.map(x => +x.toFixed(4)));
});
document.getElementById('btn-reset').addEventListener('click', () => {
state = JSON.parse(JSON.stringify(DEFAULT));
buildControls();
document.getElementById('results').classList.add('hidden');
});
buildControls();
<p class="hint">A Fisher market: 3 buyers with budgets, 3 goods each with 1 unit of supply. Adjust budgets and utility weights, then click <b>Compute Equilibrium</b> to find the unique prices that clear the market (Eisenberg–Gale, 1959).</p>
<div id="controls"></div>
<button id="btn-compute" type="button">Compute Equilibrium</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<div id="results" class="results hidden"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
#controls { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: .6rem 1rem; margin-bottom: .8rem; }
.buyer-block { background: #f0f4f8; border-radius: 10px; padding: .6rem .8rem; }
.buyer-block h4 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
label { display: block; font-size: .8rem; color: #555; margin-top: .35rem; }
input[type=range] { width: 100%; accent-color: #1d3557; }
.val { font-weight: 700; color: #1d3557; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-right: .4rem; }
button.ghost { background: #fff; color: #1d3557; }
.results { margin-top: .9rem; }
.results.hidden { display: none; }
.price-row { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
.price-chip { background: #1d3557; color: #fff; border-radius: 8px; padding: .3rem .65rem; font-size: .82rem; font-weight: 700; }
table { width: 100%; border-collapse: collapse; margin-top: .4rem; font-size: .82rem; }
th { background: #dce6ee; color: #1d3557; padding: .3rem .5rem; text-align: left; }
td { padding: .28rem .5rem; border-bottom: 1px solid #eef1f4; }
tr:last-child td { border-bottom: none; }
.ok { color: #0a7d33; font-weight: 700; }
.section-label { font-weight: 700; color: #1d3557; font-size: .85rem; margin: .6rem 0 .2rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fisher market equilibrium via tatonnement (Eisenberg-Gale, 1959).
// Each buyer i has budget b_i and linear utility u_ij for good j (1 unit supply each).
// At equilibrium: every buyer spends full budget on best bang-per-buck good(s),
// and every good is exactly sold out. This is the unique optimum of the
// Eisenberg-Gale convex program: max sum_i b_i * log(utility_i).
// We solve by multiplicative tatonnement: raise p_j when over-demanded, lower when under.
const BUYERS = ['Alice', 'Bob', 'Carol'];
const GOODS = ['A', 'B', 'C'];
const N = 3, M = 3;
const DEFAULT = {
budgets: [40, 30, 30],
utils: [
[5, 3, 1], // Alice values good A most
[2, 5, 2], // Bob values good B most
[1, 2, 6] // Carol values good C most
]
};
let state = JSON.parse(JSON.stringify(DEFAULT));
// Demand: buyer i spends whole budget on good(s) with highest utility/price.
// Returns alloc[i][j] = fraction of good j allocated to buyer i.
function demand(p, budgets, utils) {
const alloc = Array.from({length: N}, () => Array(M).fill(0));
for (let i = 0; i < N; i++) {
const ratios = utils[i].map((u, j) => u / p[j]);
const maxR = Math.max(...ratios);
const best = utils[i].map((u, j) => Math.abs(ratios[j] - maxR) < maxR * 1e-9 ? u : 0);
const sumBestRatio = best.reduce((s, u, j) => s + (u > 0 ? u / p[j] : 0), 0);
for (let j = 0; j < M; j++) {
if (best[j] > 0) {
const spendJ = budgets[i] * (best[j] / p[j]) / sumBestRatio;
alloc[i][j] = spendJ / p[j];
}
}
}
return alloc;
}
function computeEquilibrium(budgets, utils) {
// Initialize prices proportional to weighted utility (good heuristic start)
const totalU = Array(M).fill(0);
for (let j = 0; j < M; j++) for (let i = 0; i < N; i++) totalU[j] += utils[i][j] * budgets[i];
const sumU = totalU.reduce((a, b) => a + b, 0);
const totalBudget = budgets.reduce((a, b) => a + b, 0);
let p = totalU.map(u => u / sumU * totalBudget);
// Tatonnement with decaying step size (converges for non-degenerate inputs)
const ITERS = 20000;
const STEP0 = 0.003;
for (let t = 0; t < ITERS; t++) {
const step = STEP0 * Math.pow(0.9999, t);
const alloc = demand(p, budgets, utils);
for (let j = 0; j < M; j++) {
const totalDemand = alloc.reduce((s, row) => s + row[j], 0);
p[j] *= Math.exp(step * (totalDemand - 1));
}
// Normalize: total market value = total budget
const mv = p.reduce((a, b) => a + b, 0);
for (let j = 0; j < M; j++) p[j] = p[j] * totalBudget / mv;
}
return { p, alloc: demand(p, budgets, utils) };
}
function buildControls() {
const cont = document.getElementById('controls');
cont.innerHTML = '';
for (let i = 0; i < N; i++) {
const div = document.createElement('div');
div.className = 'buyer-block';
let html = `<h4>${BUYERS[i]}</h4>`;
html += fmt(`<label>Budget: <span class="val" id="bval-%i%">%v%</span>
<input type="range" min="10" max="80" step="5" value="%v%"
oninput="state.budgets[%i%]=+this.value;document.getElementById('bval-%i%').textContent=this.value"></label>`, { i: i, v: state.budgets[i] });
for (let j = 0; j < M; j++) {
html += fmt(`<label>Utility for %v%: <span class="val" id="uval-%i%-%j%">%v2%</span>
<input type="range" min="1" max="9" step="1" value="%v2%"
oninput="state.utils[%i%][%j%]=+this.value;document.getElementById('uval-%i%-%j%').textContent=this.value"></label>`, { v: GOODS[j], i: i, j: j, v2: state.utils[i][j] });
}
div.innerHTML = html;
cont.appendChild(div);
}
}
function showResults(p, alloc, budgets) {
const res = document.getElementById('results');
res.classList.remove('hidden');
let html = '<div class="section-label">Equilibrium Prices</div><div class="price-row">';
for (let j = 0; j < M; j++) {
html += fmt(`<div class="price-chip">Good %v%: $%v2%</div>`, { v: GOODS[j], v2: p[j].toFixed(2) });
}
html += '</div>';
html += '<div class="section-label">How Each Buyer Spends</div>';
html += '<table><tr><th>Buyer</th><th>Budget</th>';
for (let j = 0; j < M; j++) html += fmt(`<th>Good %v% qty</th>`, { v: GOODS[j] });
html += '<th>Spent</th><th>Cleared?</th></tr>';
for (let i = 0; i < N; i++) {
const spent = alloc[i].reduce((s, q, j) => s + q * p[j], 0);
const ok = Math.abs(spent - budgets[i]) < 0.5;
html += `<tr><td>${BUYERS[i]}</td><td>${budgets[i]}</td>`;
for (let j = 0; j < M; j++) html += `<td>${alloc[i][j].toFixed(3)}</td>`;
html += `<td>${spent.toFixed(2)}</td><td class="${ok ? 'ok' : ''}">${ok ? '✓' : '!'}</td></tr>`;
}
html += '</table>';
html += '<div class="section-label">Supply Cleared?</div>';
html += '<table><tr><th>Good</th><th>Total demand</th><th>Supply</th><th>Cleared?</th></tr>';
for (let j = 0; j < M; j++) {
const td = alloc.reduce((s, row) => s + row[j], 0);
const ok = Math.abs(td - 1) < 0.02;
html += `<tr><td>${GOODS[j]}</td><td>${td.toFixed(3)}</td><td>1.000</td><td class="${ok ? 'ok' : ''}">${ok ? '✓' : '!'}</td></tr>`;
}
html += '</table>';
res.innerHTML = html;
}
document.getElementById('btn-compute').addEventListener('click', () => {
const { p, alloc } = computeEquilibrium(state.budgets, state.utils);
showResults(p, alloc, state.budgets);
console.log(`prices:`, p.map(x => +x.toFixed(4)));
const totalDemand = GOODS.map((_, j) => alloc.reduce((s, row) => s + row[j], 0));
console.log(`total demand per good:`, totalDemand.map(x => +x.toFixed(4)));
});
document.getElementById('btn-reset').addEventListener('click', () => {
state = JSON.parse(JSON.stringify(DEFAULT));
buildControls();
document.getElementById('results').classList.add('hidden');
});
buildControls();
Comments
Loading comments...