Vest: a claim names the completed cycle it ends, a day's second and one after the reset too

Two of the cycles a claim ends never got a completion entry. A Vest
day's second cycle is one: the auto-claim at the next tick beats the 30 s
report, and the payout books into that day's fund row, so the latest fund
date never moves for the catch-up. A cycle claimed right after the 20:00
ET reset is the other: the reset puts it back on stage 1, and all of its
P&L sits in the previous claim's day row, so endedCycleCompletion reads
none of it.

Before a claim goes out, claimedCycle (trading-logic) checks whether the
cycle stands complete: cycleCompletion 'today' or 'before' on the
client's numbers. It keys the cycle the way the final day would
(completionPlace on the payout list), so the master dedupes the two.
afterClaim keeps the result in account_meta. The reporter names each kept
cycle for the catch-up's week. It lists them last in the slice, newest
first, under the master's per-slice cap, and adds no counter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 16:08:17 -05:00
co-authored by Claude Opus 5.5
parent 4a8e027056
commit e02838409b
7 changed files with 348 additions and 15 deletions
+72 -2
View File
@@ -742,6 +742,13 @@ const ROLL_SETTLE_MS = 5 * 60 * 1000;
* still checked for a missed completion. */
const ENDED_CYCLE_WINDOW_DAYS = 7;
/** Is `date` (a fund date, or a claimed cycle's — claimedCycle) within the
* week its ended cycle is still named? False for an unparseable date. */
export function withinEndedCycleWindow(date: string, rawTradeDate: string): boolean {
const ageDays = (Date.parse(`${rawTradeDate}T00:00:00Z`) - Date.parse(`${date}T00:00:00Z`)) / (24 * 60 * 60 * 1000);
return ageDays <= ENDED_CYCLE_WINDOW_DAYS; // NaN: false
}
/** 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. */
function cycleOwesNothing(
@@ -924,8 +931,7 @@ export function endedCycleCompletion(opts: {
const everyDay = cfg.tradesEveryDay === true;
const combine = effectiveCombine(cfg);
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 (!withinEndedCycleWindow(lastFundDate, rawTradeDate)) 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);
@@ -975,6 +981,70 @@ export function endedCycleCompletion(opts: {
};
}
/** A completed cycle a payout claim ended (claimedCycle). */
export interface ClaimedCycle { date: string; stage: number; drain: boolean; cycle: string; cycleSeq: number }
/**
* The cycle a payout claim about to go out ends (PlatformClient.claimProfit),
* when it stands complete — cycleCompletion's 'today' or 'before' on the
* client's numbers just before the claim — named and numbered as the final
* day names it (completionPlace), so the master dedupes the two. date = the
* claim's trade date. Null for a claim short of the rule (a Claim now below
* the target) or on numbers not read yet.
*
* Taken before the claim books its payout, and kept once it goes through
* (auto-trade afterClaim). The claim starts a new cycle at once, and two of
* the cycles it can end no report would name otherwise. A Vest day's second
* cycle: the auto-claim at the next tick beats the 30 s report, and its
* payout books into that day's fund row, so the latest fund date doesn't
* move for the catch-up. And one claimed right after the 20:00 ET reset,
* which puts it back on the first stage, whose target the day's leftover
* profit meets: all of its P&L sits in the day row of the cycle before it,
* so endedCycleCompletion reads none of it.
*/
export function claimedCycle(
client: {
accountCashBalances: { [accountId: number]: { amount: number; realizedPnL: number } | undefined };
dailyPnL: { [accountId: number]: { date: string; pnl: number }[] | undefined };
fundTransactions?: { [accountId: number]: { date: string; amount: number }[] | undefined };
lastFundDates?: { [accountId: number]: string | null | undefined };
priorProfit?: { [accountId: number]: number | undefined };
tradeDate?: (now: number) => string;
sameDayClaims?: (accountId: number) => number;
payoutDays?: (accountId: number) => string[] | undefined;
},
accountId: number,
cfg: {
profitTarget: number; consistency: number; minTradingDays: number; minDayPnL: number; accountSize: number;
targetSameEquity: boolean; withdrawalStages: WithdrawalStage[]; splitEnabled: boolean;
combineCmeHolidays?: boolean; tradesEveryDay?: boolean; dailyLossLimit?: number; stagesSameDay?: boolean;
},
now: Date = new Date(),
): ClaimedCycle | null {
const cash = client.accountCashBalances[accountId];
const cycleDailyPnL = client.dailyPnL[accountId];
const fundTransactions = client.fundTransactions?.[accountId];
if (!cash || !cycleDailyPnL || !fundTransactions) return null;
const effective = resolveEffectiveConfig(
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages,
client.priorProfit?.[accountId] ?? 0, fundTransactions, cfg.minDayPnL, stageInputsFor(client, accountId, cfg),
);
const rawTradeDate = tradeDateFor(client, now);
const completion = cycleCompletion({
accountId, rawTradeDate, client, cycleDailyPnL, rawRealizedPnL: cash.realizedPnL,
combineCmeHolidays: cfg.combineCmeHolidays === true, tradesEveryDay: cfg.tradesEveryDay === true,
effective, equityProfit: cash.amount - cfg.accountSize, splitEnabled: cfg.splitEnabled, now,
});
if (completion === 'none') return null;
const cycle = client.lastFundDates?.[accountId] ?? '';
const payoutDays = client.payoutDays?.(accountId) ?? fundTransactions.filter((f) => f.amount < 0).map((f) => f.date);
const place = completionPlace({ payoutDays, fundTransactions, cycle });
return {
date: combineCmeSessions(effectiveCombine(cfg), [], rawTradeDate, 0).tradeDate,
stage: place.stage, drain: effective.drain, cycle, cycleSeq: place.cycleSeq,
};
}
/**
* Compute the next trading day's profit target for an account.
*