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,