docs: ADR-066 account-level initial-margin admission - #420
Conversation
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>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The ADR contains unresolved correctness and consistency issues that should be addressed before approval.
Review effort: Lite
Findings: 4
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.
| 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. |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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.
| - A missing =ReferencePrice= or mark, a non-positive ratio, or a | ||
| currency mismatch is a classifiable error (=ErrInsufficientRuleInput= | ||
| or =ErrInvalidRule=), never an approval. |
There was a problem hiding this comment.
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.
| every changed fixture and why. A run can reproduce the old behavior | ||
| explicitly with a smaller ratio, for example =0.25=. |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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>
|
Addressed all four review items in ad745bc (replies are on each thread):
I also aligned the per-position margin wording in |
rustyeddy
left a comment
There was a problem hiding this comment.
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.

What changed
Adds ADR-066 (
docs/arch/adr-066-account-initial-margin-admission.org) and indexes it inadr-decisions.org. No code changes.It settles every decision #410 lists:
initial_margin_ratio= equity required per unit of gross notional (1.0 / 0.5 / 0.25 → 1× / 2× / 4×).maintenance_margin_ratioreserved for later.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.resultingPosition(100L → 100S counts as 100).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). NeverAvgPrice. A missing mark is a classifiable error. Staleness is defined against the scheduler's phases.Submit→Rejected(ReasonInsufficientMargin). A resting order refused at trigger → the existing broker-initiatedbuildInternalCancellationpath (as in #352), plus a new additiveOrder.CancelReason *Rejection.ModelInfo.allow_shortdeferred.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:
internal/order/transition.goReasonInsufficientMarginOrder.RejectioninvariantbuildInternalCancellationsubmitBracketOutcomeModelInforisk.NewEngine()in the backtest serviceDocumentation 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