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