Reporter: name the accounts whose final day completed their cycle

cycleCompletion() says whether today's P&L finished the current cycle
(profit target, consistency and minimum days all met) — projectPerfectPath's
completion probe run without and with today. The reporter counts those as
`completed` (a subset of targetHit) and sends their names, so the master
can keep an account a firm erases after it passes. Checked ahead of the
lockout branch: a firm locking a passed account is no DLL loss. The local
dashboard shows them with a purple Completed pill.

Because the master keeps a completion for good, doubtful reads stay
'none': weekends, a Monday with a weekend-dated row, the first minutes
after the 5 PM roll, and history that hasn't freshly loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-23 12:28:41 -05:00
co-authored by Claude Opus 5.5
parent a47a86a59c
commit acb8879575
8 changed files with 205 additions and 19 deletions
+74
View File
@@ -626,6 +626,80 @@ export function projectPerfectPath(opts: {
return out;
}
/** Where the current cycle stands against today's P&L: 'today' = today's
* trading finished it (the final day), 'before' = it already stood complete
* without today, 'none' = profit or days still owed. */
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. */
const ROLL_SETTLE_MS = 5 * 60 * 1000;
/**
* Did today's trading finish the account's current cycle — profit target,
* consistency and minimum days all met? projectPerfectPath's "already
* complete" probe, run once without today's P&L and once with it, so only
* the final day reads 'today'.
*
* The master keeps a 'today' permanently, so every doubtful case reads
* '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
* 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. */
rawTradeDate: string;
/** Current-cycle daily P&L as the client holds it (client.dailyPnL). */
cycleDailyPnL: { date: string; pnl: number }[];
rawRealizedPnL: number;
combineCmeHolidays: boolean;
effective: EffectiveConfig;
/** amount − accountSize — today's P&L included. */
equityProfit: number;
splitEnabled: boolean;
/** Defaults to the current time. */
now?: Date;
}): CycleCompletion {
const { accountId, rawTradeDate, effective: eff } = opts;
const view = combineCmeSessions(opts.combineCmeHolidays, opts.cycleDailyPnL, rawTradeDate, opts.rawRealizedPnL);
const prior = view.dailyPnL.filter((d) => d.date !== view.tradeDate);
// Nothing left to trade AND the days booked: with no min-day amount the
// plan coasts at 0 while days are still owed, so count them too.
const owesNothing = (rows: { date: string; pnl: number }[], date: string, realized: number): boolean => {
const plan = computeDayPlan(
accountId, date, eff.profitTarget, eff.consistency, rows, eff.minDayPnL,
eff.minTradingDays, opts.equityProfit, realized, opts.splitEnabled, eff.drain,
);
const qualifying = rows.filter((d) => d.date !== date
&& (eff.minDayPnL > 0 ? d.pnl >= eff.minDayPnL : d.pnl !== 0)).length;
return plan !== null && plan.dayTarget <= 0 && qualifying >= eff.minTradingDays;
};
if (owesNothing(prior, view.tradeDate, view.realizedPnL)) return 'before';
const day = new Date(`${rawTradeDate}T00:00:00Z`);
const dow = day.getUTCDay();
if (dow === 0 || dow === 6) return 'none';
if (dow === 1) {
const sunday = fmtUTC(new Date(day.getTime() - 24 * 60 * 60 * 1000));
const saturday = fmtUTC(new Date(day.getTime() - 2 * 24 * 60 * 60 * 1000));
if (opts.cycleDailyPnL.some((d) => d.date === saturday || d.date === sunday)) return 'none';
}
// Settle window: a few minutes ago it was still an earlier trade date.
const now = opts.now ?? new Date();
if (currentTradeDateCT(new Date(now.getTime() - ROLL_SETTLE_MS)) < rawTradeDate) return 'none';
const withToday = [...prior, { date: view.tradeDate, pnl: view.realizedPnL }];
if (view.realizedPnL !== 0 && owesNothing(withToday, nextTradingDate(view.tradeDate), 0)) return 'today';
return 'none';
}
/** Dollar-per-point value for common futures products. */
export const POINT_VALUES: { [symbol: string]: number } = {
NQ: 20, MNQ: 2, ES: 50, MES: 5,