Payout projection: a completed cycle is requestable today, not next session

When target and min trading days are already satisfied, the payout fires
on the current trade date instead of being pushed to the next session —
so an account that has already hit its target with its days in shows the
payout as requestable now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-12 19:01:27 -05:00
co-authored by Claude Fable 5
parent bf56a76a6d
commit 70eab33d1b
+45 -28
View File
@@ -484,6 +484,47 @@ export function projectPerfectPath(opts: {
}
}
// Fires the payout ON the given date — the day the cycle's final trading
// day books (or today, when the cycle already stands complete), the
// payout becomes requestable. The withdrawal takes the completed stage's
// share of the EQUITY PROFIT (for a $0-start account that is the whole
// balance), capped. Prop payouts come out of profit, never the funded
// principal. Returns true when the projection should stop (no further
// stages).
const finishCycle = (onDate: string, dayPnl: number, stage: number, drain: boolean): boolean => {
const { pct, cap } = stagePayout();
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);
if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && gross > 1) {
funds.push({ date: onDate, amount: -gross });
balance = round2(balance - gross);
sim = []; // withdrawal excludes its day — fresh cycle
withdrawals++;
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;
}
out.push({ date: onDate, pnl: dayPnl, stage, drain, payoutReady: true, prob, gross, net });
return true;
};
// Already complete right now — target hit and min days done? Then the
// payout is requestable TODAY, whatever day of the week it is, not on
// the next session.
{
const eff = effectiveNow();
const probe = nextTradingDate(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;
if (finishCycle(today, 0, stage, eff.drain)) return out;
}
}
let date = today;
for (let step = 0; step < horizon; step++) {
date = nextTradingDate(date);
@@ -497,34 +538,10 @@ export function projectPerfectPath(opts: {
const daysNeeded = eff.minTradingDays > qualifyingDays(eff.minDayPnL, date);
// Fires the payout ON the given date — the day the cycle's final
// trading day books, the payout becomes requestable. The withdrawal
// takes the completed stage's share of the EQUITY PROFIT (for a
// $0-start account that is the whole balance), capped. Prop payouts
// come out of profit, never the funded principal. Returns true when
// the projection should stop (no further stages).
const finishCycle = (dayPnl: number, drain: boolean): boolean => {
const { pct, cap } = stagePayout();
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);
if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && gross > 1) {
funds.push({ date, amount: -gross });
balance = round2(balance - gross);
sim = []; // withdrawal excludes its day — fresh cycle
withdrawals++;
high = Math.max(high, dd); // a payout locks the trail at breakeven
out.push({ date, pnl: dayPnl, stage, drain, withdrawal: -gross, prob, gross, net });
return false;
}
out.push({ date, pnl: dayPnl, stage, drain, payoutReady: true, prob, gross, net });
return true;
};
if (plan.dayTarget <= 0 && !daysNeeded) {
// The cycle was already complete before this day — the pending
// payout lands on the first projected day.
if (finishCycle(0, eff.drain)) break;
// 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;
continue;
}
@@ -547,7 +564,7 @@ export function projectPerfectPath(opts: {
&& plan2.dayTarget <= 0
&& eff2.minTradingDays <= qualifyingDays(eff2.minDayPnL, nd);
if (doneAfter) {
if (finishCycle(pnl, eff.drain)) break;
if (finishCycle(date, pnl, stage, eff.drain)) break;
} else {
out.push({ date, pnl, stage, drain: eff.drain, prob });
}