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:
co-authored by
Claude Opus 5.5
parent
e33e890a83
commit
266100b65c
+25
-20
@@ -600,7 +600,7 @@ export default function AccountPage() {
|
|||||||
// books exactly its plan, cycles complete, and the configured withdrawal
|
// books exactly its plan, cycles complete, and the configured withdrawal
|
||||||
// fires the day a cycle finishes. Same planner the trade path runs, so the
|
// 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
|
// 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({
|
const rawProjection = (cfg && !isDead) ? projectPerfectPath({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
cfg,
|
cfg,
|
||||||
@@ -610,7 +610,8 @@ export default function AccountPage() {
|
|||||||
realizedPnL: account.realizedPnL,
|
realizedPnL: account.realizedPnL,
|
||||||
// A type whose stages reset each day: its state stage counts today's payouts.
|
// A type whose stages reset each day: its state stage counts today's payouts.
|
||||||
stageInputs: { stagesSameDay: cfg.stagesSameDay === true, sameDayWithdrawals: account.stage - 1 },
|
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: {
|
payout: {
|
||||||
withdrawalPct: cfg.withdrawalPct,
|
withdrawalPct: cfg.withdrawalPct,
|
||||||
payoutCap: cfg.payoutCap,
|
payoutCap: cfg.payoutCap,
|
||||||
@@ -626,23 +627,21 @@ export default function AccountPage() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
}) : [];
|
}) : [];
|
||||||
// Cut the projection at the second payout — days beyond it are noise here.
|
// Cut the projection after the second payout day — days beyond it are
|
||||||
const secondPayoutIdx = (() => {
|
// noise here. The path's dates never go back, and a payoutReady entry is
|
||||||
let seen = 0;
|
// always its last.
|
||||||
for (let i = 0; i < rawProjection.length; i++) {
|
const payoutDays = [...new Set(rawProjection.filter((p) => p.withdrawal !== undefined || p.payoutReady).map((p) => p.date))];
|
||||||
const p = rawProjection[i];
|
const projection = payoutDays.length >= 2 ? rawProjection.filter((p) => p.date <= payoutDays[1]) : rawProjection;
|
||||||
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;
|
|
||||||
// A payout day is the cycle's FINAL trading day — it can carry both the
|
// A payout day is the cycle's FINAL trading day — it can carry both the
|
||||||
// day's booked P&L and the withdrawal.
|
// day's booked P&L and the withdrawal. A same-day type's day can hold
|
||||||
const projPnlMap = new Map(projection.filter((p) => p.pnl !== 0).map((p) => [p.date, p.pnl]));
|
// several cycles: their P&L and withdrawals add up, and the day shows
|
||||||
const projFundMap = new Map(projection.filter((p) => p.withdrawal !== undefined).map((p) => [p.date, p.withdrawal!]));
|
// 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]));
|
const projStageMap = new Map(projection.map((p) => [p.date, p.stage]));
|
||||||
/** Cumulative probability of the perfect path surviving through each projected day. */
|
/** Cumulative probability of the perfect path surviving through each projected day. */
|
||||||
const projProbMap = new Map(projection.map((p) => [p.date, p.prob]));
|
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();
|
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
|
const sortedWithdrawalDates = fundTransactions
|
||||||
.filter((f) => f.amount < 0)
|
.filter((f) => f.amount < 0)
|
||||||
.map((f) => f.date)
|
.map((f) => f.date)
|
||||||
.sort();
|
.sort();
|
||||||
const stageMap = new Map<string, number>();
|
const stageMap = new Map<string, number>();
|
||||||
for (const d of displayPnL) {
|
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);
|
stageMap.set(d.date, stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-6
@@ -534,15 +534,23 @@ export function projectPerfectPath(opts: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// A type whose stages reset each day (Vest) counts only the payouts made
|
// 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 —
|
// since its day began: today's real ones plus the path's own on that
|
||||||
// the path pays out at most once a day, as the day's last entry, so
|
// date, so every projected day starts on the base stage.
|
||||||
// every projected day trades the base stage.
|
|
||||||
const sameDay = opts.stageInputs?.stagesSameDay === true;
|
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
|
/** Payouts before the stage traded on `day`: that day's own for a
|
||||||
* same-day type, else every withdrawal so far. */
|
* same-day type, else every withdrawal so far. */
|
||||||
const paidBefore = (day: string): number => (sameDay
|
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);
|
: 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(
|
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,
|
||||||
@@ -604,6 +612,9 @@ export function projectPerfectPath(opts: {
|
|||||||
balance = round2(balance - gross);
|
balance = round2(balance - gross);
|
||||||
sim = []; // withdrawal excludes its day — fresh cycle
|
sim = []; // withdrawal excludes its day — fresh cycle
|
||||||
withdrawals++;
|
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
|
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 });
|
out.push({ date: onDate, pnl: dayPnl, stage, drain, withdrawal: -gross, prob, gross, net });
|
||||||
return false;
|
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;
|
let date = today;
|
||||||
for (let step = 0; step < horizon; step++) {
|
let again = todayHasSession && tradesAgain(today);
|
||||||
date = nextDay(date);
|
for (let step = 0; again || step < horizon;) {
|
||||||
|
if (again) again = false;
|
||||||
|
else { date = nextDay(date); step++; }
|
||||||
const eff = effectiveOn(date);
|
const eff = effectiveOn(date);
|
||||||
const stage = 1 + paidBefore(date);
|
const stage = 1 + paidBefore(date);
|
||||||
const plan = computeDayPlan(
|
const plan = computeDayPlan(
|
||||||
@@ -650,6 +665,7 @@ export function projectPerfectPath(opts: {
|
|||||||
// The cycle completed mid-projection with nothing left to trade —
|
// The cycle completed mid-projection with nothing left to trade —
|
||||||
// the pending payout lands on this day.
|
// the pending payout lands on this day.
|
||||||
if (finishCycle(date, 0, stage, eff.drain)) break;
|
if (finishCycle(date, 0, stage, eff.drain)) break;
|
||||||
|
again = tradesAgain(date);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,6 +689,7 @@ export function projectPerfectPath(opts: {
|
|||||||
&& eff2.minTradingDays <= qualifyingDays(eff2.minDayPnL, nd);
|
&& eff2.minTradingDays <= qualifyingDays(eff2.minDayPnL, nd);
|
||||||
if (doneAfter) {
|
if (doneAfter) {
|
||||||
if (finishCycle(date, pnl, stage, eff.drain)) break;
|
if (finishCycle(date, pnl, stage, eff.drain)) break;
|
||||||
|
again = tradesAgain(date);
|
||||||
} else {
|
} else {
|
||||||
out.push({ date, pnl, stage, drain: eff.drain, prob });
|
out.push({ date, pnl, stage, drain: eff.drain, prob });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1157,7 +1157,7 @@ const vestFunded = {
|
|||||||
};
|
};
|
||||||
const claimedBefore = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-25', amount: -1500 }];
|
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 = {
|
const opts = {
|
||||||
accountId: 7, cfg: vestFunded, cycleDailyPnL: [], fundTransactions: claimedBefore,
|
accountId: 7, cfg: vestFunded, cycleDailyPnL: [], fundTransactions: claimedBefore,
|
||||||
balance: 50000, realizedPnL: 0, payout: { withdrawalPct: 1 }, now: cdt('2026-10-27T12:00:00'),
|
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]);
|
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
|
// 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 } });
|
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.deepEqual(firstOf(fresh, 5), [
|
||||||
assert.ok(fresh.every((d) => d.stage === 1));
|
['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 } });
|
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.
|
// Any other type counts every withdrawal, as it always has.
|
||||||
const lifetime = projectPerfectPath(opts);
|
const lifetime = projectPerfectPath(opts);
|
||||||
|
|||||||
Reference in New Issue
Block a user