From 70eab33d1b443e6825d62ea4bf28d6648a2fbf85 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sat, 12 Sep 2026 19:01:27 -0500 Subject: [PATCH] Payout projection: a completed cycle is requestable today, not next session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/trading-logic.ts | 73 +++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index eefdfdc..f58f1d6 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -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 }); }