From 1996937ef2be5d50c3908adfc0e16ef4f16769c9 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sun, 13 Sep 2026 14:10:36 -0500 Subject: [PATCH] Mark TopstepX accounts dead only off landed fills and settled books MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MLL floor no longer recomputes while a position is open or while the balance disagrees with the books beyond fee noise — the last settled floor stands, so balance ≤ floor can only come true when a booked fill crosses a settled floor (Topstep's own liquidation fill included). The floor also survives canTrade flipping off, so a blown account stays ☠ instead of demoting to inactive. Co-Authored-By: Claude Fable 5 --- app/accounts/[id]/page.tsx | 2 +- lib/topstepx-class.ts | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/accounts/[id]/page.tsx b/app/accounts/[id]/page.tsx index 1a89859..e076541 100644 --- a/app/accounts/[id]/page.tsx +++ b/app/accounts/[id]/page.tsx @@ -592,7 +592,7 @@ export default function AccountPage() {

Account Blown

- Balance ${fmt(account.amount)} has breached the Tradovate auto-liquidation floor + Balance ${fmt(account.amount)} has breached the auto-liquidation floor {liqFloor != null ? ` of $${fmt(liqFloor)}` : ''}.

diff --git a/lib/topstepx-class.ts b/lib/topstepx-class.ts index 03070d2..bb16541 100644 --- a/lib/topstepx-class.ts +++ b/lib/topstepx-class.ts @@ -584,12 +584,24 @@ export class TopstepXClient implements PlatformClient { * accounts are skipped: the platform has already retired them. */ private updateMllFloor(account: PlatformAccount): void { - if (!account.active) { this.autoLiqThresholds[account.id] = 0; return; } + // Deliberately computed for canTrade=false accounts too: when Topstep + // liquidates a breach and disables the account, the floor must SURVIVE + // so the dashboards show it as blown (☠), not merely inactive. Retired + // accounts without an assigned type fall out below via dd = 0. const dd = getAccountConfig(account.id).accountType?.trailingDrawdown ?? 0; const history = this.fullDailyPnL[account.id]; const cash = this.accountCashBalances[account.id]; if (!(dd > 0) || !history || !cash) { this.autoLiqThresholds[account.id] = 0; return; } + // Only move the floor on SETTLED books: never mid-trade (the fill + // hasn't landed — the dead check must not fire off half-processed + // state), and never while the balance disagrees with the books by + // more than fee noise (a fill or fund event is still being absorbed; + // reconcileFunds or the next sync settles it). The previous floor + // stands until then, so `balance ≤ floor` can only come true off a + // landed, booked fill. + if (this.positions[account.id]) return; + const today = currentTradeDateCT(); const fees = this.dailyFees[account.id] ?? new Map(); @@ -608,6 +620,9 @@ export class TopstepXClient implements PlatformClient { const booked = dates.reduce((s, d) => s + events.get(d)!, 0); const todayFund = (this.fundTransactions[account.id] ?? []).find((f) => f.date === today)?.amount ?? 0; const residual = Math.round((cash.amount - cash.realizedPnL - todayFund - booked) * 100) / 100; + // Books not settled (an unabsorbed fill or an unbooked fund event) — + // keep the previous floor rather than skewing the curve with it. + if (Math.abs(residual) >= 200) return; const totalFees = dates.reduce((s, d) => s + (fees.get(d) ?? 0), 0); let bal = 0;