From 9b2d823210581f5d951582e8b674e83da6d5bd59 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sat, 12 Sep 2026 18:09:00 -0500 Subject: [PATCH] Account calendar: reach probabilities, 2-payout horizon, outline cells, same-day payouts, real drawdown floors - projectPerfectPath books each cycle's payout ON its final trading day and anchors ruin probabilities to the platform's real liquidation floor (Tradovate auto-liq) when reported, else the configured trailing drawdown with the maxLoss/$2,000 fallback - Account calendar shows the cumulative reach % on every projected day, caps the projection at two payouts ahead, and renders potential days as dashed outlines instead of shaded cells Co-Authored-By: Claude Fable 5 --- app/accounts/[id]/page.tsx | 71 +++++++++++++++++++++++++++--------- lib/reporter.ts | 5 +++ lib/trading-logic.ts | 73 ++++++++++++++++++++++++++------------ 3 files changed, 111 insertions(+), 38 deletions(-) diff --git a/app/accounts/[id]/page.tsx b/app/accounts/[id]/page.tsx index 1d049bb..e5f5d06 100644 --- a/app/accounts/[id]/page.tsx +++ b/app/accounts/[id]/page.tsx @@ -204,16 +204,18 @@ const MONTH_NAMES = [ ]; const DOW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, projFundMap, projStageMap, payoutDate }: { +function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, projFundMap, projStageMap, projProbMap, payoutDate }: { year: number; month: number; pnlMap: Map; fundMap: Map; stageMap: Map; - /** Perfect-path projection — rendered faded and dashed on days with no real data. */ + /** Perfect-path projection — rendered as dashed outlines on days with no real data. */ projPnlMap?: Map; projFundMap?: Map; projStageMap?: Map; + /** Cumulative probability of reaching each projected day (fair-walk ruin model). */ + projProbMap?: Map; payoutDate?: string; }) { const daysInMonth = new Date(year, month, 0).getDate(); @@ -265,11 +267,13 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro const hasData = pnl !== undefined; const positive = hasData && pnl! >= 0; - // Projection only fills days with no real data. + // Projection only fills days with no real data. Potential + // days render as dashed OUTLINES — no fill. const projPnl = !hasData && fundAmt === undefined ? projPnlMap?.get(key) : undefined; const projFund = !hasData && fundAmt === undefined ? projFundMap?.get(key) : undefined; const isPayout = !hasData && key === payoutDate; const isProj = projPnl !== undefined || projFund !== undefined || isPayout; + const projProb = isProj ? projProbMap?.get(key) : undefined; const stage = stageMap.get(key) ?? (isProj ? projStageMap?.get(key) : undefined); return ( @@ -283,11 +287,11 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro : fundAmt !== undefined ? 'bg-amber-50 border border-amber-100' : isPayout - ? 'bg-blue-50/60 border border-dashed border-blue-200' + ? 'border border-dashed border-blue-300' : projFund !== undefined - ? 'bg-amber-50/50 border border-dashed border-amber-200' + ? 'border border-dashed border-amber-300' : projPnl !== undefined - ? 'bg-green-50/50 border border-dashed border-green-200' + ? 'border border-dashed border-green-300' : 'bg-slate-50 border border-transparent' }`} > @@ -316,12 +320,12 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro )} {projPnl !== undefined && ( - + +${fmt(projPnl)} )} {projFund !== undefined && ( - + W/D −${fmt(Math.abs(projFund))} )} @@ -330,6 +334,14 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro Payout ✓ )} + {projProb !== undefined && ( + + {Math.round(projProb * 100)}% + + )} ); })} @@ -474,26 +486,52 @@ export default function AccountPage() { const strokeGradId = 'equityStroke'; // Perfect-path projection: what the future looks like if every trading day - // books exactly its plan, cycles complete, and a 50% withdrawal 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 aimed for. - const projection = (cfg && !isDead) ? projectPerfectPath({ + // books exactly its plan, cycles complete, and the configured withdrawal + // 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 + // aimed for. Shown up to TWO payouts ahead. + const rawProjection = (cfg && !isDead) ? projectPerfectPath({ accountId: account.id, cfg, cycleDailyPnL: account.dailyPnL ?? [], fundTransactions, balance: account.amount, realizedPnL: account.realizedPnL, + maxWithdrawals: 2, payout: { withdrawalPct: cfg.withdrawalPct, payoutCap: cfg.payoutCap, profitSplit: cfg.profitSplit, - drawdown: cfg.trailingDrawdown ?? 0, + // Same default chain the reporter uses for reach probabilities. + drawdown: (cfg.trailingDrawdown ?? 0) > 0 + ? cfg.trailingDrawdown! + : Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000, + // The platform's real liquidation floor, when it reports one. + startThreshold: account.autoLiqThreshold > 0 + ? Math.round((account.autoLiqThreshold - cfg.accountSize) * 100) / 100 + : undefined, }, }) : []; - const projPnlMap = new Map(projection.filter((p) => p.withdrawal === undefined && !p.payoutReady).map((p) => [p.date, p.pnl])); + // Cut the projection at the second payout — days beyond it are noise here. + const secondPayoutIdx = (() => { + let seen = 0; + for (let i = 0; i < rawProjection.length; i++) { + const p = rawProjection[i]; + 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 + // day's booked P&L and the withdrawal. + const projPnlMap = new Map(projection.filter((p) => p.pnl !== 0).map((p) => [p.date, p.pnl])); const projFundMap = new Map(projection.filter((p) => p.withdrawal !== undefined).map((p) => [p.date, p.withdrawal!])); const projStageMap = new Map(projection.map((p) => [p.date, p.stage])); + /** Cumulative probability of the perfect path surviving through each projected day. */ + const projProbMap = new Map(projection.map((p) => [p.date, p.prob])); const payoutDate = projection.find((p) => p.payoutReady)?.date; // Build calendar data @@ -775,7 +813,7 @@ export default function AccountPage() {

Daily P&L

{projection.length > 0 && ( - Dashed days: the perfect path — planned targets booked daily, 50% withdrawals when a stage completes + Dashed outlines: the perfect path, two payouts ahead — planned targets daily, the projected withdrawal on each cycle's final day, % = odds of reaching that day )} @@ -787,7 +825,8 @@ export default function AccountPage() { key={ym} year={y} month={m} pnlMap={pnlMap} fundMap={fundMap} stageMap={stageMap} projPnlMap={projPnlMap} projFundMap={projFundMap} - projStageMap={projStageMap} payoutDate={payoutDate} + projStageMap={projStageMap} projProbMap={projProbMap} + payoutDate={payoutDate} /> ); })} diff --git a/lib/reporter.ts b/lib/reporter.ts index 6ce9f5d..2c00211 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -152,6 +152,11 @@ function collectFirmStats() { ? cfg.trailingDrawdown! : Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000, startHigh: highWater, + // Tradovate reports the account's REAL liquidation + // floor — anchor the trail there for exact room. + startThreshold: autoLiqThreshold > 0 + ? r2(autoLiqThreshold - cfg.accountSize) + : undefined, }, }) .filter((p) => p.withdrawal !== undefined || p.payoutReady) diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index 84643ab..3be3bdd 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -398,6 +398,11 @@ export function projectPerfectPath(opts: { drawdown?: number; /** Current cycle's end-of-day equity-profit high-water mark. */ startHigh?: number; + /** The account's REAL current liquidation floor on the equity-profit + * scale (platform floor − account size), when the platform reports + * one (Tradovate auto-liq). The modeled trail never sits below it, + * so today's room is exact and only tightens going forward. */ + startThreshold?: number; }; }): ProjectedDay[] { const { accountId, cfg } = opts; @@ -424,10 +429,16 @@ export function projectPerfectPath(opts: { // telescope, so a whole day survives with exactly room/(room + target). let high = Math.max(0, opts.payout?.startHigh ?? 0, balance - cfg.accountSize); if (funds.some((f) => f.amount < 0)) high = Math.max(high, dd); + // The platform's real floor (when reported) is the baseline the modeled + // trail can never sit below — a floor that already trailed higher than + // our history explains stays exact. + const startThreshold = opts.payout?.startThreshold; let prob = 1; const dayProb = (dayTarget: number): number => { - if (dd <= 0 || dayTarget <= 0) return 1; - const threshold = high >= dd ? 0 : high - dd; + if (dayTarget <= 0) return 1; + if (dd <= 0 && startThreshold === undefined) return 1; + const modeled = dd > 0 ? (high >= dd ? 0 : high - dd) : -Infinity; + const threshold = Math.max(modeled, startThreshold ?? -Infinity); const room = (balance - cfg.accountSize) - threshold; if (room <= 0) return 0; return room / (room + dayTarget); @@ -485,32 +496,35 @@ export function projectPerfectPath(opts: { const daysNeeded = eff.minTradingDays > qualifyingDays(eff.minDayPnL, date); - if (plan.dayTarget <= 0 && !daysNeeded) { - // Cycle complete — nothing left to trade. 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. + // 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 withdrawal = -gross; - if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && withdrawal < -1) { - funds.push({ date, amount: withdrawal }); - balance = round2(balance + withdrawal); + 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: 0, stage, drain: eff.drain, withdrawal, prob, - gross, net: round2(gross * profitSplit), - }); - continue; + out.push({ date, pnl: dayPnl, stage, drain, withdrawal: -gross, prob, gross, net }); + return false; } - out.push({ - date, pnl: 0, stage, drain: eff.drain, payoutReady: true, prob, - gross, net: round2(gross * profitSplit), - }); - break; + 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; + continue; } // Extra days (min-day 0, days still owed) book a token dollar so the @@ -520,7 +534,22 @@ export function projectPerfectPath(opts: { sim.push({ date, pnl }); balance = round2(balance + pnl); high = Math.max(high, balance - cfg.accountSize); - out.push({ date, pnl, stage, drain: eff.drain, prob }); + + // Did this day complete the cycle? Then it IS the payout day. + const nd = nextTradingDate(date); + const eff2 = effectiveNow(); + const plan2 = computeDayPlan( + accountId, nd, eff2.profitTarget, eff2.consistency, sim, eff2.minDayPnL, + eff2.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff2.drain, + ); + const doneAfter = plan2 !== null + && plan2.dayTarget <= 0 + && eff2.minTradingDays <= qualifyingDays(eff2.minDayPnL, nd); + if (doneAfter) { + if (finishCycle(pnl, eff.drain)) break; + } else { + out.push({ date, pnl, stage, drain: eff.drain, prob }); + } } return out;