deriveOrderOutcome — the single settlement-outcome derivation (fn2)
The settlement axis (function 2) is a single pure derivation for the per-order, provider-reported settlement path. No existing wiring breaks — it is a behavior-preserving extraction, not a contract change.
Implemented scope (revised after grounding)
deriveOrderOutcome is now exported from settlement.ts and settleOrderFromBetsApi routes
through it (the previously-inline quarter-line refinement + computeOrderPnl + pnl-sign→outcome block,
extracted verbatim). Grounding corrected the original "both paths call it" aim: the market-wide
netting path (settleMarketForUser) computes its outcome from the netting vector, not a provider
result, and uses a different rule (gte(0)→win, no void-on-zero). Routing it through
deriveOrderOutcome would be a settlement-math change, not an extraction — so it is deliberately
not routed through. deriveOrderOutcome is the single home for the per-order provider-result
derivation; unifying the netting path is a separate, guardian-reviewed decision if ever wanted.
Outcome set
settlementOutcome ∈ { win, lose, void, half_win, half_lose }. push is dropped — neither
provider ever emits it (Bifrost maps PUSH → void; Betfair whole-line push arrives as VOIDED;
racing dead-heats are a partial win, not a push). (Before removing from the type: verify no existing
settlementOutcome = 'push' rows in prod — data check at build time.)
Half-win / half-lose derivation (Q7 — logic to be revisited by Bhargav)
- Bifrost sends it explicitly (
HALF_WON/HALF_LOST) → direct map. - Betfair derives it via
classifyQuarterLineOutcome(settlement.ts:303): quarter-lines only (line mod 0.5 ≠ 0); the provider's cleared profit is compared as a ratio against the locally computed full P&L (computeOrderPnl(matchedStake, placedOdds)) — full band [0.8,1.2], half band [0.4,0.6]; PT-hedge-slice aware. The provider figure only selects full-vs-half. - No-guesswork is enforced: null profit on a quarter-line, or a ratio in neither band, throws
FINANCIAL_INTEGRITYand leaves the orderacceptedfor retry.
P/L basis (Q10)
fn2 never books the provider's number. Booked P&L is always
computeOrderPnl(matchedStake, placedOdds, outcome), settled in user currency from the order's
outcome; providerPnl is stored audit-only + a divergence check (§3.9 R3).
Partial void (Q11) — settlement axis, not the match axis
A partial void does NOT make an order partially_matched. Voided stake did match (it is only
refunded at settlement; an unmatched bet in a voided market lapses). Therefore:
- Match axis (
deriveOrderMatchStatus): the "did-match" amount =sizeMatched + sizeVoided. AnM=60, V=40order ismatched(60+40=100=R), not partial. - Settlement axis (fn2):
settlementOutcomereflects the non-voided portion's result (win/lose); the voided part is the separatevoidedStakerefund; P&L is computed on(matchedStake − voided). Lossless — refund and win/lose are both shown, never conflated.
One order → one outcome (Q12)
Even for Bifrost session/ladder (priceIndex) markets, one order settles on its single line to one
settlementOutcome (win/lose/half). The runs-vector handling lives in the netting engine, not here.
Resettlement (Q13 — verified, no change)
reverseSettlement (reversalService.ts:86) performs a full double-entry reversal via
recordDoubleEntry — settlement credit (settlement_reversal), commission (commission_reversal),
and margin — fail-closed (aborts rather than driving balance negative; no clamping), then re-settles
through the normal settleOrderFromBetsApi path. Status flows settled → accepted → settled through
the same statuses defined here; the corrected result is shown silently. This is the existing behavior —
the new functions must preserve it.