Raxx · internal docs

internal · gated

ADR 0146 — Options buying power + margin fidelity model for MBT paper trading

Status: Accepted: Reg-T 2:1 (operator decision 2026-07-30 — recorded on epic #3483, see #3483 epic comment) Date: 2026-07-29 UTC Deciders: software-architect, operator (Kristerpher) — decision on margin scope (§Alternatives, Q2) required before sub-cards are built Scope: Raptor (backend_v2/api/services/mbt_fill_engine.py, paper_accounts, paper_orders submission paths)


Update — 2026-07-30 (Status change: Proposed → Accepted: Reg-T 2:1)

The operator resolved Open Question 2 (below) by accepting Alternative B: full Reg-T 2:1 margin math ships in this pass, not deferred. This overrides the original recommendation in §Decision/§Alternatives ("cash-account fidelity only for this pass") — that original reasoning is preserved below unedited for the historical record, but it no longer reflects what shipped.

Implemented in #4357 (buying_power_multiplier live: 2.0 for account_type='margin', Reg-T 50%/25% initial/maintenance margin on open long equity positions),

4358 (the collateral-reservation model this ADR describes, unchanged from

the original design), and #4359 (the pre-fill rejection layer, unflagged). Margin CALL / forced-liquidation enforcement remains explicitly out of scope (see #4357 Non-goals) — this pass ships margin math, not margin enforcement.


Context

The MBT fill engine currently has no concept of buying-power sufficiency: get_account() returns buying_power = cash_balance verbatim, and no order-submission call site checks cash or collateral before writing a fill. This means (a) equity orders can drive cash_balance arbitrarily negative, and (b) selling a cash-secured put or a covered call credits premium with zero collateral hold — the account behaves as if every options position were fully unsecured margin, regardless of account_type. Operator directive A requires the paper engine to emulate Alpaca's paper semantics closely enough that a future live-Alpaca cutover is a swap, not a rewrite; Alpaca rejects orders that exceed buying power, and Alpaca's options buying power calculation reserves cash for cash-secured puts and requires 100 shares held (or a spread-defined max-loss) for covered/vertical positions. Operator directive B requires options buying power math (CSP reserve, CC collateral) explicitly.

Three decisions were required: (1) whether to model buying power at all in v1 (vs. deferring, as today), (2) whether to implement full Reg-T 2:1 margin math for account_type='margin' accounts or keep v1 cash-account-only, and (3) how options collateral is represented — a derived calculation at check-time, or a persisted reservation ledger.


Decision

MBT ships a persisted collateral-reservation model: a new paper_collateral_reservations table holds one row per open options position that requires collateral (cash-secured put: cash reserved = strike * 100 * contracts; covered call: the underlying long-share position itself is the collateral, tracked by linkage rather than a cash reservation). paper_accounts.options_buying_power is a maintained column, recomputed on every fill and on the MTM sweep, equal to cash_balance - SUM(open reservations). Every order-submission path (single-leg equity, single-leg option, multi-leg) gains a pre-fill buying-power check: reject with 400 {"reject_reason": "insufficient_buying_power"} before any paper_orders/paper_fills/paper_positions write when the order's cost (equity) or collateral requirement (naked/cash-secured option) exceeds options_buying_power. Covered positions (short call fully covered by an existing long-share position of equal or greater size) do not consume additional cash collateral — the coverage check inspects paper_positions for the paired underlying position at submit time.

Margin scope for this pass: cash-account fidelity only. buying_power_multiplier ships as a schema column (default 1.0) but the 2:1 Reg-T multiplier for account_type='margin' accounts is not implemented in this pass — see Alternatives below. This is a scope-reduction from the original mbt-paper-trading-engine.md promise and is called out explicitly rather than silently dropped.


Consequences

Positive

Negative / risks

Neutral


Alternatives considered

Alternative A — Derived (non-persisted) buying-power check

Compute collateral requirements on the fly from open paper_positions at submit time, with no reservation table. Rejected because it recomputes the same OCC-parsing + coverage-matching logic on every order submission (performance + correctness-drift risk) and gives the UI nothing concrete to display for "how much is reserved right now." A persisted ledger is one extra table for a durable, auditable answer.

Alternative B — Full Reg-T 2:1 margin in this pass

Implement margin buying power (cash * 2 minus maintenance requirements) alongside the options collateral work. Rejected for this pass — not because it's wrong, but because it roughly doubles the engine surface being changed (initial margin, maintenance margin, margin call semantics, all interacting with the same collateral reservations) and the original design doc's promise of Reg-T margin was never load-bearing for anything downstream (no card references margin-specific behavior). Flagged as an explicit operator decision in the reconciliation doc's Open Questions rather than silently deferred or silently built.


Security / GDPR checklist


Revisit when

Reg-T margin math (Alternative B) becomes load-bearing — e.g. a live-Alpaca-cutover card needs margin parity, or a product card wants to market "margin trading" as a paper-trading feature distinct from cash accounts.