Reconstruct TopstepX MLL floors from the EOD balance curve

TopstepX never reports the Maximum Loss Limit, but it's deterministic:
floor = min(0, eodBalanceHigh − trailingDrawdown), locking at the $0
starting balance. The EOD curve rebuilds from daily P&L + fund events,
with off-book fee drift spread across days proportionally to reported
fees (validated against a platform-reported MLL to ~$0.10). Stored in
autoLiqThresholds — negative, or +$0.01 once locked — so dead detection,
dashboards, and the ruin model's startThreshold all use the real room.
Guards move from '> 0' to '!== 0'; 0 still means no floor. canTrade=false
accounts are skipped. daily_pnl gains a fees column for the weighting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-13 13:57:06 -05:00
co-authored by Claude Fable 5
parent 4edb771701
commit fc65b4340d
7 changed files with 121 additions and 18 deletions
+8 -5
View File
@@ -411,9 +411,11 @@ export default function AccountPage() {
const hasLossLimit = cfg != null && cfg.minDayPnL !== -999;
const daysPassed = cfg != null && account.daysTraded >= cfg.minTradingDays;
// Dead when balance hits Tradovate's auto-liquidation floor
const isDead = account.autoLiqThreshold > 0 && account.amount <= account.autoLiqThreshold;
const liqFloor = account.autoLiqThreshold > 0 ? account.autoLiqThreshold : null;
// Dead when balance hits the liquidation floor — Tradovate reports its
// (positive) floor; TopstepX floors are reconstructed MLLs, negative or
// +$0.01 once locked at breakeven. 0 = no floor known.
const isDead = account.autoLiqThreshold !== 0 && account.amount <= account.autoLiqThreshold;
const liqFloor = account.autoLiqThreshold !== 0 ? account.autoLiqThreshold : null;
// Ground-truth profit = balance minus funded account size.
// Falls back to FIFO total when accountSize is unavailable.
@@ -508,8 +510,9 @@ export default function AccountPage() {
drawdown: (cfg.trailingDrawdown ?? 0) > 0
? cfg.trailingDrawdown!
: Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000,
// The platform's real liquidation floor, when it reports one.
startThreshold: account.autoLiqThreshold > 0
// The platform's real liquidation floor, when one is known —
// reported by Tradovate, reconstructed for TopstepX.
startThreshold: account.autoLiqThreshold !== 0
? Math.round((account.autoLiqThreshold - cfg.accountSize) * 100) / 100
: undefined,
},
+2 -1
View File
@@ -45,7 +45,8 @@ export async function GET() {
const dailyPnL = combined.dailyPnL;
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold;
// 0 = no floor; negative floors are reconstructed TopstepX MLLs.
const isDead = autoLiqThreshold !== 0 && cash.amount <= autoLiqThreshold;
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
// Stage = 1 + number of withdrawals (negative fund transactions)
+2 -1
View File
@@ -24,7 +24,8 @@ function fmt(value: number) {
/** Returns true when an account's balance has breached Tradovate's auto-liquidation floor. */
function isAccountDead(account: AccountState): boolean {
return account.autoLiqThreshold > 0 && account.amount <= account.autoLiqThreshold;
// 0 = no floor; negative floors are reconstructed TopstepX MLLs.
return account.autoLiqThreshold !== 0 && account.amount <= account.autoLiqThreshold;
}
function SettingsIcon() {