Merge branch 'vest-fix-display' into vest-fixes

# Conflicts:
#	package.json
#	scripts/check-trading-logic.mjs
This commit is contained in:
Brandon Li
2026-10-27 14:51:59 -05:00
11 changed files with 350 additions and 60 deletions
+49 -29
View File
@@ -282,6 +282,7 @@ function ObjRow({ passed, label, value }: { passed: boolean; label: ReactNode; v
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function EquityTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null;
// Always read from the raw data point so fill-only series don't interfere
@@ -487,9 +488,11 @@ export default function AccountPage() {
}, [accountId]);
useEffect(() => {
void load();
// The first load from a timer too, like the polls: not setState
// from the effect's own body.
const first = setTimeout(() => { void load(); }, 0);
const interval = setInterval(() => { void load(); }, 5000);
return () => clearInterval(interval);
return () => { clearTimeout(first); clearInterval(interval); };
}, [load]);
if (!account) {
@@ -513,9 +516,12 @@ export default function AccountPage() {
// The firm's hard daily loss limit, and the least a day must make to count
// as a trading day — two different numbers; neither is set on every type.
const dailyLossLimit = (cfg?.dailyLossLimit ?? 0) > 0 ? cfg!.dailyLossLimit! : null;
// The limit and the min days are the current stage's, which may set its own.
const stageLossLimit = account.effectiveDailyLossLimit ?? cfg?.dailyLossLimit ?? 0;
const dailyLossLimit = stageLossLimit > 0 ? stageLossLimit : null;
const minDayPnL = (cfg?.minDayPnL ?? 0) > 0 ? cfg!.minDayPnL : null;
const daysPassed = cfg != null && account.daysTraded >= cfg.minTradingDays;
const minTradingDays = account.effectiveMinTradingDays ?? cfg?.minTradingDays ?? 0;
const daysPassed = cfg != null && account.daysTraded >= minTradingDays;
// Dead when balance hits the liquidation floor — Tradovate reports its
// (positive) floor; TopstepX floors are reconstructed MLLs, negative or
// +$0.01 once locked at breakeven. 0 = no floor known.
@@ -552,13 +558,19 @@ export default function AccountPage() {
}
const sortedEvents = [...allDates].sort().map((d) => eventsByDate.get(d)!);
// Running equity after each event, summed here rather than inside the
// map below: a render callback mustn't reassign an outer variable.
const runningAfter: number[] = [];
let running = 0;
for (const ev of sortedEvents) {
if (ev.fundAmt !== undefined) running += ev.fundAmt; // withdrawals reduce equity
if (ev.pnl !== undefined) running += ev.pnl;
runningAfter.push(running);
}
const equityData = [
{ label: '', equity: 0, pnl: 0, origin: true, pos: 0, neg: 0, isWithdrawal: false },
...sortedEvents.map((ev) => {
if (ev.fundAmt !== undefined) running += ev.fundAmt; // withdrawals reduce equity
if (ev.pnl !== undefined) running += ev.pnl;
const equity = Math.round(running * 100) / 100;
...sortedEvents.map((ev, i) => {
const equity = Math.round(runningAfter[i] * 100) / 100;
return {
label: fmtDate(ev.date),
equity,
@@ -597,7 +609,7 @@ export default function AccountPage() {
// 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.
// aimed for. Shown up to TWO payout days ahead.
const rawProjection = (cfg && !isDead) ? projectPerfectPath({
accountId: account.id,
cfg,
@@ -605,7 +617,10 @@ export default function AccountPage() {
fundTransactions,
balance: account.amount,
realizedPnL: account.realizedPnL,
maxWithdrawals: 2,
// A type whose stages reset each day: its state stage counts today's payouts.
stageInputs: { stagesSameDay: cfg.stagesSameDay === true, sameDayWithdrawals: account.stage - 1 },
// Such a type pays out once per stage on each of those days.
maxWithdrawals: cfg.stagesSameDay === true ? 2 * (1 + cfg.withdrawalStages.length) : 2,
payout: {
withdrawalPct: cfg.withdrawalPct,
payoutCap: cfg.payoutCap,
@@ -621,23 +636,21 @@ export default function AccountPage() {
: undefined,
},
}) : [];
// 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;
// Cut the projection after the second payout day — days beyond it are
// noise here. The path's dates never go back, and a payoutReady entry is
// always its last.
const payoutDays = [...new Set(rawProjection.filter((p) => p.withdrawal !== undefined || p.payoutReady).map((p) => p.date))];
const projection = payoutDays.length >= 2 ? rawProjection.filter((p) => p.date <= payoutDays[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!]));
// day's booked P&L and the withdrawal. A same-day type's day can hold
// several cycles: their P&L and withdrawals add up, and the day shows
// the last stage it traded.
const projPnlMap = new Map<string, number>();
const projFundMap = new Map<string, number>();
for (const p of projection) {
if (p.pnl !== 0) projPnlMap.set(p.date, Math.round(((projPnlMap.get(p.date) ?? 0) + p.pnl) * 100) / 100);
if (p.withdrawal !== undefined) projFundMap.set(p.date, Math.round(((projFundMap.get(p.date) ?? 0) + p.withdrawal) * 100) / 100);
}
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]));
@@ -652,14 +665,20 @@ export default function AccountPage() {
];
const calendarMonths = [...new Set(allCalendarDates.map((d) => d.slice(0, 7)))].sort();
// Build stage map: stage increments after each withdrawal (negative fund txn)
// Build stage map: stage increments after each withdrawal (negative fund txn).
// A type whose stages reset each day starts every day on stage 1 and
// trades its next stage after a payout that day, as the projected days
// show — one fund row a day holds all of that day's payouts, so a day
// with one reads stage 2.
const sortedWithdrawalDates = fundTransactions
.filter((f) => f.amount < 0)
.map((f) => f.date)
.sort();
const stageMap = new Map<string, number>();
for (const d of displayPnL) {
const stage = sortedWithdrawalDates.filter((wd) => wd < d.date).length + 1;
const stage = cfg?.stagesSameDay === true
? sortedWithdrawalDates.filter((wd) => wd === d.date).length + 1
: sortedWithdrawalDates.filter((wd) => wd < d.date).length + 1;
stageMap.set(d.date, stage);
}
@@ -739,7 +758,7 @@ export default function AccountPage() {
<ObjRow
passed={daysPassed}
label="Trading Days"
value={cfg ? `${account.daysTraded} of ${cfg.minTradingDays}` : '—'}
value={cfg ? `${account.daysTraded} of ${minTradingDays}` : '—'}
/>
{dailyTarget != null && (
<div className="flex items-center justify-between py-3 border-b border-slate-100 last:border-0">
@@ -892,6 +911,7 @@ export default function AccountPage() {
stroke={`url(#${strokeGradId})`}
strokeWidth={2.5}
fill="none"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dot={(props: any) => {
if (props.payload?.origin) return <g key={props.key} />;
return (
+4 -2
View File
@@ -173,7 +173,7 @@ export async function GET() {
};
}
}
if (plan && !dayBooked && botStopsActive(cfg)) {
if (plan && !dayBooked && botStopsActive(cfg, effective)) {
const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday);
// The reserve grows with the position (dllReserve): at
// the size the next leg would trade, where it's known.
@@ -276,7 +276,7 @@ export async function GET() {
// An account the bot puts stops on whose day booked red via
// its own stop — the personal DLL. Firm DLL lockouts show
// via !active instead.
personalDll: !!cfg && botStopsActive(cfg)
personalDll: !!cfg && !!effective && botStopsActive(cfg, effective)
&& acc.active && !client.positions[acc.id]
&& combined.realizedPnL <= -25,
positionDirection: client.positions[acc.id]
@@ -296,6 +296,8 @@ export async function GET() {
lastStand: lastStandNext,
remainingToday,
effectiveProfitTarget,
effectiveDailyLossLimit: effective?.dailyLossLimit ?? null,
effectiveMinTradingDays: effective?.minTradingDays ?? null,
dailyPnL,
fullDailyPnL: combineCmeSessions(
cfg?.combineCmeHolidays === true,
+1 -1
View File
@@ -453,7 +453,7 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned, onStateChang
<AccountTypeCell account={account} firm={firm} onAssigned={onAssigned} />
</td>
<td className="px-4 py-3 text-slate-600 tabular-nums">
{account.daysTraded} / {cfg?.minTradingDays ?? '—'}
{account.daysTraded} / {account.effectiveMinTradingDays ?? cfg?.minTradingDays ?? '—'}
</td>
<td className="px-4 py-3 text-slate-600 tabular-nums">
<div className="flex flex-col">