Concept · Survival & ruin
A (strategy, symbol) pair stops trading when its equity reaches or falls below a configured floor. This is the simulator's account-survival model: the moment that closes the gap between "the strategy stopped making money" and "the account that would have funded the strategy no longer exists."
A real trading account can run out of money. Once it does, no further trades happen — not because the strategy stopped firing signals, but because there is nothing left to size a position from. Before phase 121.1, the simulator did not enforce this: equity could be driven arbitrarily negative by accumulated losses, and the strategy would keep entering trades as long as MIN_POSITION_USD = $1 of margin could be carved out.
Bankruptcy is the missing constraint. After phase 121.1, each (strategy, symbol) pair carries an equityBankruptcyFloor. When a closing trade's debit drives the pair's equity to or below the floor, the pair is marked bankrupt — that trade is the last one for that pair, and every subsequent entry signal is recorded with fillStatus: 'account_bankrupt' instead of opening a position.
The default floor is $0 (true zero-equity bust). The wire contract also accepts a percent-of-initial-equity string (e.g. '20%'), which models psychological capitulation — a real trader typically stops adding margin long before equity literally hits zero.
UI note (2026-05-18): The Global Settings input in the frontend currently accepts dollars only — the percent-string form remains a valid wire shape but can only be set programmatically (e.g. via a direct
PATCH /api/simulation-settingscall) until a dedicated dollar/percent toggle is added to the UI.
Bankruptcy is a consequence of a normal exit signal, not a separate force-close path (D-09 / D-09a).
computePnl() resolves the trade's true PnL — the simulator does not clip the loss to land cleanly on the floor. The recorded finalEquity may slightly overshoot (sit just below the configured floor or even below zero).EquityLedger.apply(strategyId, symbol, netPnl) returns { newEquity, bankrupted: true } because the apply call crossed the floor.exit_reason='bankruptcy'.(strategyId, symbol) are written with fillStatus: 'account_bankrupt' and no position opens.Cross-symbol open positions are not force-closed when one symbol busts (D-09b). Bankruptcy is keyed by (strategyId, symbol) exactly like the EquityLedger, mirroring the Phase 107 partitioning invariant. A strategy that trades BTC and ETH can go bust on BTC while ETH continues independently. Portfolio-level bust cascade is explicitly deferred.
bankruptcyTradeIndex on the /api/analytics payload — number for busted pairs, null otherwise.finalEquity floors near (not exactly on) the bankruptcy threshold per the D-18 contract — slight overshoot is the trade's true realized PnL, which is the only honest number to record.exit_reason='bankruptcy' in the existing enum; no new column was added (D-10 revised). The full enum is 'condition' | 'take_profit' | 'stop_loss' | 'flip' | 'liquidation' | 'bankruptcy'.$10 min-order-sizeminOrderSizeUsd defaults to $10 (D-05) — a behavioral floor that reflects fee+slippage break-even on majors, not venue mechanics. Orderly's per-symbol venue minimums are typically far lower (sub-dollar for BTC), but trading $1 of notional through a real account is not economically meaningful: bid/ask spread alone exceeds expected per-trade edge.
The simulator therefore enforces the fidelity-aligned floor rather than the venue-mechanic floor. When a strategy's computed position size resolves below minOrderSizeUsd, the entry signal is recorded with fillStatus: 'margin_insufficient' and no position opens. This is a different gate from bankruptcy (fillStatus: 'account_bankrupt') — the strategy might recover (equity grows back above the threshold) — but it shrinks the viable strategy count on $200-starting-equity backtests, which is the intended consequence. Per-symbol venue-driven minimums are deferred and would be a fidelity regression, not a future improvement.
In the EMA-cross dossier the headline number is a zero: 0 bankruptcies across all 210 strategies, even at 50× and 100× leverage (a position-size multiplier that scales both profit and loss). That sounds reassuring. It is not — it is an artifact of position sizing.
Every row sizes each trade at 5% of current equity. So no single leveraged position can zero the account: a total loss on one trade removes 5% of what is left, not 100% of it. As losses compound, equity asymptotes toward zero — the deepest rows post −98% to −99.9% drawdown (peak-to-trough equity fall) — but it barely reaches zero, so the bankruptcy floor (default $0) is never crossed.
The trap, then, is reading "survived" as "did fine". The simulator's survival binary is generous: "didn't lose ≥50%". The real ruin signal in this dataset is the heavy_loss outcome — a deep, near-total wipe — and 110 of 210 rows land there. A strategy can be heavy_loss, down 98%, riding a 145-trade losing streak (see risk of ruin), and still never trip the bankruptcy gate. The gate didn't fire because the sizing rule kept a sliver of equity alive, not because the strategy was safe.
So in this fleet bankruptcy is the dog that didn't bark. The mechanism is real and modeled; the dataset just never exercises it, which is itself the teaching point — survival is not the same as success.
.planning/phases/121.1-account-bankruptcy-modeling-and-realistic-margin-floor/121.1-CONTEXT.md (D-01 through D-23)apps/backend/src/evaluation/equity-ledger.ts — apply() returns { newEquity, bankrupted } and isBankrupt(strategyId, symbol)apps/backend/src/simulation/config-manager.ts — equityBankruptcyFloor and minOrderSizeUsd FIELDS descriptors (both 'rebacktest'-classified)packages/shared/src/index.ts — FillStatus enum extended with 'account_bankrupt'Related concepts
See it in a real result →Put it to the test
Spawn your variant, run it on the same engine, and read the edge-significance verdict — before you risk real money.