← Natura·Docs/Modules/Source

speculation.js

100 lines · in natura-src-1.0.0.zip as natura/speculation.js · a module · its entry in the module reference · its test: speculation-test.js

// speculation.js -- Natura/EXISTA SPECULATION (the SOCIETY domain's FINANCIAL market, the shadow of economy.js).
// economy.js is the REAL market, where a price finds the level that clears goods against wants -- its wisdom. This
// is the ASSET market, where a price answers not to what a thing is worth but to what everyone thinks everyone
// else will pay next -- its madness. One asset of fixed fundamental value V, and a population split between two
// ways of guessing tomorrow's price: FUNDAMENTALISTS, who believe price reverts to value and so buy it cheap and
// sell it dear (a stabilising force), and CHARTISTS / trend-followers, who extrapolate the recent move and so buy
// what is rising and sell what is falling (a destabilising one). Agents SWITCH toward whichever style has been
// paying lately. Out of nothing but that comes the whole life of a market: endogenous BUBBLES (when the trend
// pays, agents turn chartist, which makes the trend pay more, until the price has left value far below it) and
// CRASHES (when the gap is so wide that a stumble makes the fundamentalists right, and everyone switches back at
// once); FAT-TAILED returns (calm broken by violent days); and VOLATILITY CLUSTERING (storms cluster into storms).
// No news, no shock, no outside cause -- the boom and the bust are made entirely of the crowd watching itself.
//
// A Natura system (init/step/fields/events), deterministic + node-testable. Randomness only from ctx.rng. UMD.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.Speculation = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;

  function makeSpeculation(config) {
    config = config || {};
    const V = config.V != null ? config.V : 100;        // the asset's FUNDAMENTAL value (what it is really worth)
    const g = config.g != null ? config.g : 3.0;        // chartist TREND-following strength (extrapolate the last move); the loop gain mu*g sits just under 1, the edge that makes bubbles GROW then REVERT instead of exploding
    const phi = config.phi != null ? config.phi : 0.10; // fundamentalist reversion strength (pull back toward V)
    const mu = config.mu != null ? config.mu : 0.30;    // market-maker price-adjustment speed (excess demand -> price move)
    const beta = config.beta != null ? config.beta : 2; // INTENSITY OF CHOICE: how fast agents chase the winning strategy (high -> sharp herding)
    const memory = config.memory != null ? config.memory : 0.55;   // how long a strategy's past profit is remembered when judging it
    const costC = config.costC != null ? config.costC : 0.3;       // small per-step cost of being a chartist (info/effort) -> fundamentalism is the default when nothing trends
    const noiseSig = config.noiseSig != null ? config.noiseSig : 0.8;   // small exogenous price noise (a live market is never perfectly still; it seeds the trends the chartists then chase)
    const chartistOnly = !!config.chartistOnly, fundOnly = !!config.fundOnly;   // test scenarios: force one type

    return {
      name: config.name || 'speculation',
      domain: 'society',
      scale: 'collective',
      params: config,

      init() {
        return {
          V, p: V, pPrev: V, nc: 0.5,                 // price starts at fair value, the crowd evenly split
          Wf: 0, Wc: 0, zf: 0, zc: 0,                 // strategy fitnesses (remembered profit) + last positions
          wealthF: 0, wealthC: 0,                      // CUMULATIVE profit of each strategy (undecayed): who actually wins over a bubble
          ret: 0, dev: 1, vol: 0, peakDev: 1, t: 0, evs: []
        };
      },

      step(st, ctx) {
        const rng = ctx && ctx.rng ? ctx.rng : Math.random;
        st.evs.length = 0;
        // the two ways of guessing next price
        const Ef = st.p + phi * (st.V - st.p);          // fundamentalist: expect reversion toward value
        const Ec = st.p + g * (st.p - st.pPrev);        // chartist: expect the last move to continue (momentum)
        const zf = Ef - st.p, zc = Ec - st.p;           // desired positions = expected gain over the current price
        let nc = fundOnly ? 0 : chartistOnly ? 1 : st.nc, nf = 1 - nc;
        const excess = nf * zf + nc * zc;               // net demand pushing on the market maker
        const noise = noiseSig * (2 * rng() - 1);
        let pNext = clamp(st.p + mu * excess + noise, 1, st.V * 40);
        const dp = pNext - st.p;

        // FITNESS: the profit each style's position earned as the price actually moved (position x price change),
        // remembered with decay. Chartism carries a small standing cost, so when nothing trends, caution wins.
        st.Wf = memory * st.Wf + zf * dp;
        st.Wc = memory * st.Wc + zc * dp - costC;
        st.wealthF += zf * dp; st.wealthC += zc * dp;   // undecayed cumulative P&L: the fundamentalist buys low/sells high, the chartist buys the top

        // SWITCH: agents move toward the more profitable style (a logit / soft-max on remembered fitness)
        if (!fundOnly && !chartistOnly) {
          const mx = Math.max(st.Wf, st.Wc);
          const ef = Math.exp(beta * (st.Wf - mx)), ec = Math.exp(beta * (st.Wc - mx));
          st.nc = ec / (ef + ec);
        } else st.nc = nc;

        st.pPrev = st.p; st.p = pNext;
        st.zf = zf; st.zc = zc;
        st.ret = dp / (st.pPrev || 1);
        st.dev = st.p / st.V;                            // how far price has left fundamental value (1 = fair, >1 a bubble, <1 a slump)
        st.vol = 0.94 * st.vol + 0.06 * Math.abs(st.ret);   // a slow gauge of recent turbulence
        if (Math.abs(st.dev - 1) > Math.abs(st.peakDev - 1)) st.peakDev = st.dev;
        st.t++;
        if (st.dev > 1.5 && st.nc > 0.6) st.evs.push({ kind: 'bubble', dev: st.dev, nc: st.nc });
        if (st.ret < -0.08) st.evs.push({ kind: 'crash', ret: st.ret });
        return st;
      },

      fields(st) {
        return {
          V: st.V, price: st.p, ret: st.ret, chartists: st.nc, deviation: st.dev,
          volatility: st.vol, peakDev: st.peakDev, wealthF: st.wealthF, wealthC: st.wealthC, t: st.t
        };
      },

      events(st) { return st.evs; }
    };
  }

  return { makeSpeculation };
});