The arithmetic a clearing layer is made of.
Windows, novation, netting, margin, collateral and the default waterfall as pure functions. No transport, no keys, no network: feed it data you fetched yourself and check the result against your own.
Every amount is BigInt and every rate is an integer. A margin rate written as a float is a rounding error waiting for a stress event, and the one place you find out is the one place nobody wants to.
npm install @spireproto/core| Module | What it settles |
|---|---|
window |
Which settlement window a fill belongs to, and what phase that window is in |
obligation |
Discharging a bilateral trade into the two obligations that replace it |
netting |
Collapsing a window per member and per asset, across venues |
margin |
What a position costs to carry, against the net and at the tier of the asset |
collateral |
Posted value turned into a limit, and a breach turned into a code |
waterfall |
Absorbing a default in an order fixed before anyone defaults |
Windows are derived from the clock, not announced by an operator. Given a unix second, anyone can say which window it belongs to without asking us.
const { window } = require('@spireproto/core');
window.windowOf(1800000042); // 6000000
window.phaseOf(6000000, 1800000310); // 'finalising'
window.assign(1800000310, 6000000);
// { windowId: 6000001, deferred: true, reason: 'SPIRE-4001' }Intake is contiguous: window N finalises and settles while N+1 is already accepting fills. That is why there are 288 windows in a day and not 192, and it is why a fill sent to a window that just closed is carried rather than refused.
const { netting } = require('@spireproto/core');
// buy 400, sell 250, buy 350, one window, two venues
netting.positions(obligations);
// [{ member, asset, windowId, gross: 1000n, net: 500n, compression: 5000, ... }]Netting is per member, per asset, per window. Not per venue: after novation two fills from two venues are the same instrument, so they net, and neither venue learns that the other side exists.
const { margin } = require('@spireproto/core');
margin.marginOnPosition({ gross: 1000n, net: 250n }, price, 1);
// grossMargin 80 netMargin 20 saved 60A thousand of turnover in one window that nets to 250 carries 20 of margin at tier 1. Under prefunding the same turnover requires a thousand of assets sitting still. That difference is the product, not an optimisation.
const { collateral } = require('@spireproto/core');
collateral.limitOf(1_000_000_000_000n, 1); // 12,500,000 USDC of notional
collateral.checkNovation({ acct, tier: 1, addNotional });
// { ok: false, code: 'SPIRE-3001', limit, attempted }Checks run at novation, before anything is owed, and they return a code rather
than throwing. A limit breach is a business condition, not an outage. free is
signed: negative is a live margin call with an absolute deadline, and that
deadline survives a window boundary.
const { waterfall } = require('@spireproto/core');
waterfall.absorb(shortfall, { margin, ownFund, insurance, members });
// { fromMargin, fromOwnFund, fromInsurance, fromMutual, toAuction, layers, events }Five layers, in order: the defaulter's margin, the defaulter's fund contribution, protocol insurance, the mutualised fund of the other members, then auction. The four sums are returned separately so the order of absorption is observable after the fact rather than asserted. No member is assessed past twice its contribution in one event.
git clone https://github.com/spireproto/spire-core
cd spire-core
node --test test/*.test.js
node examples/netting.js
node examples/default.js55 tests, zero dependencies. Every worked example from the parameter table is one of them, so if the published numbers and this package ever drift apart, the suite fails here first.
All of them live in src/params.js and mirror
spireproto.xyz/docs/parameters: window
300s plus 30s finalisation plus 120s to deliver, margin 8/12/20% by tier,
haircuts 0/2/15/35%, default fund 15% of margin with a 50,000 USDC floor, a
2,500,000 USDC insurance layer, assessments capped at 2x, concentration 25%,
cure 600s, clearing fee 0.35 bps.
It is not a client and it does not talk to a chain. It holds no keys and signs nothing: see spire-sdk for the wire format and spire-contracts for the on-chain surface. Nothing here custodies an asset.
MIT