0024. Equity-anchored sizing with explicit Risk_config

Status: Accepted Date: 2026-05-19

#Context

Portfolio Management runs two construction pathways: alpha-driven single-asset (a directional view + dimensionless conviction) and pair_mean_reversion (a β-hedged two-leg construction). Until this ADR they shared no upstream input shape, no sizing logic, no clipping invocation. Concretely:

This ADR settles the construction-time arithmetic and risk discipline for PM with a single decomposition all current and future construction policies share.

#Decision

Adopt the construction → sizing → clipping decomposition canonical in portfolio-management literature (Grinold-Kahn, Pedersen). Concretely:

  1. Construction policies emit dimensionless intents, not sized proposals. The Construction_intent.t sum type has two variants:

    • Scalar { direction; strength; ... } for single-asset alpha-driven decisions;
    • Coupled { legs; coupling; ... } for multi-leg policies (pair, future basket / factor / risk-parity). The Σ |weight| ≤ 1 invariant is enforced by the smart constructor; a shared Coupling.t ties the legs together.
  2. Sizing is a pluggable pure function from intent to proposal, captured by Sizing_policy.S. The single implementation today is Equity_proportional: target_qty = book_equity × weight / mark. Sentinel behaviour on edges: non-positive mark, zero book_equity, or Direction.Flat collapses the leg to target_qty = 0 rather than raising — so a stale mark cache cannot stop the rest of the proposal.

  3. Capital is equity-anchored, not notional-cap-anchored. book_equity is computed as Risk_config.risk_budget_fraction × total_equity_for(book). The risk_budget_fraction ∈ [0, 1] is an operator-level capital allocation between books — explicit and configurable, replacing the implicit notional_cap_for placeholder.

  4. Clipping is coupling-aware. Risk_policy.clip now distinguishes independent legs (coupling = None) from members of a coupling group (coupling = Some c). In the per-instrument pass, legs of one group are scaled by a single common factor sufficient to bring the worst- offending leg under the cap; the gross-exposure pass already preserved ratios across all legs. β-symmetry and basket weights survive clipping by construction, not by accident.

  5. Per-book risk configuration lives in Risk_config. The aggregate carries:

    • risk_budget_fraction (sizing primitive);
    • Risk_limits.t (clipping primitive);
    • construction_source (the single Source.t permitted to publish targets to this book — "one construction source per book" as a structural invariant, formalised in the aggregate, enforced via Risk_config.authorises in the unified handler).
  6. A unified handler funnels every intent through the same pipeline. Build_target_on_construction_intent.handle does: resolve Risk_config → enforce source authorisation → compute book_equity → call per-book Sizing_policy.sizeRisk_policy.clipTarget_portfolio.apply_proposal → publish Target_set. Both Direction_changed (alpha) and pair-MR's on_bar output project to a Construction_intent.t and feed this single handler.

  7. domain/sizing.ml is removed. The migrated-but-unwired scaffold from M3 has no place in the new decomposition. Equity-aware sizing now lives in Equity_proportional and future variations (Volatility_target, Kelly_fraction, Inverse_vol, Risk_parity_legs) plug in as additional modules under domain/sizing_policy/ against the same Sizing_policy.S module type.

#Alternatives considered

#A. Keep notional-cap as the sizing anchor

Retain notional_cap_for(book) as the cap-as-budget primitive. Construction policies size against a configured cap; equity does not enter sizing.

Rejected because:

#B. Collapse alpha and construction into a single weight-vector

abstraction (qstrader-style)

Have every policy — alpha-driven, pair, basket — return dict[Asset, weight]. A single OrderSizer materialises the dict into quantities using equity.

Rejected because:

The sum-type Construction_intent preserves the structural distinction between scalar and coupled origins while still unifying downstream — every variant feeds the same handler. This is the unification at the right level of abstraction.

#C. Per-policy anchor choice (some policies equity-anchored,

some notional-anchored)

Allow Sizing_policy.S implementations to differ on what they read: Equity_proportional reads book_equity, a hypothetical Notional_fixed reads a per-book configured cap.

Rejected for today because there is no current driver — every present policy reads equity. Holding both modalities open in the abstraction without an implementation that needs it is a premature-abstraction smell (YAGNI). If a future driver appears, Sizing_policy.S is wide enough to accommodate (the [config] type is per-implementation): a Notional_fixed module can take a notional cap in its config and produce a proposal independent of equity, sitting alongside Equity_proportional. The decomposition does not preclude C; it just does not pre-build for it.

#Consequences

Easier:

Harder:

To watch for:

#References