The Math of B-Book Netting, in Plain English
A teaching companion to BBOOK_NETTING_ONE_PAGER.md. Builds from zero up to the routing decision with worked examples. Every formula is just a tidy way of asking one of three questions: "how much do we owe at worst?", "how much do we expect to make?", and "how much of this new bet should we keep?" Everything else is bookkeeping.
1. The single idea: a "what happens to us" table
Imagine the platform is a bookie at a horse race with three horses: A, B, and C. Right now the bookie has no bets on the books, so the bookie's table looks like this:
| If A wins | If B wins | If C wins |
|---|---|---|
| 0 | 0 | 0 |
That's it. Three numbers. Each number says "the platform's net profit/loss if that horse wins." Zero everywhere because no bets have been placed.
Now a player walks up and backs horse B with a £100 stake at odds 4.0. What does the bookie's table become?
- If A wins → player loses, bookie keeps the £100. Entry: +100.
- If B wins → player wins. Bookie pays them £100 × (4.0 − 1) = £300. Entry: −300.
- If C wins → player loses, bookie keeps the £100. Entry: +100.
So this one bet has a "what happens to us" row of:
| If A wins | If B wins | If C wins |
|---|---|---|
| +100 | −300 | +100 |
This row is the bet's pnlVector (also called v). It's the same vector the netting engine builds inside buildPnlVector — there's nothing fancy about it; it's just "platform's P&L per outcome." One number per outcome. For a market with N outcomes, the vector has N entries.
For a lay bet (the platform is taking the user's bet against an outcome), the signs flip — you just multiply the back-bet vector by −1. That's the only difference between back and lay.
2. Many bets stack up — Pmax
When a second bet comes in, the bookie doesn't keep a separate row per bet — they add the rows together. If a new player backs horse A for £50 at odds 3.0:
The new bet's row is [−100, +50, +50].
The bookie's running total — what we call Pmax — is the old row plus the new row, entry by entry:
| If A | If B | If C | |
|---|---|---|---|
| Bet 1 | +100 | −300 | +100 |
| Bet 2 | −100 | +50 | +50 |
| Pmax (sum) | 0 | −250 | +150 |
So Pmax = [0, −250, +150]. This is what MarketExposure.pmax_vector stores. As bets come and go, this vector grows or shrinks.
Plain-English rule: Pmax is just the bookie's running tally of "what happens to me on each outcome, given everything I've taken so far."
3. The worst case — Emax
Out of those three possible futures [0, −250, +150], which is the worst for the bookie? Clearly the one where horse B wins: they're down £250.
Emax = min(Pmax)
Emax is just the worst entry in Pmax. In our running example, Emax = −250.
This single number tells you the bookie's worst-case loss right now. Sometimes it's positive (every outcome is profitable), in which case the bookie has a guaranteed profit no matter what. Sometimes it's negative, in which case it's the most they could be on the hook for.
The bookie's "locked capital" — the money they must have on hand against current liabilities — is exactly |Emax|. If Emax = −250, they need £250 of risk capital to keep that book open. No matter what horse wins, they always have enough money to pay out.
4. The expected case — pt (Platform Take)
Worst-case is one number. The bookie also wants to know: on average, am I making money or losing money on this position?
That depends on what they believe the probabilities are. If they think:
- A wins with probability 25% (
p_A = 0.25) - B wins with probability 50% (
p_B = 0.50) - C wins with probability 25% (
p_C = 0.25)
Then the expected P&L is each outcome's P&L weighted by its probability:
pt = p_A × Pmax[A] + p_B × Pmax[B] + p_C × Pmax[C]
= 0.25 × 0 + 0.50 × (−250) + 0.25 × (+150)
= 0 + (−125) + 37.5
= −87.5
So on average the bookie expects to lose £87.50 on this book. They might still take more bets — they're not over the worst-case cliff yet — but each new bet should ideally push pt back up.
We compute pt the same way for a single bet's vector v, against the same beliefs p. pt(v, p) is what decideKeep checks before deciding to keep a bet. It's just (probability × P&L) added up across outcomes.
Where does p come from? Sharp markets like Pinnacle or Betfair "sharp" prices imply probabilities that have been corrected for the bookmaker's margin (called "devigging"). Those are the most accurate public estimates. The platform uses them as p. If our offered odds give better-than-sharp prices to the user, our pt is negative — we expect to lose — and we should pass the bet to the exchange.
5. Why accept and match are split — the asymmetry
Here's where the netting engine does something subtle but important. When a bet is placed but not yet matched on the exchange, the bookie should lock capital against the worst case of that bet, but not give themselves credit for the best case until it actually matches.
Think of it this way: when you put a bet on Betfair, you tell the exchange "I want £100 at 4.0". The exchange may or may not find someone to take that bet. Until it's matched:
- The platform's risk is real if the bet eventually matches and the user wins.
- The platform's gain from the user losing is only real once the bet is locked in (matched).
So the engine treats placement in two phases:
Phase 1 — Accept (only the bad parts count):
For each outcome in the new bet's vector v:
- If
v[i]is negative (it's a loss for the bookie if outcome i wins), add it toPmax. - If
v[i]is positive (a gain for the bookie), add zero for now.
In math:
Pmax_after_accept[i] = Pmax[i] + min(v[i], 0)
min(v[i], 0) is "v[i] if v[i] is negative, else 0." So this step only makes Pmax worse, never better. It's pessimistic on purpose.
The locked capital from accept = how much worse Emax got:
acr = Emax_old - Emax_after_accept (always ≥ 0)
If Emax went from −250 to −350, then acr = (−250) − (−350) = 100. The bookie locks an extra £100 of capital. Always positive or zero — this is a key invariant the engine enforces.
Phase 2 — Match (now the good parts count):
Once the bet matches:
Pmax_after_match[i] = Pmax_after_accept[i] + max(v[i], 0)
max(v[i], 0) is "v[i] if v[i] is positive, else 0." Now the bookie's "if user loses" gains finally enter the tally.
The released capital from match = how much better Emax got:
rc = Emax_after_match - Emax_after_accept (always ≥ 0)
If Emax improved from −350 back to −320, then rc = (−320) − (−350) = 30. The bookie unlocks £30 of capital. Always positive or zero.
Net effect on the bookie's locked capital from one bet:
net_lock = acr - rc
If a new bet worsens the worst case (no useful hedge), net_lock > 0 — capital goes in. If a new bet hedges an existing position (improves the worst case), rc > acr and net_lock < 0 — capital comes back. This is the hedge refund the engine recovers correctly.
6. A worked example of the asymmetry
Suppose Pmax = [+100, −200, +50] (we'd lose £200 if B wins, otherwise we're up). Emax = −200.
A new bet has vector v = [−40, +120, −40]. (A back-bet on B that would push us further into the red on B but help us on A and C.)
Phase 1 — Accept:
- Negative parts of
v:min(−40, 0) = −40,min(+120, 0) = 0,min(−40, 0) = −40. - Pessimistic vector to add:
[−40, 0, −40]. Pmax_after_accept = [+60, −200, +10].Emax_after_accept = −200(unchanged — the worst case is still B).acr = (−200) − (−200) = 0. No extra capital locked yet.
That's nice — accepting this bet doesn't immediately tie up money, because outcome B was already the worst case.
Phase 2 — Match (instant in Bifrost-style):
- Positive parts of
v:max(−40, 0) = 0,max(+120, 0) = +120,max(−40, 0) = 0. - Optimistic vector to add:
[0, +120, 0]. Pmax_after_match = [+60, −80, +10].Emax_after_match = −80.rc = (−80) − (−200) = 120. £120 of capital released.
net_lock = 0 − 120 = −120 → the user is refunded £120 of previously-locked capital because this bet hedged the book.
The whole point of the two-phase math is that this refund is computed correctly. A single-step max(0, Emax_old − Emax_new) would clamp the refund to zero and silently lose money. That bug exists in some implementations; the netting engine doesn't have it.
7. The B-Book question
So far this is all per-player or per-market netting — what already runs in production. The B-Book question is just the same math, run again at the platform's level, with one extra decision: how much of each incoming bet should be kept on the platform's own book vs passed through to an exchange?
That decision is decideKeep returning a number f (kept fraction) between 0 and 1.
f = 1→ keep the whole bet on the book, no exchange.f = 0→ pass everything to the exchange.f = 0.4→ keep 40%, send 60% to exchange.
When we keep a fraction f of a bet, the bet's vector at the book level is f × v — every entry scaled by f.
So the routing/decision becomes:
Find the largest
fsuch that:
- keeping
f × vdoesn't make the book'sEmaxworse than the policy's worst-case capL, AND- the expected take on
f × v, divided by the bet's notional size, meets a minimum marginμ.
That's it. Two checks, take the smaller fraction. Let me show each piece.
8. Headroom — how big can f get before we breach the worst-case cap?
The book has a policy: "I will never let my book's Emax go below −L." So if my current book Emax = −300 and my cap is L = 1000, I have 1000 − 300 = 700 of room before I bust the cap.
For each outcome i, the bet's worst-case contribution per unit fraction is min(v[i], 0) (only the negative parts count for worst-case, like the accept phase). If we keep fraction f, we add f × min(v[i], 0) to Pmax[i].
We need every outcome's post-accept Pmax to satisfy Pmax[i] + f × min(v[i], 0) ≥ −L. Solving for f:
For each i where min(v[i], 0) < 0:
f_i = (Pmax[i] − (−L)) / |min(v[i], 0)|
= (Pmax[i] + L) / (−v[i]) [since v[i] is negative here]
f_max_risk = min over all such i of f_i clipped to [0, 1]
If min(v[i], 0) = 0 for some outcome (the bet doesn't hurt the book on that outcome), we skip it — no constraint from that side. In plain English: the kept fraction is limited by the most painful outcome, and only by how close that outcome was to the cap.
For a 2-outcome market this is a one-line calculation. For a 10-outcome market it's still one pass over the entries. For ladders it's the same (the ladder is just a longer vector).
Worked example: Book has Pmax = [+200, −300, +100], cap L = 1000. A back-bet has v = [+50, −150, +50].
- Outcome A:
min(v[A], 0) = 0→ skip. - Outcome B:
min(v[B], 0) = −150.f_B = (−300 + 1000) / 150 = 700/150 = 4.67. Clip to 1. - Outcome C:
min(v[C], 0) = 0→ skip.
f_max_risk = min(1, ...) = 1. The book has plenty of headroom; the worst-case test does not constrain us.
If instead the book were already Pmax = [+200, −920, +100] with the same cap and bet:
- Outcome B:
f_B = (−920 + 1000) / 150 = 80/150 ≈ 0.53.
Then f_max_risk = 0.53. We can keep at most 53% of this bet before busting the cap.
9. The expected-value gate — why μ exists
Headroom says "you can keep up to f." pt says "should you?"
Compute the bet's pt:
pt(v, p) = Σ p[i] × v[i]
If pt > 0, the bet is +EV (positive expected value) for the platform — keeping it makes us money on average. If pt < 0, we'd be losing money in expectation; pass to exchange.
But just "is pt positive" isn't strict enough. We want a margin: "expected take per pound of notional stake must be at least μ." So the gate is:
pt(v, p) ≥ μ × notional(v)
notional(v) is just the bet's stake size (its "footprint"). For a single bet with stake s, notional = s. For a fractionally-kept bet f × v, notional = f × s.
If the bet clears the gate, f_pt = 1. If not, f_pt = 0 — we don't keep any of it.
(You could make this smoother — "scale linearly with how good pt is" — but the binary gate is simpler and easier to reason about. Start there.)
10. Putting it together — decideKeep
f_max_risk = headroom_fraction(Pmax, v, L) # from §8
f_pt = 1 if pt(v, p) ≥ μ × notional(v) else 0
f_cap = policy.max_fraction # e.g. 1.0, or 0.5 for risky users
keptFraction = min(f_max_risk, f_pt, f_cap)
Done. Three numbers, take the smallest. That's the entire routing decision. The kept portion goes through the existing onAcceptAndMatch math (against the book's Pmax), the rest goes to the exchange.
Notice that all the things the current code spends hundreds of lines on — cascade tiers, terminal-retain, forwarding-matrix lookups, sharp-user routing — are just specific choices of L, μ, and f_cap at different scopes (book / agent / player). The math is the same.
11. The three knobs in plain English
Same content as the one-pager, restated here for completeness.
μ (mu) — "is this deal good enough to bother with?"
L is a ceiling on total losses. μ is a quality threshold on each new bet.
Think of a fruit stall: L is "I have room for 50 more kilos of fruit in the truck" — pure capacity. μ is "I won't buy any fruit unless I make at least 10p per kilo" — pure margin requirement. Two different decisions; both must pass.
Concretely, μ is the minimum expected profit, per £1 of stake, that we demand before keeping a bet on our book.
If μ = 0.04, we're saying: "I won't keep a bet unless I expect to earn 4 pence on every pound the user stakes."
- Bet stake = £100.
pt(v, p) = +£12→ margin per £ = 12/100 = 12%. Passesμ. - Bet stake = £100.
pt(v, p) = +£2→ margin per £ = 2/100 = 2%. Failsμ(we wanted ≥ 4%). Pass to exchange. - Bet stake = £100.
pt(v, p) = −£5→ margin per £ = −5%. Fails. Pass to exchange.
A bet can clear L (we have plenty of room) and still fail μ (the deal is bad). Or it can pass μ (it's a good deal) but fail L (we're out of room). The decision is min(f_risk, f_pt, f_cap) — every check must pass.
Why have μ at all?
- Friction cost. Exchanges cost us commission and capital tied up. If we're only making 1p per £ kept, the friction eats the margin — we should pass to exchange instead.
- Uncertainty cushion. Our
p(belief over outcomes) isn't exactly right. Demanding a margin gives us a buffer for being wrong aboutp. The higher our uncertainty, the higherμshould be.
f_cap — "don't put too many eggs in one basket"
f_cap is the simplest: a flat ceiling on how much of any single bet we keep.
If f_cap = 1.0, we're willing to keep 100% of a single bet. If f_cap = 0.4, we'll keep at most 40% of any single bet — the other 60% always goes to the exchange even if the bet is excellent.
It exists to limit concentration risk: even if L says we have huge capacity and μ says this particular bet is +EV, a single very large bet against the book could still be catastrophic if our p is wrong. f_cap says "spread it out anyway."
Common uses:
- Big-stake bets:
f_cap = 0.5for any bet over £10k stake. - Untrusted user buckets: new accounts get
f_cap = 0.2for the first month. - Volatile markets: in-play (live) markets get
f_cap = 0.3becausepchanges quickly.
When each fires — three quick stories
| Scenario | f_risk | f_pt | f_cap | Kept | Who said no |
|---|---|---|---|---|---|
| Tiny good bet on a busy market | 1.0 | 1.0 | 1.0 | 1.0 | nobody — keep it all |
| Tiny good bet but the book is already loaded on this market | 0.2 | 1.0 | 1.0 | 0.2 | L (capacity) |
| Huge bet at sharp-market odds (no edge) | 1.0 | 0.0 | 1.0 | 0.0 | μ (margin) |
| Huge bet at great odds, but it's a single £50k slug | 1.0 | 1.0 | 0.5 | 0.5 | f_cap (concentration) |
These three knobs decouple three different worries. You tune them mostly independently.
12. Sharp probability — how it slots in
p is the belief vector. The natural question is: whose belief? And: how does sharp routing actually work?
Step 1: p should be sharp-derived
The platform doesn't make p up. We compute it from sharp markets (Pinnacle, Betfair sharp prices) by devigging — removing the bookmaker's margin to recover implied "true" probabilities.
Devigging in plain English: a bookmaker quotes prices that sum to slightly more than 100% probability so they earn a margin. If A is quoted at 2.10, B at 2.10 (each implying ~47.6%), the implied total is 95.2% which is impossible — the bookmaker's margin is the 4.8% surplus. We scale every implied probability by 1/95.2% so they sum to 100%, and now we have an unbiased estimate. That's p_sharp.
We refresh p_sharp continuously from the sharp feeds. Every time we route a bet, we use the freshest available p_sharp.
Step 2: pt(v, p_sharp) tells us if the offered price is good for the book
Here's the crucial thing about sharp p: if our user is betting at prices worse than p_sharp implies, pt(v, p_sharp) is automatically positive. That's the platform's margin showing up as expected profit.
Example. Sharp markets say p_A = 0.5, p_B = 0.5 (a coin flip). We offer the user 1.90 on A (which implies 52.6% — worse than the true 50%). User backs A with £100.
v = [−90, +100] (we owe £90 if A wins, win £100 if B wins).
pt = 0.5 × (−90) + 0.5 × (+100) = −45 + 50 = +5.
Expected take = +£5 on a £100 stake = 5% margin. The platform's edge is exactly the overround we baked into the offered odds, automatically.
If we instead offered 2.10 on A (better-than-sharp odds for the user — implying 47.6%):
v = [−110, +100].
pt = 0.5 × (−110) + 0.5 × (+100) = −55 + 50 = −5.
Expected take = −5%. We'd be paying the user. μ = 4% rejects this; the bet goes to the exchange.
μ becomes the question "how much edge over sharp do we demand?" If μ = 0 we'll keep any bet that's even microscopically in our favour. If μ = 0.04 we demand a 4-point edge.
Step 3: Adjust μ per user — the "sharp user" decision
Sharp users are players who consistently beat the market. The book might still be priced fairly against sharp p_sharp, but this user's actions hint they know something the sharp markets don't yet.
For these users, our p_sharp underestimates their winning probability — they have private information (closing line moves, model-driven picks, expert knowledge). So a bet that looks +5% to us is probably 0% or worse in reality.
The clean fix: raise μ for that user.
- A regular punter:
μ = 0.04— 4 points of edge required. - A confirmed sharp user (consistently picks bets that close at better-than-our-offered prices):
μ = 0.50— 50 points of edge required. They essentially never clear it. Result: their bets always go to the exchange. - Brand-new account, unknown skill:
μ = 0.10— demand a bigger margin until we have a track record.
The same single function decideKeep handles all of these. You don't need a separate "sharp user → 100% exchange" boolean anywhere. That behaviour falls out of μ being unattainably high for that user.
Step 4: Adjust μ per market — confidence in p
Some markets we trust more than others:
- Heavily traded mature pre-match market: sharp feeds are tight, devigged
pis reliable → lowμ. - Thin / pre-launch / live in-play market: sharp feeds are stale or absent → high
μ(margin for being wrong).
So μ becomes a function μ(user, market, time_to_event). The decision stays the same — decideKeep just looks up μ from a small policy table indexed by those keys.
Step 5: Adjust p directly — confidence-weighted blend
A subtler control: when sharp feeds disagree with each other, blend them, and widen the probability you compare against to reflect uncertainty.
If Pinnacle says p_A = 0.50 and Betfair sharp says p_A = 0.55, we might use p_A = 0.525 (the average). But we also tell the system "treat this as 0.475 to 0.575" — and when computing pt, use the worst end of that range for ourselves:
- For computing book-friendly bet evaluation: pretend
p_A = 0.475(gives lowerpt, harder to clearμ). - For computing book-hostile (lay) evaluation: pretend
p_A = 0.575(gives lowerpt, harder to clearμ).
This is conservative p. It doesn't change the math — pt is still Σ p[i] × v[i] — it just shifts which p you feed in based on how confident you are. Same function, different inputs.
Step 6: Sharp signal can adjust f_cap
f_cap is the concentration knob. Sharp users get a tighter f_cap even when they do clear μ (because we want diversification when we're trading against someone smarter than us). So f_cap also becomes a function f_cap(user, market).
Putting sharp routing together
For a single bet:
p = devigged sharp probability (refreshed continuously)
L = book-level worst-case cap (large, mostly capacity-limited)
μ(user, mkt) = required margin — rises with user-sharpness, market-uncertainty
f_cap(user) = concentration cap — falls with user-sharpness, stake size
pt = Σ p[i] × v[i] ← uses sharp probability
f_risk = headroom(L, Pmax, v) ← uses L
f_pt = 1 if pt ≥ μ × stake else 0
f = min(f_risk, f_pt, f_cap)
kept on book = f × v
to exchange = (1 − f) × v
Sharp routing isn't a separate code path. It's:
pderived from sharp feeds (drivespt).μraised for sharp users (rejects bets that look only marginally good).f_captightened for sharp users (limits damage when we do take a piece).
Every B-Book decision the platform makes today — sharp routing, win caps, cascade %, terminal-retain — is some combination of (p, L, μ, f_cap) plus the existing netting math. The decision logic is one function. The differences between users, markets, and conditions live in the policy table that feeds (L, μ, f_cap) into it.
13. End-to-end worked example
Let's trace one full bet through the engine. Three-outcome market, current state:
- Book's
Pmax = [+200, −300, +100],Emax = −300. - Platform belief
p = [0.30, 0.40, 0.30]. - Policy:
L_worstCase = 800,μ_minPt = 0.04,f_cap = 1.0.
A user places a back-bet on outcome B, stake £100, odds 3.5. Build v:
v[A] = +100,v[B] = −100 × (3.5 − 1) = −250,v[C] = +100.
So v = [+100, −250, +100].
Step 1 — pt:
pt(v, p) = 0.30 × 100 + 0.40 × (−250) + 0.30 × 100
= 30 − 100 + 30
= −40
Expected take is negative £40. Notional is 100. Margin check: −40 ≥ 0.04 × 100 = 4? No, −40 < 4. f_pt = 0.
keptFraction = min(f_max_risk, 0, 1.0) = 0. The platform keeps none of this bet; the whole thing goes to the exchange. (Which is correct: at odds 3.5 on outcome B when sharp probability is only 40%, the bet is +EV for the user, so the platform should pass it through.)
Now swap: same bet but at odds 2.2 instead. Vector becomes:
v[A] = +100,v[B] = −100 × (2.2 − 1) = −120,v[C] = +100.
Step 1 — pt:
pt = 0.30 × 100 + 0.40 × (−120) + 0.30 × 100
= 30 − 48 + 30
= +12
12 ≥ 4? Yes. f_pt = 1.
Step 2 — headroom:
- Only outcome B is negative:
f_B = (−300 + 800) / 120 = 500/120 ≈ 4.17. Clip to 1. f_max_risk = 1.
keptFraction = min(1, 1, 1) = 1 → keep the whole bet on the book.
Step 3 — apply via netting engine, the existing onAcceptAndMatch on the book's Pmax:
Accept phase (only negative parts of v enter):
- Pessimistic vector:
[0, −120, 0]. Pmax_after_accept = [+200, −420, +100].Emax_after_accept = −420.acr_book = (−300) − (−420) = 120. £120 of platform risk capital locked.
Match phase (positive parts enter):
- Optimistic vector:
[+100, 0, +100]. Pmax_after_match = [+300, −420, +200].Emax_after_match = −420(unchanged — match doesn't make worst case better here).rc_book = (−420) − (−420) = 0. £0 released.
So the book's new state is Pmax = [+300, −420, +200], Emax = −420, and £120 was added to the book's locked-capital pool.
When the market eventually settles at, say, outcome A, the engine runs onReverseAcceptAndMatch(Pmax, v_kept):
Pmax_after_settle = [+300 − 100, −420 − (−120), +200 − 100] = [+200, −300, +100]— exactly where we started before this bet.- Realised platform P&L on this bet =
v_kept[A] = +100. Credit back to the risk pool.
The book is whole; the win-payout flow at the player layer handles the user's side. No special-case code for outcomes A vs B vs C — just an index lookup into the vector.
14. The ladder structure — same vector, longer
Cricket totals, score-bands, handicap markets: instead of "horse A, B, C," the outcome space is something like "0 runs, 1 run, 2 runs, ..., 200 runs." 201 outcomes. The vector v has 201 entries.
A back-bet on "team scores 100 or more at odds 1.8" with stake 50:
- For all indices
i ≥ 100:v[i] = −50 × (1.8 − 1) = −40(we owe the player). - For all indices
i < 100:v[i] = +50(player loses).
The vector is now a step function: [+50, +50, ..., +50, −40, −40, ..., −40] with the step at index 100.
Every formula above keeps working unchanged. Emax = min(...) still picks the worst outcome (here −40). pt = Σ p[i] × v[i] uses the platform's probability distribution over the ladder. The headroom calculation looks at every losing index and takes the minimum constraint.
The ladder doesn't introduce new math — it just makes N bigger.
15. The invariants — what's guaranteed to be true
The math has four properties that the engine and the routing function preserve no matter what:
acr ≥ 0always. Locked capital can't go negative — accepting can only make the worst case worse or equal.rc ≥ 0always. Released capital can't go negative — matching can only make the worst case better or equal.Emax_before − Emax_after ≤ |Δv|per scope. The worst case can't move by more than the bet's worst contribution.keptFraction ∈ [0, 1]. You can't keep more than the whole bet or less than none.
These four invariants are enough that you can write a test that runs a million random bets through the engine and asserts none of them are ever violated. That test (plus the existing eligibility / odds-change / ladder tests) is your safety net.
16. Mapping back to platform vocabulary
| What I called it here | What it's called in code |
|---|---|
| The "what happens to us" table | pnlVector, v |
| Running tally of all open bets | pmaxVector, Pmax |
| The worst row of the tally | emax, Emax |
| Expected platform P&L | pt (new) |
| Locked capital on accept | acr (already in AcceptResult) |
| Released capital on match | rc (already in MatchResult) |
| Kept fraction on book | keptFraction (new column on Order) |
| Per-scope worst-case cap | policy.L_worstCase (new) |
| Per-scope minimum margin | policy.μ_minPt (new) |
| Per-scope concentration cap | policy.f_cap (new) |
Three new things in the math: pt, keptFraction, and policy.{L, μ, f_cap}. Everything else is already there in nettingEngine.ts — it just needs to be called twice (once at the player scope, once at the book scope) instead of once.
That's the entire system. Three questions ("worst case?", "expected case?", "how much do we keep?"), three formulas, and one routing decision that's literally min of three numbers.