Skip to main content

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_INTEGRITY and leaves the order accepted for 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. An M=60, V=40 order is matched (60+40=100=R), not partial.
  • Settlement axis (fn2): settlementOutcome reflects the non-voided portion's result (win/lose); the voided part is the separate voidedStake refund; 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.