From 4add379c686f8e558feafa9c9cd8ffbc8d02fa98 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 27 Oct 2026 14:15:02 -0500 Subject: [PATCH] Dashboard: a stage's own daily loss limit shows its stop, its personal DLL and its min days Since a withdrawal stage can carry its own daily loss limit, the engine places that stop, but the dashboard and report still asked the type's base limit whether the bot stops at all: with a base of 0 and no obfuscation options the row showed no "stop $X", and a stopped-out red day read "Flat" (and counted as a loss, not a personal DLL, in the report). botStopsActive now takes the stage-resolved config. The state also carries the stage's daily loss limit and min trading days, which the account page and the Days column show instead of the type's. Co-Authored-By: Claude Opus 5.5 --- app/accounts/[id]/page.tsx | 9 ++++++--- app/api/state/route.ts | 6 ++++-- app/page.tsx | 2 +- lib/reporter.ts | 4 ++-- lib/trading-logic.ts | 13 +++++++++---- scripts/check-trading-logic.mjs | 16 ++++++++++++++++ types.ts | 4 ++++ 7 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/accounts/[id]/page.tsx b/app/accounts/[id]/page.tsx index 4f22694..914613e 100644 --- a/app/accounts/[id]/page.tsx +++ b/app/accounts/[id]/page.tsx @@ -513,9 +513,12 @@ export default function AccountPage() { // The firm's hard daily loss limit, and the least a day must make to count // as a trading day — two different numbers; neither is set on every type. - const dailyLossLimit = (cfg?.dailyLossLimit ?? 0) > 0 ? cfg!.dailyLossLimit! : null; + // The limit and the min days are the current stage's, which may set its own. + const stageLossLimit = account.effectiveDailyLossLimit ?? cfg?.dailyLossLimit ?? 0; + const dailyLossLimit = stageLossLimit > 0 ? stageLossLimit : null; const minDayPnL = (cfg?.minDayPnL ?? 0) > 0 ? cfg!.minDayPnL : null; - const daysPassed = cfg != null && account.daysTraded >= cfg.minTradingDays; + const minTradingDays = account.effectiveMinTradingDays ?? cfg?.minTradingDays ?? 0; + const daysPassed = cfg != null && account.daysTraded >= minTradingDays; // 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. @@ -739,7 +742,7 @@ export default function AccountPage() { {dailyTarget != null && (
diff --git a/app/api/state/route.ts b/app/api/state/route.ts index 7654ae1..19cd077 100644 --- a/app/api/state/route.ts +++ b/app/api/state/route.ts @@ -171,7 +171,7 @@ export async function GET() { }; } } - if (plan && !dayBooked && botStopsActive(cfg)) { + if (plan && !dayBooked && botStopsActive(cfg, effective)) { const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday); // The reserve grows with the position (dllReserve): at // the size the next leg would trade, where it's known. @@ -273,7 +273,7 @@ export async function GET() { // An account the bot puts stops on whose day booked red via // its own stop — the personal DLL. Firm DLL lockouts show // via !active instead. - personalDll: !!cfg && botStopsActive(cfg) + personalDll: !!cfg && !!effective && botStopsActive(cfg, effective) && acc.active && !client.positions[acc.id] && combined.realizedPnL <= -25, positionDirection: client.positions[acc.id] @@ -293,6 +293,8 @@ export async function GET() { lastStand: lastStandNext, remainingToday, effectiveProfitTarget, + effectiveDailyLossLimit: effective?.dailyLossLimit ?? null, + effectiveMinTradingDays: effective?.minTradingDays ?? null, dailyPnL, fullDailyPnL: combineCmeSessions( cfg?.combineCmeHolidays === true, diff --git a/app/page.tsx b/app/page.tsx index 94a184b..ff1663e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -453,7 +453,7 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned, onStateChang - {account.daysTraded} / {cfg?.minTradingDays ?? '—'} + {account.daysTraded} / {account.effectiveMinTradingDays ?? cfg?.minTradingDays ?? '—'}
diff --git a/lib/reporter.ts b/lib/reporter.ts index ef52e02..c5b3ad7 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -373,11 +373,11 @@ function collectFirmStats() { // A grind-day fill (dayTarget 0) can flag targetHit on a // negative day — a loss is still a loss, so red wins. // A red day on an account the bot puts stops on (the - // obfuscation stops, or a hard daily loss limit) is its own + // obfuscation stops, or its stage's hard daily loss limit) is its own // personal DLL, booked on purpose — not a genuine loss. (A // firm DLL lockout took the !acc.active branch above and // stays a loss.) - if (botStopsActive(cfg) && combined.realizedPnL <= -25) { + if (botStopsActive(cfg, effective) && combined.realizedPnL <= -25) { personalDllCount++; slice.personalDll = (slice.personalDll ?? 0) + 1; } else if (combined.realizedPnL < 0) { diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index 9c40fac..da9e6d0 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -1536,10 +1536,15 @@ export function stopTicks(o: { } /** Whether the bot places its own stops for this account type — the - * obfuscation options or a hard daily loss limit. A red day on such an - * account is the bot's own stop (the personal DLL), not a firm lockout. */ -export function botStopsActive(cfg: { firstDayStop: boolean; minDayStop: boolean; dailyLossLimit?: number }): boolean { - return cfg.firstDayStop || cfg.minDayStop || (cfg.dailyLossLimit ?? 0) > 0; + * obfuscation options or a hard daily loss limit: the current stage's + * (resolveEffectiveConfig) when given, which a stage may set on a type with + * none. A red day on such an account is the bot's own stop (the personal + * DLL), not a firm lockout. */ +export function botStopsActive( + cfg: { firstDayStop: boolean; minDayStop: boolean; dailyLossLimit?: number }, + effective?: { dailyLossLimit: number }, +): boolean { + return cfg.firstDayStop || cfg.minDayStop || (effective ? effective.dailyLossLimit : cfg.dailyLossLimit ?? 0) > 0; } /** diff --git a/scripts/check-trading-logic.mjs b/scripts/check-trading-logic.mjs index 5d828bd..5f25f50 100644 --- a/scripts/check-trading-logic.mjs +++ b/scripts/check-trading-logic.mjs @@ -305,6 +305,22 @@ test('dllViable and botStopsActive', () => { assert.equal(botStopsActive({ firstDayStop: true, minDayStop: false, dailyLossLimit: 0 }), true); }); +test('botStopsActive: the current stage\'s daily loss limit, which a stage may set on a type with none', () => { + const cfg = { firstDayStop: false, minDayStop: false, dailyLossLimit: 0 }; + const stages = [{ profit: 3000, consistency: 0, minTradingDays: 0, dailyLossLimit: 3000 }]; + const paidOnce = [{ date: '2026-10-25', amount: -1500 }]; + // Stage 2 places its $3,000 stop: a red day there is the personal DLL. + const stage2 = resolveEffectiveConfig(1500, 0, 0, false, stages, 0, paidOnce, 0, { dailyLossLimit: 0 }); + assert.equal(botStopsActive(cfg, stage2), true); + // Stage 1 of the same type places none. + assert.equal(botStopsActive(cfg, resolveEffectiveConfig(1500, 0, 0, false, stages, 0, [], 0, { dailyLossLimit: 0 })), false); + // A stage that sets 0 lifts the type's limit. + const lifted = resolveEffectiveConfig(1500, 0, 0, false, [{ ...stages[0], dailyLossLimit: 0 }], 0, paidOnce, 0, { dailyLossLimit: 1229 }); + assert.equal(botStopsActive({ ...cfg, dailyLossLimit: 1229 }, lifted), false); + // The obfuscation stops count whatever the stage. + assert.equal(botStopsActive({ ...cfg, minDayStop: true }, lifted), true); +}); + // ── sizing ─────────────────────────────────────────────────────────────────── const btcSigma = 0.03 / Math.sqrt(24); // 3%/day diff --git a/types.ts b/types.ts index bda98ca..86b6a8b 100644 --- a/types.ts +++ b/types.ts @@ -136,6 +136,10 @@ export interface AccountState { lastStand: boolean; /** Actual profit target to hit — max(stage profit target, consistency realTarget = maxDay/consistency). */ effectiveProfitTarget: number | null; + /** The current stage's hard daily loss limit (0 = none) and minimum + * trading days — a withdrawal stage may set its own. null = unassigned. */ + effectiveDailyLossLimit: number | null; + effectiveMinTradingDays: number | null; /** Current cycle daily P&L — used for target calculations. */ dailyPnL: { date: string; pnl: number }[]; /** Full P&L history across all cycles — used for calendar and equity curve display. */