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