Vest: report on Vest's day, and close a leg whose open came back unknown

The reporter and the dashboard take each firm's own trade date
(tradeDateFor), as the engine does: between 5 PM CT and Vest's 8 PM ET
reset the Central date counted the open Vest day as a prior day and as
today's P&L both, and could report a pass the master keeps for good. The
completion check's settle window now follows the client's own roll.

An open whose outcome came back unknown is marked the bot's once a read
shows the position it made, so the market-close guard closes it before a
long closure; an unmarked leg is no longer logged as a hand trade.

A symbol the close gate holds back stays on the candidate board for the
dashboard and the report, and fireLeg holds each account's leg to the
close at its own size, not just the batch lead's. check-vest-client.mjs
drives the guard and the board with every gateway call answered locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 14:41:20 -05:00
co-authored by Claude Opus 5.5
parent ec3e6ea388
commit ef15200e5e
11 changed files with 345 additions and 42 deletions
+27 -11
View File
@@ -684,8 +684,9 @@ export type CycleCompletion = 'none' | 'today' | 'before';
/** How long a new trade date waits before it can read 'today': the platforms
* reset realizedPnL on their own clock (a Tradovate cashBalance event, the
* TopstepX 60s sync), not at 5 PM sharp, and until then the last session's
* number — already booked under its own date — would count as a second day. */
* TopstepX 60s sync, Vest's next sync after 20:00 ET), not at the roll
* sharp, and until then the last session's number — already booked under
* its own date — would count as a second day. */
const ROLL_SETTLE_MS = 5 * 60 * 1000;
/** How long after a fund transaction starts a new cycle the cycle it ended is
@@ -716,13 +717,14 @@ function cycleOwesNothing(
* weekend (it is the last session's stale number, already booked under its
* own date), a Monday with a weekend-dated row — the live session filed under
* a weekend date, which would count it twice — and the first minutes after
* the 5 PM roll. An every-day firm's weekends are real sessions, so only the
* roll window applies to it. */
* the roll: the client's own (tradeDateFor), else 5 PM CT. An every-day
* firm's weekends are real sessions, so only the roll window applies to it. */
function sessionDoubtful(
rawTradeDate: string,
cycleDailyPnL: { date: string; pnl: number }[],
now: Date,
everyDay = false,
client?: { tradeDate?: (now: number) => string },
): boolean {
const day = new Date(`${rawTradeDate}T00:00:00Z`);
const dow = day.getUTCDay();
@@ -733,7 +735,7 @@ function sessionDoubtful(
if (cycleDailyPnL.some((d) => d.date === saturday || d.date === sunday)) return true;
}
// Settle window: a few minutes ago it was still an earlier trade date.
return currentTradeDateCT(new Date(now.getTime() - ROLL_SETTLE_MS)) < rawTradeDate;
return tradeDateFor(client, new Date(now.getTime() - ROLL_SETTLE_MS)) < rawTradeDate;
}
/**
@@ -746,14 +748,18 @@ function sessionDoubtful(
* 'none' instead: a weekend (realizedPnL is the last session's stale number,
* already booked under its own date), a Monday with a weekend-dated row — the
* live session filed under a weekend date, which would count it twice — and
* the first minutes after the 5 PM roll. 'before' skips those guards: it only
* the first minutes after the roll. 'before' skips those guards: it only
* keeps a locked, untraded pass out of the reporter's losses.
*/
export function cycleCompletion(opts: {
accountId: number;
/** currentTradeDateCT() — NOT the combined date, which rolls a weekend
* forward to Monday. */
/** tradeDateFor(client) — NOT the combined date, which rolls a weekend
* forward to Monday. On a platform whose day turns later than 5 PM CT
* (Vest), the Central date would count its open day twice. */
rawTradeDate: string;
/** The account's client: the settle window follows its day's roll
* (tradeDateFor). */
client?: { tradeDate?: (now: number) => string };
/** Current-cycle daily P&L as the client holds it (client.dailyPnL). */
cycleDailyPnL: { date: string; pnl: number }[];
rawRealizedPnL: number;
@@ -776,7 +782,7 @@ export function cycleCompletion(opts: {
const prior = view.dailyPnL.filter((d) => d.date !== view.tradeDate);
if (cycleOwesNothing(accountId, eff, prior, view.tradeDate, equityProfit, view.realizedPnL, splitEnabled)) return 'before';
if (sessionDoubtful(rawTradeDate, opts.cycleDailyPnL, opts.now ?? new Date(), everyDay)) return 'none';
if (sessionDoubtful(rawTradeDate, opts.cycleDailyPnL, opts.now ?? new Date(), everyDay, opts.client)) return 'none';
const withToday = [...prior, { date: view.tradeDate, pnl: view.realizedPnL }];
const probe = nextTradingDate(view.tradeDate, everyDay);
@@ -811,8 +817,10 @@ export function cycleCompletion(opts: {
*/
export function endedCycleCompletion(opts: {
accountId: number;
/** currentTradeDateCT(). */
/** tradeDateFor(client). */
rawTradeDate: string;
/** As in cycleCompletion. */
client?: { tradeDate?: (now: number) => string };
/** Every cycle's daily P&L, raw (client.fullDailyPnL). */
fullDailyPnL: { date: string; pnl: number }[];
/** Current-cycle daily P&L, raw (client.dailyPnL). */
@@ -841,7 +849,7 @@ export function endedCycleCompletion(opts: {
if (lastFundDate === null || start === null) return null;
const ageDays = (Date.parse(`${rawTradeDate}T00:00:00Z`) - Date.parse(`${lastFundDate}T00:00:00Z`)) / (24 * 60 * 60 * 1000);
if (!(ageDays <= ENDED_CYCLE_WINDOW_DAYS)) return null; // NaN included: an unparseable date
if (sessionDoubtful(rawTradeDate, opts.cycleDailyPnL, opts.now ?? new Date(), everyDay)) return null;
if (sessionDoubtful(rawTradeDate, opts.cycleDailyPnL, opts.now ?? new Date(), everyDay, opts.client)) return null;
const latest = (a: string, b: string) => (b > a ? b : a);
const earlierFunds = fundTransactions.filter((f) => f.date < lastFundDate);
@@ -1408,6 +1416,14 @@ export function clearsClose(o: { closeInMs: number; holdHours: number }): boolea
return o.closeInMs >= RESET_CLEAR_HOLDS * o.holdHours * 3_600_000;
}
/** Why a leg of `holdHours` starting at `now` doesn't open: its market
* closes at `closesAt` (epoch ms) before it clears (clearsClose). null when
* it clears, or no close is known (a futures contract). */
export function legCloseNote(closesAt: number | null | undefined, holdHours: number, now: number): string | null {
if (closesAt === null || closesAt === undefined || clearsClose({ closeInMs: closesAt - now, holdHours })) return null;
return `closes in ${((closesAt - now) / 3_600_000).toFixed(1)}h, under ${RESET_CLEAR_HOLDS} holds of ${holdHours.toFixed(1)}h`;
}
/**
* dllRoom where the platform states its daily-loss floor itself (Vest: the
* equity the account may not fall below today, already moved by the day's