From 266100b65cb645112ebdae6eecaf1a30732d5d0f Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 27 Oct 2026 14:34:30 -0500 Subject: [PATCH] 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 --- app/accounts/[id]/page.tsx | 45 ++++++++++++++++++--------------- lib/trading-logic.ts | 29 ++++++++++++++++----- scripts/check-trading-logic.mjs | 32 ++++++++++++++++++----- 3 files changed, 74 insertions(+), 32 deletions(-) diff --git a/app/accounts/[id]/page.tsx b/app/accounts/[id]/page.tsx index bb91408..a77f4b7 100644 --- a/app/accounts/[id]/page.tsx +++ b/app/accounts/[id]/page.tsx @@ -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(); + const projFundMap = new Map(); + 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(); 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); } diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index af10716..7816806 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -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(); /** 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 }); } diff --git a/scripts/check-trading-logic.mjs b/scripts/check-trading-logic.mjs index 07f0b61..1e6d15d 100644 --- a/scripts/check-trading-logic.mjs +++ b/scripts/check-trading-logic.mjs @@ -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);