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
- Closes the single highest-priority Alpaca-fidelity gap: orders now reject the way a real Alpaca account would, which is also what makes the paper-first profitability gate meaningful (a strategy that "worked" only because the account went infinitely negative is not a validated strategy).
- A persisted reservation ledger (vs. a derived-at-check-time calculation) means collateral is auditable per-position and survives partial-fill/replace flows without recomputation drift, and gives the Simulate UI a concrete number to show ("$4,800 reserved for 1 open CSP") rather than an opaque buying-power delta.
- Covered-call coverage detection reuses the existing
paper_positionsread path — no new position-tracking mechanism.
Negative / risks
- Reservation bookkeeping adds a new failure mode: a reservation row orphaned by a bug (position closed without releasing its reservation) silently locks buying power. Mitigate with a reconciliation assertion in the nightly EOD snapshot job (PAPER-21) that
SUM(open reservations) <= cash_balanceand pages on violation. - Deferring margin math means
account_type='margin'remains cosmetically inert in this pass — a user who selects "margin" sees no behavioral difference from "cash." This needs explicit product copy ("margin accounts behave as cash accounts in paper trading v1") so it doesn't read as a bug later. - Retroactively fixing negative
cash_balancerows (pre-existing bug window) requires a one-time reconciliation sweep before theNOT VALIDCHECK can be validated — tracked in the reconciliation doc's Migrations section, not repeated here.
Neutral
- The
buying_power_multipliercolumn ships now so a later margin-math pass is additive (flip the multiplier, extend the check formula) rather than another schema migration.
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
- PII collected: None beyond existing
paper_*financial data already covered by parent ADR-0108. - Retention period:
paper_collateral_reservationsfollows the same tier-based retention aspaper_positions(its parent); purged by the existing PAPER-22 retention job once extended to cover this table. - Deletion on DSR:
CASCADE DELETEonpaper_accounts/paper_positionscoverspaper_collateral_reservationsvia its FKs. - Audit trail: Every reservation create/release writes an
audit_logrow (paper_collateral_reserved/paper_collateral_released), matching the existing per-fill audit pattern. - Stored credentials: None; this ADR touches no credential surface.
- Breach notification path: No new sensitive-data category introduced; standard ADR-0003 path applies.
- Secrets location + rotation: N/A — no secrets introduced.
- Kill-switch: Inherits
MBT_TRADING_DISABLED/MBT_NEW_ORDERS_DISABLED; the buying-power check itself has no independent kill-switch since disabling it would reintroduce the correctness bug this ADR fixes — if it needs to be bypassed operationally, that's aMBT_NEW_ORDERS_DISABLEDsituation, not a partial-feature toggle.
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.