ISDA SIMM: how banks agree on initial margin without a regulator in the room
Two counterparties on a non-cleared derivative post collateral against each other using the same delta/vega/curvature sensitivities FRTB needs — but ISDA, not a regulator, calibrates the risk weights, and the aggregation blends risk classes together in a way FRTB never does. Real numbers from a run: SIMM total $3.127m, a concentration factor that's wired but dormant at this book's scale, and a backtest that isn't what it sounds like.
How do two banks agree on how much collateral to post against a derivative that never goes through a clearinghouse — without a regulator setting the number for either side? Both sides run the same model, ISDA's SIMM, on their own book, and reconcile. It needs the same kind of sensitivity vector FRTB SA needed in 10.2 — but grouped differently, with one wrinkle FRTB doesn't have at all.
The whole story
FRTB (10.2) keeps every risk class separate until the very end — Σ_class max(L, M, H), no cross-class correlation at all. SIMM's own aggregation,
SimmShowcase.psi(a, b), blends risk classes together within a product
class before that: RATES_FX combines GIRR and FX at ψ = 0.28,
CREDIT combines CSR_NON_SEC and CSR_SEC at ψ = 0.15. Same
NestedAggregation machinery underneath, a genuinely different
aggregation shape on top — the two regimes solve the same "how much
capital/margin" question but disagree about where the walls between risk
types go.
Build the non-cleared book
Seven trades, four counterparties, the four SIMM product classes:
private static final String BOOK_CSV = """
id,counterparty,product,notional,side
IRS-USD-10Y,CPTY-ALPHA,RATES_FX,120000000,1
XCCY-EURUSD-5Y,CPTY-ALPHA,RATES_FX,80000000,-1
CDS-IG-CPTY-5Y,CPTY-BRAVO,CREDIT,50000000,1
CDX-HY-INDEX-5Y,CPTY-BRAVO,CREDIT,25000000,-1
EQ-CALL-SX5E-1Y,CPTY-CHARLIE,EQUITY,15000000,-1
EQ-PUT-SPX-1Y,CPTY-CHARLIE,EQUITY,10000000,1
COMDTY-WTI-SWAP-2Y,CPTY-DELTA,COMMODITY,30000000,1
""";
Each ProductClass names which RiskClass values it rolls up —
RATES_FX covers GIRR and FX, CREDIT covers both non-securitisation
and securitisation CSR — the same enum 10.1–10.3 already used, just
grouped a new way.
Generate the sensitivity set
The showcase's one expensive stage: an adjoint sweep per position versus the prescribed-bump alternative, both feeding the same aggregation.
Nabla.TypedValuation<EquityMarket> risk =
greeks.run(tradeMarket(base, trade), pathsPerTrade, seed + trade);
Real numbers, 6 positions, 400,000 paths each, 252 steps, cpu-jit:
6 adjoint sweeps, 2,400,000 path valuations total, 1.621 s; 60
prescribed bump revaluations, 18.498 s — 11.4× this run. Smaller than
10.1's or 10.3's gap, and honestly so: each sweep here already does a
full 400,000-path valuation, not a cheap repricing, so the "expensive"
side of the comparison starts from a higher floor before the bump loop's
extra cost even enters.
Read closely, those 6 sweeps are 6 generic positions from
runHeavySensitivities's own synthetic loop — not the 7 named trades in
BOOK_CSV above. buildSensitivities grafts their aggregate delta,
vega, curvature and rate-delta onto exactly four named risk factors
(equity delta, equity vega, equity curvature, one GIRR delta); every
other one of the CRIF's factors is the same demo formula 10.2's
FrtbFullShowcase used — trade notional and side times an alternating
sign, not a Monte-Carlo number.
Aggregate: risk class, then product class
double kRiskType = /* NestedAggregation.delta(...).aggregate(...).total() per risk class */;
double sum = kr * kr;
for (RiskClass s : product.riskClasses()) {
if (r != s) { sum += psi(r, s) * kr * byClass.get(s).get(measure); }
}
double value = Math.sqrt(Math.max(0.0, sum));
Real numbers ($m): RATES_FX 0.988, CREDIT 1.156, EQUITY 0.674,
COMMODITY 0.309. Summed straight across product classes (no further
correlation once you're at this level): SIMM = 3.127.
The concentration factor
SIMM adds one thing FRTB's NestedAggregation.delta calls never used:
withConcentration(...), scaling a bucket's sensitivities up once its
net position gets large relative to a market-liquidity threshold
T_b. Chapter 9.4 named this exact method and left it unrun, calling it
"SIMM's job, not this one's" — this page is that job:
double cr = Math.max(1.0, Math.sqrt(Math.abs(bucketSum) / threshold));
Ran the numbers for every one of this book's 13 buckets across all 6
SIMM classes: CR_b = 1.0000, exactly, every single time — GIRR's USD
bucket sums to 3.7492 against a threshold of 330.00; equity's bucket
4 sums to 0.9028 against 8.00. The concentration path genuinely
runs on every measure and every class here, and does nothing, because a
seven-trade teaching book never gets close to the position size SIMM's
thresholds are calibrated for. Wired doesn't mean binding — the two are
easy to conflate from the code alone.
Try it yourself
Scale IRS-USD-10Y's notional from $120m to $20bn (leaving every
other trade alone) and rebuild the CRIF. Real numbers from doing exactly
that: GIRR's USD bucket sum rises to 401.35, past its 330.00
threshold for the first time — CR = 1.1028. SIMM's total jumps from
3.127 to 130.84, but don't credit that to concentration alone: the
notional itself is 167× bigger, and most of the jump is that plain
linear scaling. The concentration multiplier's own contribution is the
10.3% on top of what an uncapped NestedAggregation.delta would already
have given that one bucket — small next to the notional change, but the
first time in this Learn section the mechanism actually does something.
The backtest
double move = 0.05 * random.nextGaussian();
double im = simm(params, crif.scaled(1.0 + move)).total();
Real numbers: 750 simulated days in 0.084 s, 23 breaches where the
synthetic clean P&L exceeded that day's IM. Read the code before trusting
the word "backtest," though: crif.scaled(1.0 + move) uniformly rescales
every sensitivity in the whole vector by one random factor per day — it
is not a real historical market replay or a per-date portfolio
revaluation, which is why it finishes in milliseconds rather than the
hours docs-reg/isda-simm.md's own §2 describes as the second heavy
workload ((counterparties) × (thousands of historical dates) × (full portfolio reval)). What's here proves the shape of a backtest —
breach-counting against a margin number — not the cost it's actually
warning about.
▶️ Run it
The same shape, live, at a smaller scale — one real equity sweep plus a
handful of demo factors for the rest of the CRIF — right here, in your
browser through TeaVM, on the
cpu engine directly for the reflection reason 1.3's cell already
established. The product-class blending, the concentration factor and the
backtest are the real mechanics, run at this book's own scale:
Or run the real thing:
mvn -o -q install
mvn -o -q -pl nablatensor-examples exec:java \
-Dexec.mainClass=com.nablatensor.examples.SimmShowcase
Defaults to cpu-jit, 6 positions, 400,000 paths, no GPU.
-Dbook.trades=, -Dpaths=, and -Dbacktest.days= all override the
defaults. Unlike every other showcase this chapter has used, SimmShowcase
has no existing entry in ExamplesSmokeTest — nothing in the repo's own
test suite currently guards these numbers against drifting, so this
page's own run this session is the only check that ran before publishing.
⚠️ What this doesn't do
docs-reg/isda-simm.md names com.nablatensor.reg.simm as an existing
package implementing equity delta, vega and concentration — a find
across the whole engine repo turns up no such package at all, the same
gap 10.2 found in docs-reg/frtb-sa.md. What actually runs is one
showcase class on nablatensor-risk's already-generic
NestedAggregation, the same class Chapter 9 built. SimmParameters .demoV26()'s risk weights, correlations, and thresholds are explicitly
indicative — not the ISDA-published v2.8+2506/2512 calibration — and the
backtest above proves the counting logic, not the historical-replay cost
the regulation's annual obligation is actually about.
What's next
→ Deeper: docs-reg/isda-simm.md
covers the cadence (semiannual SIMM recalibration, EMIR 3.0's model
validation requirement) this page's code doesn't touch.
→ Next: Supervisory stress testing —
the same scenario-DSL replay pattern 10.1 introduced, crossed two ways
into a scenario-by-horizon grid.