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. */