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:
co-authored by
Claude Fable 5
parent
52d51afebb
commit
9b2d823210
+55
-16
@@ -204,16 +204,18 @@ const MONTH_NAMES = [
|
|||||||
];
|
];
|
||||||
const DOW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
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;
|
year: number;
|
||||||
month: number;
|
month: number;
|
||||||
pnlMap: Map<string, number>;
|
pnlMap: Map<string, number>;
|
||||||
fundMap: Map<string, number>;
|
fundMap: Map<string, number>;
|
||||||
stageMap: 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>;
|
projPnlMap?: Map<string, number>;
|
||||||
projFundMap?: Map<string, number>;
|
projFundMap?: Map<string, number>;
|
||||||
projStageMap?: Map<string, number>;
|
projStageMap?: Map<string, number>;
|
||||||
|
/** Cumulative probability of reaching each projected day (fair-walk ruin model). */
|
||||||
|
projProbMap?: Map<string, number>;
|
||||||
payoutDate?: string;
|
payoutDate?: string;
|
||||||
}) {
|
}) {
|
||||||
const daysInMonth = new Date(year, month, 0).getDate();
|
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 hasData = pnl !== undefined;
|
||||||
const positive = hasData && pnl! >= 0;
|
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 projPnl = !hasData && fundAmt === undefined ? projPnlMap?.get(key) : undefined;
|
||||||
const projFund = !hasData && fundAmt === undefined ? projFundMap?.get(key) : undefined;
|
const projFund = !hasData && fundAmt === undefined ? projFundMap?.get(key) : undefined;
|
||||||
const isPayout = !hasData && key === payoutDate;
|
const isPayout = !hasData && key === payoutDate;
|
||||||
const isProj = projPnl !== undefined || projFund !== undefined || isPayout;
|
const isProj = projPnl !== undefined || projFund !== undefined || isPayout;
|
||||||
|
const projProb = isProj ? projProbMap?.get(key) : undefined;
|
||||||
const stage = stageMap.get(key) ?? (isProj ? projStageMap?.get(key) : undefined);
|
const stage = stageMap.get(key) ?? (isProj ? projStageMap?.get(key) : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -283,11 +287,11 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro
|
|||||||
: fundAmt !== undefined
|
: fundAmt !== undefined
|
||||||
? 'bg-amber-50 border border-amber-100'
|
? 'bg-amber-50 border border-amber-100'
|
||||||
: isPayout
|
: isPayout
|
||||||
? 'bg-blue-50/60 border border-dashed border-blue-200'
|
? 'border border-dashed border-blue-300'
|
||||||
: projFund !== undefined
|
: projFund !== undefined
|
||||||
? 'bg-amber-50/50 border border-dashed border-amber-200'
|
? 'border border-dashed border-amber-300'
|
||||||
: projPnl !== undefined
|
: projPnl !== undefined
|
||||||
? 'bg-green-50/50 border border-dashed border-green-200'
|
? 'border border-dashed border-green-300'
|
||||||
: 'bg-slate-50 border border-transparent'
|
: 'bg-slate-50 border border-transparent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -316,12 +320,12 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{projPnl !== undefined && (
|
{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)}
|
+${fmt(projPnl)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{projFund !== undefined && (
|
{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))}
|
W/D −${fmt(Math.abs(projFund))}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -330,6 +334,14 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, pro
|
|||||||
Payout ✓
|
Payout ✓
|
||||||
</span>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -474,26 +486,52 @@ export default function AccountPage() {
|
|||||||
const strokeGradId = 'equityStroke';
|
const strokeGradId = 'equityStroke';
|
||||||
|
|
||||||
// Perfect-path projection: what the future looks like if every trading day
|
// Perfect-path projection: what the future looks like if every trading day
|
||||||
// books exactly its plan, cycles complete, and a 50% withdrawal fires the
|
// books exactly its plan, cycles complete, and the configured withdrawal
|
||||||
// day a cycle finishes. Same planner the trade path runs, so the numbers
|
// fires the day a cycle finishes. Same planner the trade path runs, so the
|
||||||
// (bumps and drain amounts included) are what would actually be aimed for.
|
// numbers (bumps and drain amounts included) are what would actually be
|
||||||
const projection = (cfg && !isDead) ? projectPerfectPath({
|
// aimed for. Shown up to TWO payouts ahead.
|
||||||
|
const rawProjection = (cfg && !isDead) ? projectPerfectPath({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
cfg,
|
cfg,
|
||||||
cycleDailyPnL: account.dailyPnL ?? [],
|
cycleDailyPnL: account.dailyPnL ?? [],
|
||||||
fundTransactions,
|
fundTransactions,
|
||||||
balance: account.amount,
|
balance: account.amount,
|
||||||
realizedPnL: account.realizedPnL,
|
realizedPnL: account.realizedPnL,
|
||||||
|
maxWithdrawals: 2,
|
||||||
payout: {
|
payout: {
|
||||||
withdrawalPct: cfg.withdrawalPct,
|
withdrawalPct: cfg.withdrawalPct,
|
||||||
payoutCap: cfg.payoutCap,
|
payoutCap: cfg.payoutCap,
|
||||||
profitSplit: cfg.profitSplit,
|
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 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]));
|
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;
|
const payoutDate = projection.find((p) => p.payoutReady)?.date;
|
||||||
|
|
||||||
// Build calendar data
|
// Build calendar data
|
||||||
@@ -775,7 +813,7 @@ export default function AccountPage() {
|
|||||||
<h2 className="text-slate-900 font-bold text-base">Daily P&L</h2>
|
<h2 className="text-slate-900 font-bold text-base">Daily P&L</h2>
|
||||||
{projection.length > 0 && (
|
{projection.length > 0 && (
|
||||||
<span className="text-xs text-slate-400">
|
<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's final day, % = odds of reaching that day
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -787,7 +825,8 @@ export default function AccountPage() {
|
|||||||
key={ym} year={y} month={m}
|
key={ym} year={y} month={m}
|
||||||
pnlMap={pnlMap} fundMap={fundMap} stageMap={stageMap}
|
pnlMap={pnlMap} fundMap={fundMap} stageMap={stageMap}
|
||||||
projPnlMap={projPnlMap} projFundMap={projFundMap}
|
projPnlMap={projPnlMap} projFundMap={projFundMap}
|
||||||
projStageMap={projStageMap} payoutDate={payoutDate}
|
projStageMap={projStageMap} projProbMap={projProbMap}
|
||||||
|
payoutDate={payoutDate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ function collectFirmStats() {
|
|||||||
? cfg.trailingDrawdown!
|
? cfg.trailingDrawdown!
|
||||||
: Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000,
|
: Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000,
|
||||||
startHigh: highWater,
|
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)
|
.filter((p) => p.withdrawal !== undefined || p.payoutReady)
|
||||||
|
|||||||
+50
-21
@@ -398,6 +398,11 @@ export function projectPerfectPath(opts: {
|
|||||||
drawdown?: number;
|
drawdown?: number;
|
||||||
/** Current cycle's end-of-day equity-profit high-water mark. */
|
/** Current cycle's end-of-day equity-profit high-water mark. */
|
||||||
startHigh?: number;
|
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[] {
|
}): ProjectedDay[] {
|
||||||
const { accountId, cfg } = opts;
|
const { accountId, cfg } = opts;
|
||||||
@@ -424,10 +429,16 @@ export function projectPerfectPath(opts: {
|
|||||||
// telescope, so a whole day survives with exactly room/(room + target).
|
// telescope, so a whole day survives with exactly room/(room + target).
|
||||||
let high = Math.max(0, opts.payout?.startHigh ?? 0, balance - cfg.accountSize);
|
let high = Math.max(0, opts.payout?.startHigh ?? 0, balance - cfg.accountSize);
|
||||||
if (funds.some((f) => f.amount < 0)) high = Math.max(high, dd);
|
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;
|
let prob = 1;
|
||||||
const dayProb = (dayTarget: number): number => {
|
const dayProb = (dayTarget: number): number => {
|
||||||
if (dd <= 0 || dayTarget <= 0) return 1;
|
if (dayTarget <= 0) return 1;
|
||||||
const threshold = high >= dd ? 0 : high - dd;
|
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;
|
const room = (balance - cfg.accountSize) - threshold;
|
||||||
if (room <= 0) return 0;
|
if (room <= 0) return 0;
|
||||||
return room / (room + dayTarget);
|
return room / (room + dayTarget);
|
||||||
@@ -485,32 +496,35 @@ export function projectPerfectPath(opts: {
|
|||||||
|
|
||||||
const daysNeeded = eff.minTradingDays > qualifyingDays(eff.minDayPnL, date);
|
const daysNeeded = eff.minTradingDays > qualifyingDays(eff.minDayPnL, date);
|
||||||
|
|
||||||
if (plan.dayTarget <= 0 && !daysNeeded) {
|
// Fires the payout ON the given date — the day the cycle's final
|
||||||
// Cycle complete — nothing left to trade. The withdrawal takes
|
// trading day books, the payout becomes requestable. The withdrawal
|
||||||
// the completed stage's share of the EQUITY PROFIT (for a
|
// takes the completed stage's share of the EQUITY PROFIT (for a
|
||||||
// $0-start account that is the whole balance), capped. Prop
|
// $0-start account that is the whole balance), capped. Prop payouts
|
||||||
// payouts come out of profit, never the funded principal.
|
// 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 { pct, cap } = stagePayout();
|
||||||
const rawGross = round2(Math.max(0, balance - cfg.accountSize) * pct);
|
const rawGross = round2(Math.max(0, balance - cfg.accountSize) * pct);
|
||||||
const gross = cap > 0 ? Math.min(rawGross, round2(cap)) : rawGross;
|
const gross = cap > 0 ? Math.min(rawGross, round2(cap)) : rawGross;
|
||||||
const withdrawal = -gross;
|
const net = round2(gross * profitSplit);
|
||||||
if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && withdrawal < -1) {
|
if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && gross > 1) {
|
||||||
funds.push({ date, amount: withdrawal });
|
funds.push({ date, amount: -gross });
|
||||||
balance = round2(balance + withdrawal);
|
balance = round2(balance - gross);
|
||||||
sim = []; // withdrawal excludes its day — fresh cycle
|
sim = []; // withdrawal excludes its day — fresh cycle
|
||||||
withdrawals++;
|
withdrawals++;
|
||||||
high = Math.max(high, dd); // a payout locks the trail at breakeven
|
high = Math.max(high, dd); // a payout locks the trail at breakeven
|
||||||
out.push({
|
out.push({ date, pnl: dayPnl, stage, drain, withdrawal: -gross, prob, gross, net });
|
||||||
date, pnl: 0, stage, drain: eff.drain, withdrawal, prob,
|
return false;
|
||||||
gross, net: round2(gross * profitSplit),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
out.push({
|
out.push({ date, pnl: dayPnl, stage, drain, payoutReady: true, prob, gross, net });
|
||||||
date, pnl: 0, stage, drain: eff.drain, payoutReady: true, prob,
|
return true;
|
||||||
gross, net: round2(gross * profitSplit),
|
};
|
||||||
});
|
|
||||||
break;
|
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
|
// Extra days (min-day 0, days still owed) book a token dollar so the
|
||||||
@@ -520,8 +534,23 @@ export function projectPerfectPath(opts: {
|
|||||||
sim.push({ date, pnl });
|
sim.push({ date, pnl });
|
||||||
balance = round2(balance + pnl);
|
balance = round2(balance + pnl);
|
||||||
high = Math.max(high, balance - cfg.accountSize);
|
high = Math.max(high, balance - cfg.accountSize);
|
||||||
|
|
||||||
|
// 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 });
|
out.push({ date, pnl, stage, drain: eff.drain, prob });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user