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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
952e4412c4
commit
4add379c68
@@ -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() {
|
||||
<ObjRow
|
||||
passed={daysPassed}
|
||||
label="Trading Days"
|
||||
value={cfg ? `${account.daysTraded} of ${cfg.minTradingDays}` : '—'}
|
||||
value={cfg ? `${account.daysTraded} of ${minTradingDays}` : '—'}
|
||||
/>
|
||||
{dailyTarget != null && (
|
||||
<div className="flex items-center justify-between py-3 border-b border-slate-100 last:border-0">
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -453,7 +453,7 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned, onStateChang
|
||||
<AccountTypeCell account={account} firm={firm} onAssigned={onAssigned} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-600 tabular-nums">
|
||||
{account.daysTraded} / {cfg?.minTradingDays ?? '—'}
|
||||
{account.daysTraded} / {account.effectiveMinTradingDays ?? cfg?.minTradingDays ?? '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-600 tabular-nums">
|
||||
<div className="flex flex-col">
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user