speculation-test.js
90 lines · in natura-src-1.0.0.zip as natura/speculation-test.js · a test
// speculation-test.js -- the ASSET market as a heterogeneous-agent model, every stylized fact of a real market
// verified by RUNNING (never by reasoning): a stable price under fundamentalists alone, endogenous BUBBLES and
// CRASHES once trend-followers can win, mean-reversion to value in the long run, FAT-TAILED returns, VOLATILITY
// CLUSTERING, and a chartist fraction that swings from caution to mania and back -- all from no outside shock.
const { World, UNITS } = require('./natura.js');
const { makeSpeculation } = require('./speculation.js');
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
function run(seed, cfg, steps) {
const s = makeSpeculation(cfg || {}); const w = World({ seed, systems: [s] }); const st = w.get('speculation');
const rets = [], devs = []; let ncMin = 1, ncMax = 0, allFinite = true, maxDevAbs = 0;
for (let i = 0; i < steps; i++) {
w.step(UNITS.DAY); const f = w.fields().speculation;
if (!isFinite(f.price) || f.price <= 0) allFinite = false;
if (i > 100) { rets.push(f.ret); devs.push(f.deviation); const da = Math.abs(f.deviation - 1); if (da > maxDevAbs) maxDevAbs = da; if (f.chartists < ncMin) ncMin = f.chartists; if (f.chartists > ncMax) ncMax = f.chartists; }
}
const f = w.fields().speculation, n = rets.length, mean = rets.reduce((a, b) => a + b, 0) / n;
let v = 0, k = 0; for (const r of rets) v += (r - mean) ** 2; v /= n; for (const r of rets) k += (r - mean) ** 4; k = v > 1e-12 ? k / n / (v * v) : 0;
const ac = (arr) => { const m = arr.reduce((a, b) => a + b, 0) / arr.length; let num = 0, den = 0; for (let i = 1; i < arr.length; i++) num += (arr[i] - m) * (arr[i - 1] - m); for (const x of arr) den += (x - m) ** 2; return den > 1e-12 ? num / den : 0; };
const meanDev = devs.reduce((a, b) => a + b, 0) / devs.length;
return { f, meanDev, maxDevAbs, kurt: k, acAbs: ac(rets.map(Math.abs)), ncMin, ncMax, allFinite };
}
console.log('speculation assertions:');
// 1. DETERMINISM
{
const a = run(5, {}, 400).f, b = run(5, {}, 400).f;
ok(a.price === b.price && a.chartists === b.chartists, 'same seed -> identical market (reproducible)');
}
// 2. BOUNDED / never breaks -- price stays finite and positive across a long, wild run
{
const r = run(7, {}, 4000);
ok(r.allFinite, 'price stays finite and positive across 4000 steps (the market never breaks)');
}
// 3. FUNDAMENTALISTS ALONE -> a STABLE price near value. With no one chasing trends, price just wobbles around
// what the asset is worth. The control case: this is a market being sane.
{
const r = run(9, { fundOnly: true }, 1500);
ok(r.maxDevAbs < 0.12 && Math.abs(r.meanDev - 1) < 0.05, 'fundamentalists ALONE keep price near value (max departure ' + (r.maxDevAbs * 100).toFixed(0) + '%)');
}
// 4. ENDOGENOUS BUBBLES & CRASHES -- let trend-followers exist and switch in, and the price leaves value FAR
// behind (a bubble) then snaps back (a crash) -- with NO external news or shock, only the crowd chasing itself.
{
const r = run(7, {}, 3000);
ok(r.maxDevAbs > 0.3, 'with trend-followers, price BUBBLES/CRASHES far from value on its own (peak departure ' + (r.maxDevAbs * 100).toFixed(0) + '%)');
}
// 5. MEAN-REVERSION -- however wild the swings, over the long run the price keeps coming HOME to fundamental value
// (the fundamentalists win in the end). The average deviation sits at ~1.
{
const r = run(7, {}, 4000);
ok(Math.abs(r.meanDev - 1) < 0.12, 'over the long run price REVERTS to value despite the swings (mean deviation ' + r.meanDev.toFixed(2) + ')');
}
// 6. FAT-TAILED RETURNS -- extreme days happen far more than a bell curve allows: the return distribution is
// leptokurtic (kurtosis well above the Gaussian 3). The market's violence is not normal.
{
const r = run(7, {}, 4000);
ok(r.kurt > 3.2, 'returns are FAT-TAILED -- big moves far more common than a normal distribution (kurtosis ' + r.kurt.toFixed(1) + ' vs 3)');
}
// 7. VOLATILITY CLUSTERING -- turbulence begets turbulence: the SIZE of today's move predicts tomorrow's, so calm
// stretches and violent stretches each clump together (autocorrelation of |returns| clearly positive).
{
const r = run(7, {}, 4000);
ok(r.acAbs > 0.15, 'VOLATILITY CLUSTERS -- big moves follow big moves (|return| autocorrelation ' + r.acAbs.toFixed(2) + ')');
}
// 8. THE CROWD SWINGS FROM CAUTION TO MANIA -- the chartist fraction does not settle; it sweeps nearly the whole
// range as booms recruit trend-chasers and busts send everyone back to fundamentals. Greed and fear, cycling.
{
const r = run(7, {}, 3000);
ok(r.ncMax - r.ncMin > 0.5, 'the crowd swings between CAUTION and MANIA (chartist share ranges ' + (r.ncMin * 100).toFixed(0) + '%-' + (r.ncMax * 100).toFixed(0) + '%)');
}
// 9. STRONGER TREND-FOLLOWING -> BIGGER BUBBLES -- crank the extrapolation and the departures from value grow.
// The mechanism, isolated: the bubble is made of momentum, so more momentum makes a bigger bubble.
{
const mild = run(7, { g: 2 }, 3000).maxDevAbs, wild = run(7, { g: 3.2 }, 3000).maxDevAbs;
ok(wild > mild + 0.1, 'more TREND-FOLLOWING -> bigger bubbles (peak departure ' + (mild * 100).toFixed(0) + '% -> ' + (wild * 100).toFixed(0) + '%)');
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);