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:
co-authored by
Claude Opus 5.5
parent
4add379c68
commit
e33e890a83
@@ -608,6 +608,8 @@ export default function AccountPage() {
|
|||||||
fundTransactions,
|
fundTransactions,
|
||||||
balance: account.amount,
|
balance: account.amount,
|
||||||
realizedPnL: account.realizedPnL,
|
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,
|
maxWithdrawals: 2,
|
||||||
payout: {
|
payout: {
|
||||||
withdrawalPct: cfg.withdrawalPct,
|
withdrawalPct: cfg.withdrawalPct,
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ function collectFirmStats() {
|
|||||||
fundTransactions: fundTxns,
|
fundTransactions: fundTxns,
|
||||||
balance: cash.amount,
|
balance: cash.amount,
|
||||||
realizedPnL: cash.realizedPnL,
|
realizedPnL: cash.realizedPnL,
|
||||||
|
stageInputs: stageInputsFor(client, acc.id, cfg),
|
||||||
payout: {
|
payout: {
|
||||||
withdrawalPct: cfg.withdrawalPct,
|
withdrawalPct: cfg.withdrawalPct,
|
||||||
payoutCap: cfg.payoutCap,
|
payoutCap: cfg.payoutCap,
|
||||||
|
|||||||
+38
-14
@@ -442,6 +442,11 @@ export function projectPerfectPath(opts: {
|
|||||||
fundTransactions: { date: string; amount: number }[];
|
fundTransactions: { date: string; amount: number }[];
|
||||||
balance: number;
|
balance: number;
|
||||||
realizedPnL: 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. */
|
/** Trading days to look ahead. */
|
||||||
horizonDays?: number;
|
horizonDays?: number;
|
||||||
maxWithdrawals?: number;
|
maxWithdrawals?: number;
|
||||||
@@ -528,17 +533,28 @@ export function projectPerfectPath(opts: {
|
|||||||
return room / (room + dayTarget);
|
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.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||||
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
|
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. */
|
* overrides when set, else the type defaults. */
|
||||||
const stagePayout = (): { pct: number; cap: number } => {
|
const stagePayout = (stage: number): { pct: number; cap: number } => {
|
||||||
const count = funds.filter((f) => f.amount < 0).length;
|
const count = stage - 1;
|
||||||
if (count === 0 || cfg.withdrawalStages.length === 0) return { pct: basePct, cap: baseCap };
|
if (count === 0 || cfg.withdrawalStages.length === 0) return { pct: basePct, cap: baseCap };
|
||||||
const stage = cfg.withdrawalStages[Math.min(count - 1, cfg.withdrawalStages.length - 1)];
|
const s = cfg.withdrawalStages[Math.min(count - 1, cfg.withdrawalStages.length - 1)];
|
||||||
return { pct: stage.withdrawalPct ?? basePct, cap: stage.payoutCap ?? baseCap };
|
return { pct: s.withdrawalPct ?? basePct, cap: s.payoutCap ?? baseCap };
|
||||||
};
|
};
|
||||||
const qualifyingDays = (minDay: number, excludeDate: string) => (
|
const qualifyingDays = (minDay: number, excludeDate: string) => (
|
||||||
minDay > 0
|
minDay > 0
|
||||||
@@ -550,7 +566,7 @@ export function projectPerfectPath(opts: {
|
|||||||
// Skipped when "today" is a weekend date (Saturday, or Friday evening
|
// Skipped when "today" is a weekend date (Saturday, or Friday evening
|
||||||
// after the roll): there is no session to finish.
|
// after the roll): there is no session to finish.
|
||||||
if (todayHasSession) {
|
if (todayHasSession) {
|
||||||
const eff = effectiveNow();
|
const eff = effectiveOn(today);
|
||||||
const plan = computeDayPlan(
|
const plan = computeDayPlan(
|
||||||
accountId, today, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
accountId, today, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||||
eff.minTradingDays, balance - cfg.accountSize, opts.realizedPnL, cfg.splitEnabled, eff.drain,
|
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.
|
// over and over. Only a non-payoutable eval stops after one completion.
|
||||||
const loops = cfg.withdrawalStages.length > 0 || cfg.payoutable === true;
|
const loops = cfg.withdrawalStages.length > 0 || cfg.payoutable === true;
|
||||||
const finishCycle = (onDate: string, dayPnl: number, stage: number, drain: boolean): boolean => {
|
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 rawGross = round2(Math.max(0, balance - cfg.accountSize) * pct);
|
||||||
const gross = cap > 0 ? Math.min(rawGross, round2(cap)) : rawGross;
|
const gross = cap > 0 ? Math.min(rawGross, round2(cap)) : rawGross;
|
||||||
const net = round2(gross * profitSplit);
|
const net = round2(gross * profitSplit);
|
||||||
@@ -601,14 +617,14 @@ export function projectPerfectPath(opts: {
|
|||||||
// requestable after that session's close. Never "today" — a projection
|
// requestable after that session's close. Never "today" — a projection
|
||||||
// computed over the weekend must not slide a Friday finish to Sunday.
|
// computed over the weekend must not slide a Friday finish to Sunday.
|
||||||
{
|
{
|
||||||
const eff = effectiveNow();
|
const eff = effectiveOn(today);
|
||||||
const probe = nextDay(today);
|
const probe = nextDay(today);
|
||||||
const plan = computeDayPlan(
|
const plan = computeDayPlan(
|
||||||
accountId, probe, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
accountId, probe, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||||
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
|
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
|
||||||
);
|
);
|
||||||
if (plan && plan.dayTarget <= 0 && eff.minTradingDays <= qualifyingDays(eff.minDayPnL, probe)) {
|
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 traded = sim.filter((d) => d.pnl !== 0);
|
||||||
const doneOn = traded.length > 0
|
const doneOn = traded.length > 0
|
||||||
? traded.reduce((a, b) => (a.date > b.date ? a : b)).date
|
? traded.reduce((a, b) => (a.date > b.date ? a : b)).date
|
||||||
@@ -620,8 +636,8 @@ export function projectPerfectPath(opts: {
|
|||||||
let date = today;
|
let date = today;
|
||||||
for (let step = 0; step < horizon; step++) {
|
for (let step = 0; step < horizon; step++) {
|
||||||
date = nextDay(date);
|
date = nextDay(date);
|
||||||
const eff = effectiveNow();
|
const eff = effectiveOn(date);
|
||||||
const stage = 1 + funds.filter((f) => f.amount < 0).length;
|
const stage = 1 + paidBefore(date);
|
||||||
const plan = computeDayPlan(
|
const plan = computeDayPlan(
|
||||||
accountId, date, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
accountId, date, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||||
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
|
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.
|
// Did this day complete the cycle? Then it IS the payout day.
|
||||||
const nd = nextDay(date);
|
const nd = nextDay(date);
|
||||||
const eff2 = effectiveNow();
|
const eff2 = effectiveOn(date);
|
||||||
const plan2 = computeDayPlan(
|
const plan2 = computeDayPlan(
|
||||||
accountId, nd, eff2.profitTarget, eff2.consistency, sim, eff2.minDayPnL,
|
accountId, nd, eff2.profitTarget, eff2.consistency, sim, eff2.minDayPnL,
|
||||||
eff2.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff2.drain,
|
eff2.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff2.drain,
|
||||||
@@ -819,6 +835,8 @@ export function endedCycleCompletion(opts: {
|
|||||||
targetSameEquity: boolean; withdrawalStages: WithdrawalStage[];
|
targetSameEquity: boolean; withdrawalStages: WithdrawalStage[];
|
||||||
combineCmeHolidays: boolean; splitEnabled: boolean;
|
combineCmeHolidays: boolean; splitEnabled: boolean;
|
||||||
tradesEveryDay?: boolean;
|
tradesEveryDay?: boolean;
|
||||||
|
/** Stages reset each day (StageInputs). */
|
||||||
|
stagesSameDay?: boolean;
|
||||||
};
|
};
|
||||||
/** Defaults to the current time. */
|
/** Defaults to the current time. */
|
||||||
now?: Date;
|
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 fundAmt = fundTransactions.filter((f) => f.date === lastFundDate).reduce((s, f) => s + f.amount, 0);
|
||||||
const equityBefore = round2(opts.equityProfit - curTotal - fundAmt);
|
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(
|
const eff = resolveEffectiveConfig(
|
||||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||||
cfg.withdrawalStages, 0, earlierFunds, cfg.minDayPnL,
|
cfg.withdrawalStages, 0, earlierFunds, cfg.minDayPnL,
|
||||||
|
cfg.stagesSameDay === true ? { stagesSameDay: true, sameDayWithdrawals: 0 } : {},
|
||||||
);
|
);
|
||||||
const rows = combineCmeSessions(combine, endedRaw, lastFundDate, 0).dailyPnL;
|
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.
|
// 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 {
|
return {
|
||||||
date: traded.map((d) => d.date).reduce(latest),
|
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,
|
stage: 1 + earlierFunds.filter((f) => f.amount < 0).length,
|
||||||
drain: eff.drain,
|
drain: eff.drain,
|
||||||
cycle: prevFundDate ?? '',
|
cycle: prevFundDate ?? '',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import assert from 'node:assert/strict';
|
|||||||
import {
|
import {
|
||||||
percentFee, takeProfitPlan, onTick, obfuscationStopDollars, dllRoom, dllStopDollars, dllViable, stopTicks,
|
percentFee, takeProfitPlan, onTick, obfuscationStopDollars, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||||||
botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS,
|
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,
|
dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget,
|
||||||
currentTradeDateCT, foldReason, withinFirmCaps, personSizeFactor, personHoldHours, PERSON_SIZE_SPREAD, machineSide,
|
currentTradeDateCT, foldReason, withinFirmCaps, personSizeFactor, personHoldHours, PERSON_SIZE_SPREAD, machineSide,
|
||||||
resolveEffectiveConfig, stageInputsFor, floorRoom, resetClearStop, shortestNextLeg, computeDayPlan,
|
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.ok(plan.dayTarget >= 3000 && plan.dayTarget <= 3025, `dayTarget ${plan.dayTarget}`);
|
||||||
assert.equal(plan.remainingToday, 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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export interface AccountConfig {
|
|||||||
/** Hard daily loss limit in dollars (0/absent = none): every leg carries a
|
/** 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. */
|
* stop that keeps the firm's day inside it, less a reserve and fees. */
|
||||||
dailyLossLimit?: number;
|
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. */
|
/** The firm trades every calendar day, weekends and CME holidays included. */
|
||||||
tradesEveryDay?: boolean;
|
tradesEveryDay?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user