← learnChapter 10 · Regulatory capital6 min read

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

Seven trades across four counterparties and four SIMM product classes. Six adjoint sweeps generate the sensitivity set in 1.621 seconds; 60 prescribed bump revaluations take 18.498 seconds for the same numbers, an 11.4x gap smaller than earlier chapters' because the sweep itself does real per-trade work here. SIMM's own twist: RATES_FX blends GIRR and FX at a 0.28 correlation, CREDIT blends CSR_NON_SEC and CSR_SEC at 0.15 — FRTB never mixes risk classes like this. A concentration factor, CR_b, is wired into every bucket's aggregation but sits at its floor of 1.0000 everywhere in this seven-trade book; scaling one trade to $20bn pushes one bucket's CR to 1.1028, the first time this Learn section has actually seen it bind. A 750-day backtest finishes in 0.084 seconds because each "day" is a uniform rescale of the same sensitivity vector, not a real historical portfolio revaluation. Total SIMM: $3.127m.

Did you know?

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));
Did you know?

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:

Java · compile and run in this browser

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.


Questions or corrections? open an issue