Cross-margin accounts¶
Real prop accounts almost universally run cross-margin: equity is shared across all positions on the account, so a profitable BTC short backs a losing ETH long. Backtests that treat each position in isolation overstate liquidation risk for cross-margined portfolios (because cross has more shared cushion) and understate the systemic risk when one position drags the whole account.
The venue-stack's Account type owns the shared state — equity, the position
book across symbols, per-symbol mark prices, and a 30-day rolling
notional counter — and plugs into LiquidationEngine and
FeeSchedule so they evaluate at the account level instead of
per-position.
Build an account¶
The default margin mode is cross. Switch to isolated per-account
with set_margin_mode("isolated") / setMarginMode("isolated").
Closing a position¶
close_position(symbol) realises every leg on that symbol at the
symbol's current mark before dropping it, and credits the result to
account equity:
That is the same expression total_unrealised_pnl() reports, so what
the account showed as unrealised is exactly what the close books —
there is no separate add_equity to remember. A leg on a symbol that
was never marked is valued at entry and realises nothing, which is how
it was already valued in every aggregate.
Before this, closing erased the legs and left equity untouched, so a run that opened and closed positions all day reported the equity it started with.
Cross-margin liquidation¶
Attach the account to a LiquidationEngine. The engine walks
attached accounts on every on_marks tick: for accounts in cross
mode, it evaluates the account-level maintenance-margin check
(equity + total_uPnL vs total_notional * mm_fraction) and, when
the account is underwater, closes the worst-PnL position first.
Use on_marks(...) with the full set of current marks per
tick — it updates every attached account's marks atomically before
walking. The legacy single-symbol on_mark(...) is still
available but is a footgun for multi-symbol accounts: forgetting
to set the other symbols' marks leaves the cross-margin check
evaluating against stale data.
Stale-mark guard¶
When a backtest must refuse to walk on stale data, set timestamps
explicitly via set_mark(sym, price, ts_ns) (or pass ts_ns to
on_marks) and check the account before driving the engine:
When a profitable short backs a losing long, the account stays solvent and no liquidation fires. When both legs bleed, the engine closes the worst leg, re-checks, and continues until the account is solvent or no positions remain. Any residual equity deficit hits the insurance fund (and ADL, if configured).
Shared 30-day fee tier¶
Real venues compute the VIP tier from aggregate 30-day notional
across all symbols, not per-symbol. Binding the account to one or
more FeeSchedules makes them read the aggregate counter:
btc_sched = lrvx.FeeSchedule.binance_um_futures()
eth_sched = lrvx.FeeSchedule.binance_um_futures()
btc_sched.bind_account(acct)
eth_sched.bind_account(acct)
btc_sched.record_fill(ts_ns=0, notional=150_000)
eth_sched.record_fill(ts_ns=0, notional=150_000)
# Aggregate 300k crosses Binance VIP 1 (>= 250k). Both
# schedules now resolve at the higher tier.
assert btc_sched.current_tier_index() >= 1
The account's rolling counter ages out fills older than 30 days automatically (matching the venue's window). The window's total is a fixed-point sum of the fills inside it, so evicting a fill returns exactly what recording it added -- one 5-billion fill no longer swallows the small ones beside it, and there is no clamp at zero hiding the drift when the large one ages out.
Isolated mode¶
Isolated accounts skip the cross-margin netting walk — each
position carries its own posted-margin slice and liquidates
independently. Switch via the margin mode and pass
isolated_equity when opening each position:
In isolated mode the account's equity field is unused; each
position's isolated_equity slice is what backs it under the
maintenance-margin check. A profitable position on one symbol
does NOT shelter an underwater position on another.
Fixed point¶
On the C++ side every number an Account and a LeveragedPosition
carry is fixed point: Quantity quantity, Price entryPrice,
Volume equity, Quantity contractMultiplier, Volume equity, marks
as Price, and the rolling notional as Volume. Equity, notionals
and unrealised PnL are summed through mulDivI64 and checked adds, so
the aggregates the maintenance-margin check and the liquidation
decision read are exact to the raw and identical on every toolchain.
The Python, Node and C surfaces stay float / number / double:
they quantise once at the boundary (Volume::fromDouble and friends)
instead of letting a double travel through the margin arithmetic. C++
callers should prefer the fixed-point overloads; the double-taking
ones remain for configuration and for the bindings.
Notes¶
Accountis non-owning from the engine's perspective. The caller manages lifetime; the language binding's keep-alive semantics prevent premature GC.- Multiple accounts may attach to the same engine — the walk iterates them all.
- Multiple
FeeSchedules sharing the same account see a consistent aggregate counter;current_tier_index()resolves on-demand when bound so a counter increment from one schedule is immediately visible to the others. - Cross-pool collateral (e.g. Binance USDT vs BUSD pools), multi-currency accounts, and venue-specific account-tier fee discounts are out of scope.