Vest: the payout projection runs on Vest's day, and stages count each payout

projectPerfectPath takes the raw trade date (default: the Central one):
the reporter passes its tradeDateFor day and the account page the one
/api/state now gives each account. From 5 PM CT to 20:00 ET it took
tomorrow as today while every input was Vest's, so the day's claim moved
a day late, the next day's stage-1 payout dropped, and an open Vest-day
row counted twice.

Without same-day stages, the stage comes from the client's payout list
(payoutDays, one per payout) when it has read one, not the negative fund
rows: Vest books a day's two claims as one row, so the engine, claimDue,
the stage label and the projection stayed a stage behind the report.
StageInputs.payouts carries the count; a client with no list counts rows
as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 15:37:44 -05:00
co-authored by Claude Opus 5.5
parent 032ba1ca9c
commit 4a8e027056
7 changed files with 130 additions and 20 deletions
+28 -8
View File
@@ -37,6 +37,19 @@ export interface StageInputs {
dailyLossLimit?: number;
stagesSameDay?: boolean;
sameDayWithdrawals?: number;
/** Every payout the account has made, one per payout, where a fund row
* can hold several (PlatformClient.payoutDays — Vest books a day's
* claims as one row). Absent = one per negative fund row. */
payouts?: number;
}
/** The payouts that pick an account's stage: today's for a type whose
* stages reset each day, else every one — the client's own count when it
* gives one (StageInputs.payouts), else the negative fund rows. */
export function stagePayouts(fundTransactions: { date: string; amount: number }[], inputs: StageInputs = {}): number {
if (inputs.stagesSameDay) return Math.max(0, Math.floor(inputs.sameDayWithdrawals ?? 0));
if (typeof inputs.payouts === 'number' && Number.isFinite(inputs.payouts)) return Math.max(0, Math.floor(inputs.payouts));
return fundTransactions.filter((f) => f.amount < 0).length;
}
/**
@@ -73,9 +86,7 @@ export function resolveEffectiveConfig(
// Same-day stages (Vest): only today's payouts count, so every new
// day starts back at stage 1 — the room a payout frees lasts only
// until the firm's next daily reset.
const withdrawalCount = inputs.stagesSameDay
? Math.max(0, Math.floor(inputs.sameDayWithdrawals ?? 0))
: fundTransactions.filter((f) => f.amount < 0).length;
const withdrawalCount = stagePayouts(fundTransactions, inputs);
if (withdrawalCount === 0) {
return { profitTarget, consistency, minTradingDays, minDayPnL, drain: false, dailyLossLimit }; // Stage 1 = base values
}
@@ -480,6 +491,9 @@ export function projectPerfectPath(opts: {
};
/** Defaults to the current time. */
now?: Date;
/** The raw trade date the inputs follow (tradeDateFor — Vest's turns at
* 20:00 ET); defaults to the Central one at `now`. */
rawTradeDate?: string;
}): ProjectedDay[] {
const { accountId, cfg } = opts;
const horizon = opts.horizonDays ?? 40;
@@ -490,7 +504,7 @@ export function projectPerfectPath(opts: {
const dd = Math.max(0, opts.payout?.drawdown ?? 0);
const everyDay = cfg.tradesEveryDay === true;
const rawToday = currentTradeDateCT(opts.now);
const rawToday = opts.rawTradeDate ?? currentTradeDateCT(opts.now);
// The firm's own day view: a combining firm folds holiday-session P&L
// into its clearing date — a Labor-Day trade and the Tuesday after it are
// ONE trading day, exactly as the firm counts min days. Identity view
@@ -551,11 +565,13 @@ export function projectPerfectPath(opts: {
const sameDay = opts.stageInputs?.stagesSameDay === true;
/** The path's own payouts, by the date they are claimed on. */
const paidOn = new Map<string, number>();
/** The account's real payouts, one per payout (stagePayouts). */
const paidReal = stagePayouts(opts.fundTransactions, { payouts: opts.stageInputs?.payouts });
/** Payouts before the stage traded on `day`: that day's own for a
* same-day type, else every withdrawal so far. */
const paidBefore = (day: string): number => (sameDay
? (day === today ? Math.max(0, Math.floor(opts.stageInputs?.sameDayWithdrawals ?? 0)) : 0) + (paidOn.get(day) ?? 0)
: funds.filter((f) => f.amount < 0).length);
: paidReal + withdrawals);
/** A same-day type trades its next stage on the date a payout lands
* (runClaims claims, then trades again on the room the claim freed) —
* once through its stages a day: the last one repeats, and the path
@@ -566,7 +582,7 @@ export function projectPerfectPath(opts: {
const effectiveOn = (day: string) => resolveEffectiveConfig(
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
sameDay ? { stagesSameDay: true, sameDayWithdrawals: paidBefore(day) } : {},
sameDay ? { stagesSameDay: true, sameDayWithdrawals: paidBefore(day) } : { payouts: paidBefore(day) },
);
/** The payout parameters of the stage being completed: per-stage
* overrides when set, else the type defaults. */
@@ -1429,17 +1445,21 @@ export function dllReserve(dailyLossLimit: number, notional = 0, slippageFractio
export const DLL_MIN_STOP = 50;
/** The stage inputs for one account: the type's daily loss limit, and for a
* type whose stages reset each day, the client's count of today's payouts. */
* type whose stages reset each day, the client's count of today's payouts —
* for any other, of every payout, where the client lists them one by one
* (payoutDays) and has read them. */
export function stageInputsFor(
client: { sameDayClaims?: (accountId: number) => number },
client: { sameDayClaims?: (accountId: number) => number; payoutDays?: (accountId: number) => string[] | undefined },
accountId: number,
cfg: { dailyLossLimit?: number; stagesSameDay?: boolean },
): StageInputs {
const sameDay = cfg.stagesSameDay === true;
const payouts = !sameDay && typeof client.payoutDays === 'function' ? client.payoutDays(accountId)?.length : undefined;
return {
dailyLossLimit: cfg.dailyLossLimit ?? 0,
stagesSameDay: sameDay,
sameDayWithdrawals: sameDay && typeof client.sameDayClaims === 'function' ? client.sameDayClaims(accountId) : 0,
...(payouts !== undefined ? { payouts } : {}),
};
}