From e33e890a835ad25980ea69524ecb9ecbac037bbc Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 27 Oct 2026 14:15:07 -0500 Subject: [PATCH] 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 --- app/accounts/[id]/page.tsx | 2 ++ lib/reporter.ts | 1 + lib/trading-logic.ts | 52 +++++++++++++++++++++--------- scripts/check-trading-logic.mjs | 56 ++++++++++++++++++++++++++++++++- types.ts | 3 ++ 5 files changed, 99 insertions(+), 15 deletions(-) diff --git a/app/accounts/[id]/page.tsx b/app/accounts/[id]/page.tsx index 914613e..bb91408 100644 --- a/app/accounts/[id]/page.tsx +++ b/app/accounts/[id]/page.tsx @@ -608,6 +608,8 @@ export default function AccountPage() { fundTransactions, balance: account.amount, realizedPnL: account.realizedPnL, + // A type whose stages reset each day: its state stage counts today's payouts. + stageInputs: { stagesSameDay: cfg.stagesSameDay === true, sameDayWithdrawals: account.stage - 1 }, maxWithdrawals: 2, payout: { withdrawalPct: cfg.withdrawalPct, diff --git a/lib/reporter.ts b/lib/reporter.ts index c5b3ad7..089ab22 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -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, diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index da9e6d0..af10716 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -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 ?? '', diff --git a/scripts/check-trading-logic.mjs b/scripts/check-trading-logic.mjs index 5f25f50..07f0b61 100644 --- a/scripts/check-trading-logic.mjs +++ b/scripts/check-trading-logic.mjs @@ -11,7 +11,7 @@ import assert from 'node:assert/strict'; import { percentFee, takeProfitPlan, onTick, obfuscationStopDollars, dllRoom, dllStopDollars, dllViable, stopTicks, botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS, - projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM, + projectPerfectPath, cycleCompletion, endedCycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM, dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget, currentTradeDateCT, foldReason, withinFirmCaps, personSizeFactor, personHoldHours, PERSON_SIZE_SPREAD, machineSide, resolveEffectiveConfig, stageInputsFor, floorRoom, resetClearStop, shortestNextLeg, computeDayPlan, @@ -1147,3 +1147,57 @@ test('the day after a claim: a fresh cycle that aims at the next stage\'s target assert.ok(plan.dayTarget >= 3000 && plan.dayTarget <= 3025, `dayTarget ${plan.dayTarget}`); assert.equal(plan.remainingToday, plan.dayTarget); }); + +// The Vest 50k Funded type: $1,500 and claim it all, then $3,000 on the room +// the claim freed — and every new day back at $1,500. +const vestFunded = { + profitTarget: 1500, consistency: 0, minDayPnL: 0, minTradingDays: 0, accountSize: 50000, + targetSameEquity: false, splitEnabled: true, payoutable: true, tradesEveryDay: true, combineCmeHolidays: false, + withdrawalStages: [{ profit: 3000, consistency: 0, minTradingDays: 0, dailyLossLimit: 3000 }], +}; +const claimedBefore = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-25', amount: -1500 }]; + +test('projectPerfectPath: a same-day type trades each projected day from the base stage', () => { + const opts = { + accountId: 7, cfg: vestFunded, cycleDailyPnL: [], fundTransactions: claimedBefore, + balance: 50000, realizedPnL: 0, payout: { withdrawalPct: 1 }, now: cdt('2026-10-27T12:00:00'), + }; + const firstOf = (path, n) => path.slice(0, n).map((d) => [d.date, d.stage, d.withdrawal]); + + // An earlier day's claim doesn't count: today and every day after it + // make $1,500 on stage 1 and claim it. + const fresh = projectPerfectPath({ ...opts, stageInputs: { dailyLossLimit: 1229, stagesSameDay: true, sameDayWithdrawals: 0 } }); + assert.deepEqual(firstOf(fresh, 3), [['2026-10-27', 1, -1500], ['2026-10-28', 1, -1500], ['2026-10-29', 1, -1500]]); + assert.ok(fresh.every((d) => d.stage === 1)); + + // Claimed once already today: today's cycle is stage 2's $3,000, tomorrow $1,500 again. + const claimedToday = projectPerfectPath({ ...opts, stageInputs: { dailyLossLimit: 1229, stagesSameDay: true, sameDayWithdrawals: 1 } }); + assert.deepEqual(firstOf(claimedToday, 2), [['2026-10-27', 2, -3000], ['2026-10-28', 1, -1500]]); + + // Any other type counts every withdrawal, as it always has. + const lifetime = projectPerfectPath(opts); + assert.deepEqual(firstOf(lifetime, 3), [['2026-10-27', 2, -3000], ['2026-10-28', 3, -3000], ['2026-10-29', 4, -3000]]); + assert.deepEqual(projectPerfectPath({ ...opts, stageInputs: { dailyLossLimit: 1229, stagesSameDay: false, sameDayWithdrawals: 0 } }), lifetime); +}); + +test('endedCycleCompletion: a same-day type\'s ended cycle is judged on its day\'s base stage', () => { + // 2026-10-26 made $1,500 and the auto-claim took it before the reporter + // saw the final day; the account had claimed the day before too. The + // claim ended that day's stage 1, not a $3,000 stage 2. + const opts = { + accountId: 7, rawTradeDate: '2026-10-26', + fullDailyPnL: [{ date: '2026-10-25', pnl: 1500 }, { date: '2026-10-26', pnl: 1500 }], + cycleDailyPnL: [], + fundTransactions: [...claimedBefore, { date: '2026-10-26', amount: -1500 }], + lastFundDate: '2026-10-26', + endedCycleStart: { fundDate: '2026-10-25', includesDay: false }, + rawRealizedPnL: 0, equityProfit: 0, + cfg: { ...vestFunded, stagesSameDay: true }, + now: cdt('2026-10-26T12:00:00'), + }; + // Its stage stays the payout's number among the account's withdrawals, + // which the master clears the entry by. + assert.deepEqual(endedCycleCompletion(opts), { date: '2026-10-26', stage: 2, drain: false, cycle: '2026-10-25' }); + // Any other type: judged on stage 2's $3,000, as before. + assert.equal(endedCycleCompletion({ ...opts, cfg: vestFunded }), null); +}); diff --git a/types.ts b/types.ts index 86b6a8b..aac65e4 100644 --- a/types.ts +++ b/types.ts @@ -38,6 +38,9 @@ export interface AccountConfig { /** Hard daily loss limit in dollars (0/absent = none): every leg carries a * stop that keeps the firm's day inside it, less a reserve and fees. */ dailyLossLimit?: number; + /** The stage is picked by the payouts since the firm's last daily reset, + * and every new day starts back at the base stage (Vest). */ + stagesSameDay?: boolean; /** The firm trades every calendar day, weekends and CME holidays included. */ tradesEveryDay?: boolean; }