Vest: payout projections and the completion catch-up restart same-day stages each day

projectPerfectPath and endedCycleCompletion counted every withdrawal ever
made, so a Vest funded account that had claimed on any earlier day was
projected at stage 2's $3,000 forever (labelled stage 3, 4, ...), and a
day's stage-1 cycle that the auto-claim ended before the reporter saw it
was judged against $3,000 and never reported. The projection now takes
the stage inputs: a type whose stages reset each day trades today on
today's payouts and every projected day from the base stage. The catch-up
judges a same-day type's ended cycle on its day's base stage; the stage it
reports stays the payout's number among the account's withdrawals, which
the master clears the entry by. Other types project exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 14:15:07 -05:00
co-authored by Claude Opus 5.5
parent 4add379c68
commit e33e890a83
5 changed files with 99 additions and 15 deletions
+1
View File
@@ -208,6 +208,7 @@ function collectFirmStats() {
fundTransactions: fundTxns,
balance: cash.amount,
realizedPnL: cash.realizedPnL,
stageInputs: stageInputsFor(client, acc.id, cfg),
payout: {
withdrawalPct: cfg.withdrawalPct,
payoutCap: cfg.payoutCap,
+38 -14
View File
@@ -442,6 +442,11 @@ export function projectPerfectPath(opts: {
fundTransactions: { date: string; amount: number }[];
balance: number;
realizedPnL: number;
/** What picks the stage beyond the fund rows (stageInputsFor). A type
* whose stages reset each day trades today on today's payouts and
* every projected day from the base stage. Absent = every withdrawal
* counts. */
stageInputs?: StageInputs;
/** Trading days to look ahead. */
horizonDays?: number;
maxWithdrawals?: number;
@@ -528,17 +533,28 @@ export function projectPerfectPath(opts: {
return room / (room + dayTarget);
};
const effectiveNow = () => resolveEffectiveConfig(
// A type whose stages reset each day (Vest) counts only the payouts made
// since its day began: today's real ones, and none on a projected day —
// the path pays out at most once a day, as the day's last entry, so
// every projected day trades the base stage.
const sameDay = opts.stageInputs?.stagesSameDay === true;
/** 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)
: funds.filter((f) => f.amount < 0).length);
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) } : {},
);
/** The payout parameters of the stage currently being traded: per-stage
/** The payout parameters of the stage being completed: per-stage
* overrides when set, else the type defaults. */
const stagePayout = (): { pct: number; cap: number } => {
const count = funds.filter((f) => f.amount < 0).length;
const stagePayout = (stage: number): { pct: number; cap: number } => {
const count = stage - 1;
if (count === 0 || cfg.withdrawalStages.length === 0) return { pct: basePct, cap: baseCap };
const stage = cfg.withdrawalStages[Math.min(count - 1, cfg.withdrawalStages.length - 1)];
return { pct: stage.withdrawalPct ?? basePct, cap: stage.payoutCap ?? baseCap };
const s = cfg.withdrawalStages[Math.min(count - 1, cfg.withdrawalStages.length - 1)];
return { pct: s.withdrawalPct ?? basePct, cap: s.payoutCap ?? baseCap };
};
const qualifyingDays = (minDay: number, excludeDate: string) => (
minDay > 0
@@ -550,7 +566,7 @@ export function projectPerfectPath(opts: {
// Skipped when "today" is a weekend date (Saturday, or Friday evening
// after the roll): there is no session to finish.
if (todayHasSession) {
const eff = effectiveNow();
const eff = effectiveOn(today);
const plan = computeDayPlan(
accountId, today, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
eff.minTradingDays, balance - cfg.accountSize, opts.realizedPnL, cfg.splitEnabled, eff.drain,
@@ -579,7 +595,7 @@ export function projectPerfectPath(opts: {
// over and over. Only a non-payoutable eval stops after one completion.
const loops = cfg.withdrawalStages.length > 0 || cfg.payoutable === true;
const finishCycle = (onDate: string, dayPnl: number, stage: number, drain: boolean): boolean => {
const { pct, cap } = stagePayout();
const { pct, cap } = stagePayout(stage);
const rawGross = round2(Math.max(0, balance - cfg.accountSize) * pct);
const gross = cap > 0 ? Math.min(rawGross, round2(cap)) : rawGross;
const net = round2(gross * profitSplit);
@@ -601,14 +617,14 @@ export function projectPerfectPath(opts: {
// requestable after that session's close. Never "today" — a projection
// computed over the weekend must not slide a Friday finish to Sunday.
{
const eff = effectiveNow();
const eff = effectiveOn(today);
const probe = nextDay(today);
const plan = computeDayPlan(
accountId, probe, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
);
if (plan && plan.dayTarget <= 0 && eff.minTradingDays <= qualifyingDays(eff.minDayPnL, probe)) {
const stage = 1 + funds.filter((f) => f.amount < 0).length;
const stage = 1 + paidBefore(today);
const traded = sim.filter((d) => d.pnl !== 0);
const doneOn = traded.length > 0
? traded.reduce((a, b) => (a.date > b.date ? a : b)).date
@@ -620,8 +636,8 @@ export function projectPerfectPath(opts: {
let date = today;
for (let step = 0; step < horizon; step++) {
date = nextDay(date);
const eff = effectiveNow();
const stage = 1 + funds.filter((f) => f.amount < 0).length;
const eff = effectiveOn(date);
const stage = 1 + paidBefore(date);
const plan = computeDayPlan(
accountId, date, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
@@ -647,7 +663,7 @@ export function projectPerfectPath(opts: {
// Did this day complete the cycle? Then it IS the payout day.
const nd = nextDay(date);
const eff2 = effectiveNow();
const eff2 = effectiveOn(date);
const plan2 = computeDayPlan(
accountId, nd, eff2.profitTarget, eff2.consistency, sim, eff2.minDayPnL,
eff2.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff2.drain,
@@ -819,6 +835,8 @@ export function endedCycleCompletion(opts: {
targetSameEquity: boolean; withdrawalStages: WithdrawalStage[];
combineCmeHolidays: boolean; splitEnabled: boolean;
tradesEveryDay?: boolean;
/** Stages reset each day (StageInputs). */
stagesSameDay?: boolean;
};
/** Defaults to the current time. */
now?: Date;
@@ -852,10 +870,14 @@ export function endedCycleCompletion(opts: {
const fundAmt = fundTransactions.filter((f) => f.date === lastFundDate).reduce((s, f) => s + f.amount, 0);
const equityBefore = round2(opts.equityProfit - curTotal - fundAmt);
// The stage the ended cycle traded on: only the withdrawals before it.
// The stage the ended cycle traded on: only the withdrawals before it —
// for a type whose stages reset each day, only that day's. One fund row
// a day shows none before it, and the rows here hold that whole day,
// which began on the base stage: it is judged there.
const eff = resolveEffectiveConfig(
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
cfg.withdrawalStages, 0, earlierFunds, cfg.minDayPnL,
cfg.stagesSameDay === true ? { stagesSameDay: true, sameDayWithdrawals: 0 } : {},
);
const rows = combineCmeSessions(combine, endedRaw, lastFundDate, 0).dailyPnL;
// A combining firm's folded day can net to 0 — nothing traded, as the firm counts it.
@@ -866,6 +888,8 @@ export function endedCycleCompletion(opts: {
return {
date: traded.map((d) => d.date).reduce(latest),
// The payout's number among the account's withdrawals, a same-day
// type's too: the master clears the entry once that many have booked.
stage: 1 + earlierFunds.filter((f) => f.amount < 0).length,
drain: eff.drain,
cycle: prevFundDate ?? '',