From fd6a2e33b6334b8703cc043c6ef3afa587b0656b Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sun, 13 Sep 2026 14:56:11 -0500 Subject: [PATCH] Payout projection counts days the way the firm does (CME-holiday combining) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit projectPerfectPath worked on the raw daily P&L, so for a combining firm a holiday-session trade (e.g. the Labor Day session) counted as its own trading day — an account one real day short of its min days looked complete and anchored its payout to the last traded day. The projection now folds sessions through combineCmeSessions like the live path, and projected days skip dates that clear into the next full day. Co-Authored-By: Claude Fable 5 --- lib/trading-logic.ts | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index c38be9f..fd81bfc 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -389,6 +389,10 @@ export function projectPerfectPath(opts: { profitTarget: number; consistency: number; minDayPnL: number; minTradingDays: number; accountSize: number; targetSameEquity: boolean; withdrawalStages: WithdrawalStage[]; splitEnabled: boolean; + /** Firm counts CME holiday sessions with the next full day — the + * projection then folds P&L rows the same way and never books a + * projected day on a holiday session. */ + combineCmeHolidays?: boolean; /** True = completing a cycle pays out and the account keeps going * (withdraw, reset, re-target). False = it just PASSES once (an * eval) and the projection stops. */ @@ -427,16 +431,32 @@ export function projectPerfectPath(opts: { const profitSplit = opts.payout?.profitSplit ?? 0.9; const dd = Math.max(0, opts.payout?.drawdown ?? 0); - const today = currentTradeDateCT(); + const rawToday = currentTradeDateCT(); + // The firm's own day view: a combining firm folds holiday-session P&L + // into its clearing date — a Labor-Day trade and the Tuesday after it are + // ONE trading day, exactly as the firm counts min days. Identity view + // when combining is off. + const combine = cfg.combineCmeHolidays === true; + const view = combineCmeSessions(combine, opts.cycleDailyPnL, rawToday, opts.realizedPnL); // A weekend "today" (Saturday, or Sunday before the evening open) has no // session — the platform's realizedPnL is the LAST session's stale number, // already booked in cycleDailyPnL under its own date. Adding it as a - // weekend row would fabricate an extra qualifying day. - const todayDow = new Date(`${today}T00:00:00Z`).getUTCDay(); + // today row would fabricate an extra qualifying day. + const todayDow = new Date(`${rawToday}T00:00:00Z`).getUTCDay(); const todayHasSession = todayDow !== 0 && todayDow !== 6; - let sim = opts.cycleDailyPnL.filter((d) => d.date !== today); - if (todayHasSession && opts.realizedPnL !== 0) sim = [...sim, { date: today, pnl: opts.realizedPnL }]; - else sim = [...sim]; + // Today's clearing date when a session exists; on weekends keep the raw + // date so the projection's first day is Monday, not the day after it + // (combinedTradeDate rolls weekend dates forward). + const today = todayHasSession ? view.tradeDate : rawToday; + let sim = view.dailyPnL.filter((d) => d.date !== today); + if (todayHasSession && view.realizedPnL !== 0) sim = [...sim, { date: today, pnl: view.realizedPnL }]; + /** Next date the firm settles a session on: weekends always skip, and a + * combining firm's holiday sessions clear into the next full day. */ + const nextDay = (d: string): string => { + let n = nextTradingDate(d); + while (combine && combinedTradeDate(n) !== n) n = nextTradingDate(n); + return n; + }; const funds = [...opts.fundTransactions]; let balance = opts.balance; let withdrawals = 0; @@ -538,7 +558,7 @@ export function projectPerfectPath(opts: { // computed over the weekend must not slide a Friday finish to Sunday. { const eff = effectiveNow(); - const probe = nextTradingDate(today); + const probe = nextDay(today); const plan = computeDayPlan( accountId, probe, eff.profitTarget, eff.consistency, sim, eff.minDayPnL, eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain, @@ -555,7 +575,7 @@ export function projectPerfectPath(opts: { let date = today; for (let step = 0; step < horizon; step++) { - date = nextTradingDate(date); + date = nextDay(date); const eff = effectiveNow(); const stage = 1 + funds.filter((f) => f.amount < 0).length; const plan = computeDayPlan( @@ -582,7 +602,7 @@ export function projectPerfectPath(opts: { high = Math.max(high, balance - cfg.accountSize); // Did this day complete the cycle? Then it IS the payout day. - const nd = nextTradingDate(date); + const nd = nextDay(date); const eff2 = effectiveNow(); const plan2 = computeDayPlan( accountId, nd, eff2.profitTarget, eff2.consistency, sim, eff2.minDayPnL,