Vest: the payout projection runs on Vest's day, and stages count each payout
projectPerfectPath takes the raw trade date (default: the Central one): the reporter passes its tradeDateFor day and the account page the one /api/state now gives each account. From 5 PM CT to 20:00 ET it took tomorrow as today while every input was Vest's, so the day's claim moved a day late, the next day's stage-1 payout dropped, and an open Vest-day row counted twice. Without same-day stages, the stage comes from the client's payout list (payoutDays, one per payout) when it has read one, not the negative fund rows: Vest books a day's two claims as one row, so the engine, claimDue, the stage label and the projection stayed a stage behind the report. StageInputs.payouts carries the count; a client with no list counts rows as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
032ba1ca9c
commit
4a8e027056
@@ -617,8 +617,13 @@ export default function AccountPage() {
|
||||
fundTransactions,
|
||||
balance: account.amount,
|
||||
realizedPnL: account.realizedPnL,
|
||||
// A type whose stages reset each day: its state stage counts today's payouts.
|
||||
stageInputs: { stagesSameDay: cfg.stagesSameDay === true, sameDayWithdrawals: account.stage - 1 },
|
||||
// The day the firm's numbers follow (Vest's, until 20:00 ET).
|
||||
rawTradeDate: account.tradeDate,
|
||||
// The state stage counts the payouts that pick it: today's for a type
|
||||
// whose stages reset each day, else every one, each on its own.
|
||||
stageInputs: cfg.stagesSameDay === true
|
||||
? { stagesSameDay: true, sameDayWithdrawals: account.stage - 1 }
|
||||
: { payouts: 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: {
|
||||
|
||||
+10
-7
@@ -10,7 +10,7 @@ import { obfuscationDayBooked } from '@/lib/auto-trade';
|
||||
import type { AccountState } from '@/types';
|
||||
import {
|
||||
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, tradeDateFor,
|
||||
obfuscationStopDollars, dllStopDollars, botStopsActive, stageInputsFor,
|
||||
obfuscationStopDollars, dllStopDollars, botStopsActive, stageInputsFor, stagePayouts,
|
||||
maxLossRoom, lastStand as isLastStand, shortestNextLeg,
|
||||
LEG_COMPLETION_EPSILON,
|
||||
} from '@/lib/trading-logic';
|
||||
@@ -63,11 +63,11 @@ export async function GET() {
|
||||
const isDead = autoLiqThreshold !== 0 && cash.amount <= autoLiqThreshold;
|
||||
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
||||
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
||||
// Stage = 1 + number of withdrawals (negative fund transactions),
|
||||
// or of today's payouts for a type whose stages reset each day.
|
||||
const stage = 1 + (cfg?.stagesSameDay
|
||||
? (client.sameDayClaims?.(acc.id) ?? 0)
|
||||
: allFundTxns.filter((f) => f.amount < 0).length);
|
||||
// Stage = 1 + number of payouts (negative fund transactions, or
|
||||
// the client's own list where a row holds several), or of
|
||||
// today's for a type whose stages reset each day.
|
||||
const stageInputs = stageInputsFor(client, acc.id, cfg ?? {});
|
||||
const stage = 1 + stagePayouts(allFundTxns, stageInputs);
|
||||
// Stage-resolved config: a drain stage swaps in its own min-day
|
||||
// (default $5) and no profit target.
|
||||
const effective = cfg ? resolveEffectiveConfig(
|
||||
@@ -79,7 +79,7 @@ export async function GET() {
|
||||
priorProfit,
|
||||
allFundTxns,
|
||||
cfg.minDayPnL,
|
||||
stageInputsFor(client, acc.id, cfg),
|
||||
stageInputs,
|
||||
) : null;
|
||||
// Days traded counts only days that hit minDayPnL (when set) — that's what
|
||||
// the firm requires toward the min trading day rule.
|
||||
@@ -272,6 +272,9 @@ export async function GET() {
|
||||
// The combined day's P&L, not just the current session's —
|
||||
// on the Tuesday after a holiday this includes Sun/Mon.
|
||||
realizedPnL: combined.realizedPnL,
|
||||
// The firm's raw trade date, which the account page
|
||||
// projects the payouts from.
|
||||
tradeDate,
|
||||
daysTraded,
|
||||
// An account the bot puts stops on whose day booked red via
|
||||
// its own stop — the personal DLL. Firm DLL lockouts show
|
||||
|
||||
@@ -221,6 +221,7 @@ function collectFirmStats() {
|
||||
fundTransactions: fundTxns,
|
||||
balance: cash.amount,
|
||||
realizedPnL: cash.realizedPnL,
|
||||
rawTradeDate: rawToday,
|
||||
stageInputs: stageInputsFor(client, acc.id, cfg),
|
||||
payout: {
|
||||
withdrawalPct: cfg.withdrawalPct,
|
||||
|
||||
+28
-8
@@ -37,6 +37,19 @@ export interface StageInputs {
|
||||
dailyLossLimit?: number;
|
||||
stagesSameDay?: boolean;
|
||||
sameDayWithdrawals?: number;
|
||||
/** Every payout the account has made, one per payout, where a fund row
|
||||
* can hold several (PlatformClient.payoutDays — Vest books a day's
|
||||
* claims as one row). Absent = one per negative fund row. */
|
||||
payouts?: number;
|
||||
}
|
||||
|
||||
/** The payouts that pick an account's stage: today's for a type whose
|
||||
* stages reset each day, else every one — the client's own count when it
|
||||
* gives one (StageInputs.payouts), else the negative fund rows. */
|
||||
export function stagePayouts(fundTransactions: { date: string; amount: number }[], inputs: StageInputs = {}): number {
|
||||
if (inputs.stagesSameDay) return Math.max(0, Math.floor(inputs.sameDayWithdrawals ?? 0));
|
||||
if (typeof inputs.payouts === 'number' && Number.isFinite(inputs.payouts)) return Math.max(0, Math.floor(inputs.payouts));
|
||||
return fundTransactions.filter((f) => f.amount < 0).length;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,9 +86,7 @@ export function resolveEffectiveConfig(
|
||||
// Same-day stages (Vest): only today's payouts count, so every new
|
||||
// day starts back at stage 1 — the room a payout frees lasts only
|
||||
// until the firm's next daily reset.
|
||||
const withdrawalCount = inputs.stagesSameDay
|
||||
? Math.max(0, Math.floor(inputs.sameDayWithdrawals ?? 0))
|
||||
: fundTransactions.filter((f) => f.amount < 0).length;
|
||||
const withdrawalCount = stagePayouts(fundTransactions, inputs);
|
||||
if (withdrawalCount === 0) {
|
||||
return { profitTarget, consistency, minTradingDays, minDayPnL, drain: false, dailyLossLimit }; // Stage 1 = base values
|
||||
}
|
||||
@@ -480,6 +491,9 @@ export function projectPerfectPath(opts: {
|
||||
};
|
||||
/** Defaults to the current time. */
|
||||
now?: Date;
|
||||
/** The raw trade date the inputs follow (tradeDateFor — Vest's turns at
|
||||
* 20:00 ET); defaults to the Central one at `now`. */
|
||||
rawTradeDate?: string;
|
||||
}): ProjectedDay[] {
|
||||
const { accountId, cfg } = opts;
|
||||
const horizon = opts.horizonDays ?? 40;
|
||||
@@ -490,7 +504,7 @@ export function projectPerfectPath(opts: {
|
||||
const dd = Math.max(0, opts.payout?.drawdown ?? 0);
|
||||
const everyDay = cfg.tradesEveryDay === true;
|
||||
|
||||
const rawToday = currentTradeDateCT(opts.now);
|
||||
const rawToday = opts.rawTradeDate ?? currentTradeDateCT(opts.now);
|
||||
// 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
|
||||
@@ -551,11 +565,13 @@ export function projectPerfectPath(opts: {
|
||||
const sameDay = opts.stageInputs?.stagesSameDay === true;
|
||||
/** The path's own payouts, by the date they are claimed on. */
|
||||
const paidOn = new Map<string, number>();
|
||||
/** The account's real payouts, one per payout (stagePayouts). */
|
||||
const paidReal = stagePayouts(opts.fundTransactions, { payouts: opts.stageInputs?.payouts });
|
||||
/** Payouts before the stage traded on `day`: that day's own for a
|
||||
* same-day type, else every withdrawal so far. */
|
||||
const paidBefore = (day: string): number => (sameDay
|
||||
? (day === today ? Math.max(0, Math.floor(opts.stageInputs?.sameDayWithdrawals ?? 0)) : 0) + (paidOn.get(day) ?? 0)
|
||||
: funds.filter((f) => f.amount < 0).length);
|
||||
: paidReal + withdrawals);
|
||||
/** A same-day type trades its next stage on the date a payout lands
|
||||
* (runClaims claims, then trades again on the room the claim freed) —
|
||||
* once through its stages a day: the last one repeats, and the path
|
||||
@@ -566,7 +582,7 @@ export function projectPerfectPath(opts: {
|
||||
const effectiveOn = (day: string) => resolveEffectiveConfig(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
|
||||
sameDay ? { stagesSameDay: true, sameDayWithdrawals: paidBefore(day) } : {},
|
||||
sameDay ? { stagesSameDay: true, sameDayWithdrawals: paidBefore(day) } : { payouts: paidBefore(day) },
|
||||
);
|
||||
/** The payout parameters of the stage being completed: per-stage
|
||||
* overrides when set, else the type defaults. */
|
||||
@@ -1429,17 +1445,21 @@ export function dllReserve(dailyLossLimit: number, notional = 0, slippageFractio
|
||||
export const DLL_MIN_STOP = 50;
|
||||
|
||||
/** The stage inputs for one account: the type's daily loss limit, and for a
|
||||
* type whose stages reset each day, the client's count of today's payouts. */
|
||||
* type whose stages reset each day, the client's count of today's payouts —
|
||||
* for any other, of every payout, where the client lists them one by one
|
||||
* (payoutDays) and has read them. */
|
||||
export function stageInputsFor(
|
||||
client: { sameDayClaims?: (accountId: number) => number },
|
||||
client: { sameDayClaims?: (accountId: number) => number; payoutDays?: (accountId: number) => string[] | undefined },
|
||||
accountId: number,
|
||||
cfg: { dailyLossLimit?: number; stagesSameDay?: boolean },
|
||||
): StageInputs {
|
||||
const sameDay = cfg.stagesSameDay === true;
|
||||
const payouts = !sameDay && typeof client.payoutDays === 'function' ? client.payoutDays(accountId)?.length : undefined;
|
||||
return {
|
||||
dailyLossLimit: cfg.dailyLossLimit ?? 0,
|
||||
stagesSameDay: sameDay,
|
||||
sameDayWithdrawals: sameDay && typeof client.sameDayClaims === 'function' ? client.sameDayClaims(accountId) : 0,
|
||||
...(payouts !== undefined ? { payouts } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
projectPerfectPath, cycleCompletion, endedCycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM,
|
||||
dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget,
|
||||
currentTradeDateCT, foldReason, withinFirmCaps, personSizeFactor, personHoldHours, PERSON_SIZE_SPREAD, machineSide,
|
||||
resolveEffectiveConfig, stageInputsFor, floorRoom, resetClearStop, shortestNextLeg, computeDayPlan, clearsClose, RESET_CLEAR_HOLDS,
|
||||
resolveEffectiveConfig, stageInputsFor, stagePayouts, floorRoom, resetClearStop, shortestNextLeg, computeDayPlan, clearsClose, RESET_CLEAR_HOLDS,
|
||||
legCloseNote,
|
||||
} from '../lib/trading-logic.ts';
|
||||
|
||||
@@ -1337,6 +1337,61 @@ test('projectPerfectPath: a same-day type trades each projected day from the bas
|
||||
assert.deepEqual(projectPerfectPath({ ...opts, stageInputs: { dailyLossLimit: 1229, stagesSameDay: false, sameDayWithdrawals: 0 } }), lifetime);
|
||||
});
|
||||
|
||||
test('projectPerfectPath: today is the raw trade date it is given — Vest\'s, still the day before from 5 PM CT to 20:00 ET', () => {
|
||||
const firstOf = (path, n) => path.slice(0, n).map((d) => [d.date, d.stage, d.withdrawal]);
|
||||
// Claimed once on Vest day 10-27 and $1,000 made since.
|
||||
const claimed = {
|
||||
accountId: 7, cfg: vestFunded, cycleDailyPnL: [], fundTransactions: [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -1500 }],
|
||||
balance: 51000, realizedPnL: 1000, payout: { withdrawalPct: 1 }, stageInputs: { stagesSameDay: true, sameDayWithdrawals: 1 },
|
||||
};
|
||||
const before = projectPerfectPath({ ...claimed, now: cdt('2026-10-27T16:30:00') });
|
||||
assert.deepEqual(firstOf(before, 3), [['2026-10-27', 2, -3000], ['2026-10-28', 1, -1500], ['2026-10-28', 2, -3000]]);
|
||||
// 5:30 PM CT: the Central date has rolled, Vest's hasn't — the day's
|
||||
// claim stays on it, and 10-28 keeps its stage-1 payout.
|
||||
assert.deepEqual(projectPerfectPath({ ...claimed, now: cdt('2026-10-27T17:30:00'), rawTradeDate: '2026-10-27' }), before);
|
||||
// No date given: the Central one, as before.
|
||||
assert.equal(projectPerfectPath({ ...claimed, now: cdt('2026-10-27T17:30:00') })[0].date, '2026-10-28');
|
||||
|
||||
// No claim yet, Vest day 10-27's row already booked: counted once, as today's.
|
||||
const open = {
|
||||
...claimed, fundTransactions: claimedBefore, cycleDailyPnL: [{ date: '2026-10-27', pnl: 1000 }],
|
||||
stageInputs: { stagesSameDay: true, sameDayWithdrawals: 0 },
|
||||
};
|
||||
const earlier = projectPerfectPath({ ...open, now: cdt('2026-10-27T16:30:00') });
|
||||
assert.deepEqual(firstOf(earlier, 2), [['2026-10-27', 1, -1500], ['2026-10-27', 2, -3000]]);
|
||||
assert.deepEqual(projectPerfectPath({ ...open, now: cdt('2026-10-27T17:30:00'), rawTradeDate: '2026-10-27' }), earlier);
|
||||
});
|
||||
|
||||
test('stages without the same-day reset count each payout, where one fund row holds a day\'s two (Vest)', () => {
|
||||
// Two listed stages; claims of $1,500 and $3,000 on Vest day 10-27 share its row.
|
||||
const stages = [{ profit: 3000, consistency: 0, minTradingDays: 0 }, { profit: 2000, consistency: 0, minTradingDays: 0 }];
|
||||
const rows = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -4500 }];
|
||||
const resolve = (inputs) => resolveEffectiveConfig(1500, 0, 0, false, stages, 0, rows, 0, inputs);
|
||||
assert.equal(resolve({ payouts: 2 }).profitTarget, 2000);
|
||||
// No payout list: one per negative fund row, as always.
|
||||
assert.equal(resolve({}).profitTarget, 3000);
|
||||
// A same-day type still counts only today's.
|
||||
assert.equal(resolve({ stagesSameDay: true, sameDayWithdrawals: 0, payouts: 2 }).profitTarget, 1500);
|
||||
|
||||
// stageInputsFor takes the count from the client's payout list, when it has read one.
|
||||
const client = { sameDayClaims: () => 2, payoutDays: () => ['2026-10-27', '2026-10-27'] };
|
||||
assert.deepEqual(stageInputsFor(client, 7, {}), { dailyLossLimit: 0, stagesSameDay: false, sameDayWithdrawals: 0, payouts: 2 });
|
||||
assert.deepEqual(stageInputsFor({ payoutDays: () => undefined }, 7, {}), { dailyLossLimit: 0, stagesSameDay: false, sameDayWithdrawals: 0 });
|
||||
assert.deepEqual(stageInputsFor(client, 7, { stagesSameDay: true }), { dailyLossLimit: 0, stagesSameDay: true, sameDayWithdrawals: 2 });
|
||||
// The state route's stage label counts the same way.
|
||||
assert.equal(stagePayouts(rows, stageInputsFor(client, 7, {})), 2);
|
||||
assert.equal(stagePayouts(rows, stageInputsFor({}, 7, {})), 1);
|
||||
|
||||
// The projection starts from the same count: stage 3's $2,000 next, then it repeats.
|
||||
const opts = {
|
||||
accountId: 7, cfg: { ...vestFunded, withdrawalStages: stages }, cycleDailyPnL: [], fundTransactions: rows,
|
||||
balance: 50000, realizedPnL: 0, payout: { withdrawalPct: 1 }, now: cdt('2026-10-27T12:00:00'),
|
||||
};
|
||||
const firstOf = (path, n) => path.slice(0, n).map((d) => [d.date, d.stage, d.withdrawal]);
|
||||
assert.deepEqual(firstOf(projectPerfectPath({ ...opts, stageInputs: stageInputsFor(client, 7, {}) }), 2), [['2026-10-27', 3, -2000], ['2026-10-28', 4, -2000]]);
|
||||
assert.deepEqual(firstOf(projectPerfectPath(opts), 2), [['2026-10-27', 2, -3000], ['2026-10-28', 3, -2000]]);
|
||||
});
|
||||
|
||||
test('endedCycleCompletion: a same-day type\'s ended cycle is judged on its day\'s base stage', () => {
|
||||
// 2026-10-26 made $1,500 and the auto-claim took it before the reporter
|
||||
// saw the final day; the account had claimed the day before too. The
|
||||
|
||||
@@ -54,7 +54,7 @@ const { putVestSession } = await import('../lib/vest-session.ts');
|
||||
const { botOpenedPositions, loadAccountMeta, saveAccountMeta } = await import('../lib/db.ts');
|
||||
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts');
|
||||
const { encodeContractName } = await import('../lib/dxtrade-helpers.ts');
|
||||
const { tradeDateFor } = await import('../lib/trading-logic.ts');
|
||||
const { tradeDateFor, stageInputsFor, resolveEffectiveConfig } = await import('../lib/trading-logic.ts');
|
||||
const { isClientReady } = await import('../lib/platform-client.ts');
|
||||
|
||||
const iso = (s) => Date.parse(s);
|
||||
@@ -396,3 +396,24 @@ test("the client's Vest day turns with its numbers, and a sync that stalls stops
|
||||
client.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
test('two payouts on one Vest day share its fund row, and still count two toward the stage', async () => {
|
||||
clockAt('2026-10-27T23:00:00Z');
|
||||
const client = await syncingClient(unanswered);
|
||||
try {
|
||||
// $1,500 then $3,000 taken on Vest day 10-27: Vest's two WITHDRAWs, one row.
|
||||
client.withdrawals.set(A, [{ at: iso('2026-10-27T15:00:00Z'), amount: -1500 }, { at: iso('2026-10-27T19:00:00Z'), amount: -3000 }]);
|
||||
client.fundTransactions[A] = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -4500 }];
|
||||
assert.deepEqual(client.payoutDays(A), ['2026-10-27', '2026-10-27']);
|
||||
// A type whose stages don't reset each day trades stage 3's $2,000, not stage 2's again.
|
||||
const stages = [{ profit: 3000, consistency: 0, minTradingDays: 0 }, { profit: 2000, consistency: 0, minTradingDays: 0 }];
|
||||
const inputs = stageInputsFor(client, A, { stagesSameDay: false });
|
||||
assert.equal(inputs.payouts, 2);
|
||||
assert.equal(resolveEffectiveConfig(1500, 0, 0, false, stages, 0, client.fundTransactions[A], 0, inputs).profitTarget, 2000);
|
||||
// Before its history is read: one per negative fund row.
|
||||
client.historyLoadedIds.delete(A);
|
||||
assert.equal(stageInputsFor(client, A, {}).payouts, undefined);
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -84,6 +84,9 @@ export interface AccountState {
|
||||
active: boolean;
|
||||
amount: number;
|
||||
realizedPnL: number;
|
||||
/** The firm's raw trade date (tradeDateFor): Vest's turns at 20:00 ET,
|
||||
* every other firm's at 5 PM CT. */
|
||||
tradeDate: string;
|
||||
daysTraded: number;
|
||||
positionDirection: 'long' | 'short' | null;
|
||||
/** Balance floor from Tradovate's auto-liquidation profile (0 = not set) */
|
||||
@@ -99,7 +102,9 @@ export interface AccountState {
|
||||
* consistency and minimum days all met (cycleCompletion 'today'). Only
|
||||
* the final day; the firm may lock or remove the account afterwards. */
|
||||
completed: boolean;
|
||||
/** Current stage = 1 + number of withdrawals (negative fund transactions). Stage 1 = fresh account. */
|
||||
/** Current stage = 1 + number of withdrawals (negative fund transactions,
|
||||
* or the platform's own list where a row holds several; today's for a
|
||||
* type whose stages reset each day). Stage 1 = fresh account. */
|
||||
stage: number;
|
||||
/** True when the current stage is a drain stage (spam-withdraw mode). */
|
||||
stageDrain: boolean;
|
||||
|
||||
Reference in New Issue
Block a user