Skip to content

docs: ADR-066 account-level initial-margin admission - #420

Merged
rustyeddy merged 2 commits into
mainfrom
docs/410-adr-066-initial-margin
Sep 27, 2026
Merged

rustyeddy merged 2 commits into
mainfrom
docs/410-adr-066-initial-margin

Conversation

@rustyeddy

@rustyeddy rustyeddy commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

What changed

Adds ADR-066 (docs/arch/adr-066-account-initial-margin-admission.org) and indexes it in adr-decisions.org. No code changes.

It settles every decision #410 lists:

Question Decision
Terminology initial_margin_ratio = equity required per unit of gross notional (1.0 / 0.5 / 0.25 → 1× / 2× / 4×). maintenance_margin_ratio reserved for later.
Invariant Admission only: prospective gross × ratio ≤ equity, checked only for exposure increases. Drift over the limit is not liquidated; further increases stay blocked and de-risking stays allowed.
Gross vs net Gross, never netted.
Reversals Resulting position via resultingPosition (100L → 100S counts as 100).
De-risking Always admitted; reduces to ADR-034's magnitude test and is decided from quantities alone, so no ReferencePrice or marks are needed. Scoped to this rule, not every future rule.
Valuation basis Proposal's instrument at ReferencePrice (admission) or fill price (fill time), for both before and after. Other positions at per-position snapshot marks with an as-of time (#412). Never AvgPrice. A missing mark is a classifiable error. Staleness is defined against the scheduler's phases.
Admission vs fill One shared calculation (#411). Admission estimates from the reference price and pre-fee equity; the fill-time check validates the actual post-fill state (fill price, commission-adjusted equity). Neither is uniformly stricter.
Fill-time lifecycle No ADR-018 amendment. A market order refused at Submit → Rejected (ReasonInsufficientMargin). A resting order refused at trigger → the existing broker-initiated buildInternalCancellation path (as in #352), plus a new additive Order.CancelReason *Rejection.
Bracket entries Found while researching: a broker rejection of an unfilled ADR-059 entry would currently abort the run. Decided that it is harmless, like an entry-side risk rejection; #415 implements this.
Open orders Not counted at admission in v1; the simulator's fill check covers them. Live reservation is deferred to M6/M8.
Fees See "Admission vs fill" above.
Scope Configures only the simulated account and backtest risk engine, from one value. The sim with no margin model keeps today's behavior, explicitly recorded in ModelInfo.
Ratio 1.0 "Unlevered margin account", not a cash account; shorting allowed. allow_short deferred.
Per-instrument margin Sum of per-position required-margin amounts; v1 = |notional| × ratio. Fixed dollars per contract fits later. Designed for, not implemented.
Currency One account currency; mismatches are a classifiable error.
Default 1.0. Leveraged fixtures change; #414 lists them. No finite ratio reproduces the old unbounded simulator.

Why

#410 is step 1 of the Account Margin Admission milestone (#409). #411–#417 implement against these decisions.

How it was tested

Documentation only. I checked the claims against the code:

  • the transition graph in internal/order/transition.go
  • ReasonInsufficientMargin
  • the Order.Rejection invariant
  • buildInternalCancellation
  • scheduler Phase 2/3 ordering
  • submitBracketOutcome
  • ADR-028 ModelInfo
  • the empty risk.NewEngine() in the backtest service

Documentation changed

  • docs/arch/adr-066-account-initial-margin-admission.org (new)
  • docs/arch/adr-decisions.org (index row and note)

Closes #410

🤖 Generated with Claude Code

Record the design for account-level initial-margin admission in the
simulator/backtester before implementation:

- initial_margin_ratio as an admission-only invariant: prospective
  gross notional x ratio <= equity. No maintenance margin or
  liquidation in v1.
- Gross exposure (never netted), evaluated on the resulting position,
  with ADR-034's de-risking exemption.
- Valuation basis: the proposal at its reference or fill price, other
  positions at per-position snapshot marks, never AvgPrice. Mark
  staleness is defined.
- One shared calculation, enforced at admission (risk rule) and at
  fill time (an ADR-028-style simulator model recorded in the
  manifest).
- Order lifecycle without amending ADR-018: a market order refused at
  Submit is Rejected (ReasonInsufficientMargin). A resting order
  refused at trigger is canceled through the existing broker-initiated
  cancel path, with a new additive Order.CancelReason.
- A broker margin rejection of an unfilled bracket entry is harmless
  and does not abort the run.
- Open orders don't count at admission; live pending-order
  reservation is deferred.
- Ratio 1.0 means unlevered, not cash-account (shorting allowed).
- One currency only. Per-instrument rates are designed for but not
  implemented. The 1.0 default changes leveraged fixtures.

Closes #410

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The ADR contains unresolved correctness and consistency issues that should be addressed before approval.

Review effort: Lite
Findings: 4 Low severity

Open (4)
What changed in this PR

Adds ADR-066 defining account-level initial-margin admission semantics and registers it in the ADR index.

Changes:

  • Documents margin, valuation, de-risking, lifecycle, and compatibility decisions.
  • Adds no production code changes.
File Summary
docs/​arch/​adr-decisions.org Registers and summarizes ADR-066.
docs/​arch/​adr-066-account-initial-margin-admission.org Defines the account-level initial-margin admission contract.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +161 to +164
commission. The fill-time check is therefore never less conservative
than admission. At exactly the limit, an order admitted before fees can
be refused at fill because of its own commission. That is deliberate:
the invariant is about what the account actually holds after the fill.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ad745bc: the fill-time check is now described as validating the actual post-fill state (actual fill price plus commission-adjusted equity), and the ADR says explicitly that neither check is uniformly stricter.

Comment on lines +166 to +172
The calculation is defined per position — Σ notionalᵢ × rateᵢ — and v1
supplies the single account-level ratio as every position's rate. A
later per-listing or per-instrument margin policy (OANDA's per-instrument
=marginRate=, or futures' fixed dollars per contract) changes only that
per-position input, not the aggregate calculation or the meaning of the
account-level setting. v1 designs for this but does not implement it or
test mixed rates.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ad745bc: required margin is now Σ required_margin(positionᵢ, priceᵢ, policyᵢ). v1 is |notional| × ratio, and a later futures policy can return a fixed amount per contract with no pseudo-rate. docs/account-risk.md is aligned too.

Comment on lines +185 to +187
- A missing =ReferencePrice= or mark, a non-positive ratio, or a
currency mismatch is a classifiable error (=ErrInsufficientRuleInput=
or =ErrInvalidRule=), never an approval.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ad745bc: de-risking is decided from quantities alone, via resultingPosition. Any de-risking proposal (a full close, a partial reduction, or a reversal that doesn't grow the magnitude) needs no ReferencePrice or marks, matching the existing ADR-034 rules. Prices are required only for exposure-increasing proposals.

Comment on lines +295 to +296
every changed fixture and why. A run can reproduce the old behavior
explicitly with a smaller ratio, for example =0.25=.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ad745bc: the ADR now says a leveraged experiment can choose an appropriate smaller ratio (0.25 = up to 4×), that no finite ratio reproduces the old unbounded simulator, and that the 4.63× SPY run would be rejected even at 0.25.

@rustyeddy rustyeddy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed PR #420 at head 77724b8e2660fc1a9d9b05e7194a5d9e0fbac016.

Overall, ADR-066 is strong and settles the right set of decisions for the margin milestone. The separation between admission-time risk, fill-time simulator enforcement, and future maintenance-margin behavior is especially clear.

I agree with all four Copilot findings; these are small edits, but they are correctness issues in an ADR that later code will implement against.

1. Reword the fill-time commission/conservatism claim

The ADR currently says the fill-time check is never less conservative than admission because it uses post-commission equity.

That is not universally true because the fill price can move favorably relative to the reference price and reduce prospective notional enough to offset the fee.

I suggest describing the fill-time check as validation of the actual prospective post-fill state:

admission:
  reference price + pre-fee snapshot equity

fill time:
  actual fill price + actual commission-adjusted equity

The point is not that one is always stricter; the point is that fill-time enforcement evaluates the state Trader is actually about to book.

2. Define future extensibility as per-position required-margin amounts, not only rates

I agree with Copilot here, and this aligns with the wording we added to #411: design for future instrument-specific margin without implementing it now.

The ADR currently defines the future aggregate as:

Σ notional_i * rate_i

That works for equities/FX percentage margin, but futures margin is naturally:

contracts * dollars_per_contract

A more general model is:

required_margin = Σ required_margin(position_i, mark_i, policy_i)

For v1:

required_margin(position) =
    abs(notional(position)) * initial_margin_ratio

Later, the per-position function can use a listing rate, fixed dollars per contract, etc. This preserves the v1 scope while avoiding an API shape that assumes all margin policies are percentage-based.

3. Do not require ReferencePrice for a complete close

The ADR says a missing ReferencePrice is always an insufficient-input error.

That conflicts with the de-risking rule we just established and with existing ADR-034 semantics. If the resulting position is zero, no resulting notional needs to be valued for admission; an exact close should not be blocked solely because ReferencePrice is absent.

I would make the rule:

ReferencePrice is required when valuation of a remaining or increased position is needed. A proposal that fully closes the position may be admitted under the de-risking exemption without ReferencePrice, provided the rule can determine from quantity/side that resulting exposure is zero.

That keeps the "never trap an over-limit account" property intact.

4. Fix the compatibility example

This sentence is inaccurate:

A run can reproduce the old behavior explicitly with a smaller ratio, for example 0.25.

The motivating SPY run was about 4.63x exposure, while 0.25 permits only 4x. More importantly, the old simulator had no finite margin cap at all, so no finite ratio reproduces its behavior in general.

I would change it to something like:

A run that intentionally used leverage can select a smaller ratio appropriate to that experiment (for example 0.25 for up to 4x gross exposure). The previous simulator's unbounded behavior is not reproduced by any finite ratio.

Other observations

I like the decisions around:

  • no maintenance-margin semantics in v1;
  • gross rather than net exposure;
  • resulting-position semantics for reversals;
  • broker-initiated cancellation for a Working order that fails margin at trigger time;
  • keeping live pending-order reservation out of this milestone;
  • explicitly preserving quantity-driven strategies such as Buy & Hold;
  • one account / one currency / one configured ratio for v1.

I did not find another substantive architectural issue beyond the four corrections above.

Current state: changes recommended before merge. Once those four ADR wording/contract issues are fixed, I expect this to be merge-ready.

- Describe the fill-time check as validating the actual post-fill
  state, not as uniformly stricter than admission.
- Define required margin as a sum of per-position required-margin
  amounts. v1 is |notional| x ratio; futures' fixed dollars per
  contract fits later without a pseudo-rate.
- Require ReferencePrice and marks only for exposure-increasing
  proposals. De-risking is decided from quantities alone, matching
  ADR-034's rules.
- Correct the compatibility example: no finite ratio reproduces the old
  unbounded simulator, and the 4.63x SPY run fails even at 0.25.
- Align the per-position margin wording in docs/account-risk.md.

Refs #410

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rustyeddy

Copy link
Copy Markdown
Owner Author

Addressed all four review items in ad745bc (replies are on each thread):

  1. The fill-time check now validates the actual post-fill state. The claim that it is never less conservative is removed.
  2. Required margin is now a sum of per-position required-margin amounts: v1 is |notional| × ratio, and futures' fixed dollars per contract fits later.
  3. ReferencePrice and marks are required only for exposure-increasing proposals. Any de-risking proposal (not just an exact close) is decided from quantities alone, matching MaxPositionLeverageRule.
  4. Compatibility: no finite ratio reproduces the old unbounded simulator, and the 4.63× run fails even at 0.25.

I also aligned the per-position margin wording in docs/account-risk.md, updated the PR description table, and posted the corrections to #411 and #413.

@rustyeddy rustyeddy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of PR #420 at head ad745bcf6b76f447b7bd27cbf625989facc087f6.

The four issues from my previous review are fixed cleanly:

  • fill-time margin enforcement is now described as validation of the actual prospective post-fill state, without claiming it is uniformly stricter than admission;
  • future extensibility is modeled as a sum of per-position required-margin amounts/functions, so percentage-based margin and fixed dollars-per-contract both fit without changing the aggregate contract;
  • de-risking is determined from quantities/resulting position first, so a full close/reduction is not blocked by a missing ReferencePrice or unrelated marks;
  • the compatibility language now correctly states that no finite ratio reproduces the old simulator's unbounded behavior.

I also re-checked the surrounding ADR semantics after those edits:

  • gross-vs-net exposure remains clear;
  • reversals use resulting-position semantics;
  • v1 stays one account / one currency / one configured ratio;
  • mixed per-instrument policies are designed for but explicitly not implemented;
  • admission and fill-time enforcement still share one calculation;
  • broker-initiated cancellation of a Working order remains compatible with ADR-018;
  • maintenance-margin behavior remains out of scope;
  • quantity-driven strategies such as Buy & Hold keep their contract.

The aligned change in docs/account-risk.md also looks consistent with ADR-066.

I do not see another substantive blocker in the current diff.

PR #420 is merge-ready from my review.

GitHub currently shows no combined-status entries for this head, so this assessment is based on the PR contents and review state rather than an external CI result.

@rustyeddy
rustyeddy merged commit e6a98fa into main Sep 27, 2026
1 check failed
@rustyeddy
rustyeddy deleted the docs/410-adr-066-initial-margin branch September 27, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: ADR-066 account-level initial-margin admission

2 participants