Vest: projected days trade the next stage after each payout, as the bot does

runClaims claims a same-day type's target and trades its next stage the
same day on the room the claim freed, but the projection paid out once a
day, so Vest's payout calendar showed $1,500 a day instead of $1,500 then
$3,000. Each payout now counts toward its own date, and the same date is
projected again on the next stage, once through the stages a day (the
last one repeats). A cycle that stood complete before today counts
toward today, when it is claimed. Other types project as before.

The account page sums a date's cycles, cuts after the second payout day,
and labels a same-day type's past days from that day's own payout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 14:34:30 -05:00
co-authored by Claude Opus 5.5
parent e33e890a83
commit 266100b65c
3 changed files with 74 additions and 32 deletions
+25 -20
View File
@@ -600,7 +600,7 @@ export default function AccountPage() {
// books exactly its plan, cycles complete, and the configured withdrawal
// fires the day a cycle finishes. Same planner the trade path runs, so the
// numbers (bumps and drain amounts included) are what would actually be
// aimed for. Shown up to TWO payouts ahead.
// aimed for. Shown up to TWO payout days ahead.
const rawProjection = (cfg && !isDead) ? projectPerfectPath({
accountId: account.id,
cfg,
@@ -610,7 +610,8 @@ export default function AccountPage() {
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,
// Such a type pays out once per stage on each of those days.
maxWithdrawals: cfg.stagesSameDay === true ? 2 * (1 + cfg.withdrawalStages.length) : 2,
payout: {
withdrawalPct: cfg.withdrawalPct,
payoutCap: cfg.payoutCap,
@@ -626,23 +627,21 @@ export default function AccountPage() {
: undefined,
},
}) : [];
// Cut the projection at the second payout — days beyond it are noise here.
const secondPayoutIdx = (() => {
let seen = 0;
for (let i = 0; i < rawProjection.length; i++) {
const p = rawProjection[i];
if (p.withdrawal !== undefined || p.payoutReady) {
seen++;
if (seen >= 2 || p.payoutReady) return i;
}
}
return -1;
})();
const projection = secondPayoutIdx >= 0 ? rawProjection.slice(0, secondPayoutIdx + 1) : rawProjection;
// Cut the projection after the second payout day — days beyond it are
// noise here. The path's dates never go back, and a payoutReady entry is
// always its last.
const payoutDays = [...new Set(rawProjection.filter((p) => p.withdrawal !== undefined || p.payoutReady).map((p) => p.date))];
const projection = payoutDays.length >= 2 ? rawProjection.filter((p) => p.date <= payoutDays[1]) : rawProjection;
// A payout day is the cycle's FINAL trading day — it can carry both the
// day's booked P&L and the withdrawal.
const projPnlMap = new Map(projection.filter((p) => p.pnl !== 0).map((p) => [p.date, p.pnl]));
const projFundMap = new Map(projection.filter((p) => p.withdrawal !== undefined).map((p) => [p.date, p.withdrawal!]));
// day's booked P&L and the withdrawal. A same-day type's day can hold
// several cycles: their P&L and withdrawals add up, and the day shows
// the last stage it traded.
const projPnlMap = new Map<string, number>();
const projFundMap = new Map<string, number>();
for (const p of projection) {
if (p.pnl !== 0) projPnlMap.set(p.date, Math.round(((projPnlMap.get(p.date) ?? 0) + p.pnl) * 100) / 100);
if (p.withdrawal !== undefined) projFundMap.set(p.date, Math.round(((projFundMap.get(p.date) ?? 0) + p.withdrawal) * 100) / 100);
}
const projStageMap = new Map(projection.map((p) => [p.date, p.stage]));
/** Cumulative probability of the perfect path surviving through each projected day. */
const projProbMap = new Map(projection.map((p) => [p.date, p.prob]));
@@ -657,14 +656,20 @@ export default function AccountPage() {
];
const calendarMonths = [...new Set(allCalendarDates.map((d) => d.slice(0, 7)))].sort();
// Build stage map: stage increments after each withdrawal (negative fund txn)
// Build stage map: stage increments after each withdrawal (negative fund txn).
// A type whose stages reset each day starts every day on stage 1 and
// trades its next stage after a payout that day, as the projected days
// show — one fund row a day holds all of that day's payouts, so a day
// with one reads stage 2.
const sortedWithdrawalDates = fundTransactions
.filter((f) => f.amount < 0)
.map((f) => f.date)
.sort();
const stageMap = new Map<string, number>();
for (const d of displayPnL) {
const stage = sortedWithdrawalDates.filter((wd) => wd < d.date).length + 1;
const stage = cfg?.stagesSameDay === true
? sortedWithdrawalDates.filter((wd) => wd === d.date).length + 1
: sortedWithdrawalDates.filter((wd) => wd < d.date).length + 1;
stageMap.set(d.date, stage);
}
+23 -6
View File
@@ -534,15 +534,23 @@ export function projectPerfectPath(opts: {
};
// 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.
// since its day began: today's real ones plus the path's own on that
// date, so every projected day starts on the base stage.
const sameDay = opts.stageInputs?.stagesSameDay === true;
/** The path's own payouts, by the date they are claimed on. */
const paidOn = new Map<string, number>();
/** 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)
? (day === today ? Math.max(0, Math.floor(opts.stageInputs?.sameDayWithdrawals ?? 0)) : 0) + (paidOn.get(day) ?? 0)
: funds.filter((f) => f.amount < 0).length);
/** 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
* stops there. */
const tradesAgain = (day: string): boolean => sameDay
&& (paidOn.get(day) ?? 0) > 0
&& paidBefore(day) <= cfg.withdrawalStages.length;
const effectiveOn = (day: string) => resolveEffectiveConfig(
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
@@ -604,6 +612,9 @@ export function projectPerfectPath(opts: {
balance = round2(balance - gross);
sim = []; // withdrawal excludes its day — fresh cycle
withdrawals++;
// A cycle that stood complete before today is claimed today.
const claimDay = onDate < today ? today : onDate;
paidOn.set(claimDay, (paidOn.get(claimDay) ?? 0) + 1);
high = Math.max(high, dd); // a payout locks the trail at breakeven
out.push({ date: onDate, pnl: dayPnl, stage, drain, withdrawal: -gross, prob, gross, net });
return false;
@@ -633,9 +644,13 @@ export function projectPerfectPath(opts: {
}
}
// A same-day type's next stage trades on the date its payout landed,
// today's included; anything else moves on a day.
let date = today;
for (let step = 0; step < horizon; step++) {
date = nextDay(date);
let again = todayHasSession && tradesAgain(today);
for (let step = 0; again || step < horizon;) {
if (again) again = false;
else { date = nextDay(date); step++; }
const eff = effectiveOn(date);
const stage = 1 + paidBefore(date);
const plan = computeDayPlan(
@@ -650,6 +665,7 @@ export function projectPerfectPath(opts: {
// The cycle completed mid-projection with nothing left to trade —
// the pending payout lands on this day.
if (finishCycle(date, 0, stage, eff.drain)) break;
again = tradesAgain(date);
continue;
}
@@ -673,6 +689,7 @@ export function projectPerfectPath(opts: {
&& eff2.minTradingDays <= qualifyingDays(eff2.minDayPnL, nd);
if (doneAfter) {
if (finishCycle(date, pnl, stage, eff.drain)) break;
again = tradesAgain(date);
} else {
out.push({ date, pnl, stage, drain: eff.drain, prob });
}
+26 -6
View File
@@ -1157,7 +1157,7 @@ const vestFunded = {
};
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', () => {
test('projectPerfectPath: a same-day type trades each projected day from the base stage, then its next stages that day', () => {
const opts = {
accountId: 7, cfg: vestFunded, cycleDailyPnL: [], fundTransactions: claimedBefore,
balance: 50000, realizedPnL: 0, payout: { withdrawalPct: 1 }, now: cdt('2026-10-27T12:00:00'),
@@ -1165,14 +1165,34 @@ test('projectPerfectPath: a same-day type trades each projected day from the bas
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.
// make $1,500 on stage 1 and claim it, then $3,000 on stage 2 and claim
// that — once through the stages a day.
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));
assert.deepEqual(firstOf(fresh, 5), [
['2026-10-27', 1, -1500], ['2026-10-27', 2, -3000],
['2026-10-28', 1, -1500], ['2026-10-28', 2, -3000],
['2026-10-29', 1, -1500],
]);
// Claimed once already today: today's cycle is stage 2's $3,000, tomorrow $1,500 again.
// Claimed once already today: today's cycle is stage 2's $3,000, tomorrow $1,500 and $3,000 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]]);
assert.deepEqual(firstOf(claimedToday, 3), [['2026-10-27', 2, -3000], ['2026-10-28', 1, -1500], ['2026-10-28', 2, -3000]]);
// A cycle that stood complete yesterday is claimed today, so today trades stage 2.
const unclaimed = projectPerfectPath({
...opts, cycleDailyPnL: [{ date: '2026-10-26', pnl: 1500 }], balance: 51500,
stageInputs: { dailyLossLimit: 1229, stagesSameDay: true, sameDayWithdrawals: 0 },
});
assert.deepEqual(firstOf(unclaimed, 3), [['2026-10-26', 1, -1500], ['2026-10-27', 2, -3000], ['2026-10-28', 1, -1500]]);
// Two listed stages: three payouts a day, and the day after starts over.
const twoStages = projectPerfectPath({
...opts, cfg: { ...vestFunded, withdrawalStages: [...vestFunded.withdrawalStages, { profit: 2000, consistency: 0, minTradingDays: 0 }] },
maxWithdrawals: 10, stageInputs: { stagesSameDay: true, sameDayWithdrawals: 0 },
});
assert.deepEqual(firstOf(twoStages, 4).map(([date, stage]) => [date, stage]), [
['2026-10-27', 1], ['2026-10-27', 2], ['2026-10-27', 3], ['2026-10-28', 1],
]);
// Any other type counts every withdrawal, as it always has.
const lifetime = projectPerfectPath(opts);