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 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-12 18:09:00 -05:00
co-authored by Claude Fable 5
parent 52d51afebb
commit 9b2d823210
3 changed files with 111 additions and 38 deletions
+55 -16
View File
@@ -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<string, number>;
fundMap: Map<string, number>;
stageMap: Map<string, number>;
/** 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<string, number>;
projFundMap?: Map<string, number>;
projStageMap?: Map<string, number>;
/** Cumulative probability of reaching each projected day (fair-walk ruin model). */
projProbMap?: Map<string, number>;
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
</span>
)}
{projPnl !== undefined && (
<span className="text-[11px] font-semibold tabular-nums leading-tight text-green-600/60">
<span className="text-[11px] font-semibold tabular-nums leading-tight text-green-600/70">
+${fmt(projPnl)}
</span>
)}
{projFund !== undefined && (
<span className="text-[10px] font-semibold tabular-nums leading-tight mt-0.5 text-amber-500/70">
<span className="text-[10px] font-semibold tabular-nums leading-tight mt-0.5 text-amber-500/80">
W/D −${fmt(Math.abs(projFund))}
</span>
)}
@@ -330,6 +334,14 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro
Payout ✓
</span>
)}
{projProb !== undefined && (
<span
className="text-[8px] font-medium tabular-nums leading-tight mt-0.5 text-slate-400"
title="Probability of the perfect path reaching this day (fair-walk ruin model, cumulative)"
>
{Math.round(projProb * 100)}%
</span>
)}
</div>
);
})}
@@ -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() {
<h2 className="text-slate-900 font-bold text-base">Daily P&L</h2>
{projection.length > 0 && (
<span className="text-xs text-slate-400">
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&apos;s final day, % = odds of reaching that day
</span>
)}
</div>
@@ -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}
/>
);
})}