← Natura·Docs/Modules/Source

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);