0008 — Margin model for short selling

#Status

Accepted.

#Context

Before this change Portfolio.try_reserve rejected any Sell whose quantity exceeded the long position with Insufficient_qty. Opening or growing a short was therefore impossible through the reserve → commit_fill cycle, even though the fill side already supports signed positions, VWAP, realized PnL on direction flip, and mark-to-market equity for shorts. Strategies wanting to short had no path through the application layer.

Real brokerage shorting is a margined operation: the broker requires collateral (initial margin) when the short is opened, and the cap against which collateral is checked — buying power — is not just idle cash but the account's equity, with long positions counting toward the cap at a haircut.

#Decision

Add a margin model to the Portfolio aggregate so that a Sell can be reserved against buying power instead of just position quantity.

#Reservation shape

A reservation is split into two portions:

A single Reservation.t carries both portions plus a single per_unit_collateral; its identity (id) is stable across the cover + open split so the cross-BC saga (broker echoes the id back) keeps one id per logical order.

#Buying power and the gating check

buying_power = available_cash
             + Σ |position_qty| × mark × haircut

where mark is supplied by the caller as Instrument.t → Decimal.t option (precedent: Portfolio.equity). When mark returns None, the position's avg_price is used as a fallback so the model degrades gracefully when no live price source is plugged in.

try_reserve gating per side:

The pre-existing Insufficient_qty variant is retired — under the cover/open split a Sell can no longer be qty-bound (the cover is naturally clamped to the long, and the open is bounded by cash, not qty).

#Margin policy as a domain Strategy

margin_pct and haircut are per-instrument values. The domain exposes a Strategy:

type Margin_policy.t = Core.Instrument.t → margin_terms
and margin_terms = { margin_pct; haircut }

This is a domain-level Strategy, not a Hexagonal Port: the algorithm of "compute buying power, gate the reservation" is pure domain knowledge, with no IO. Concrete data inputs (a live НСР table from the broker, accumulated SMA state for a Reg T-style account) will be supplied via Repository ports inside specific strategy implementations, not via the strategy interface itself.

For this round the composition root provides a stub — Margin_policy.constant ~margin_pct ~haircut — that returns the same terms for every instrument. A live per-instrument source replaces the stub when broker integration is wired without changing any domain or application code.

#Cover-first partial-fill attribution

commit_partial_fill depletes cover_qty before open_qty. The open portion is what holds collateral; depleting it last keeps the collateral block stable for as long as possible during a multi-leg fill stream.

#Consequences

#Alternatives considered

#Out of scope