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,
|
fundTransactions,
|
||||||
balance: account.amount,
|
balance: account.amount,
|
||||||
realizedPnL: account.realizedPnL,
|
realizedPnL: account.realizedPnL,
|
||||||
// A type whose stages reset each day: its state stage counts today's payouts.
|
// The day the firm's numbers follow (Vest's, until 20:00 ET).
|
||||||
stageInputs: { stagesSameDay: cfg.stagesSameDay === true, sameDayWithdrawals: account.stage - 1 },
|
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.
|
// Such a type pays out once per stage on each of those days.
|
||||||
maxWithdrawals: cfg.stagesSameDay === true ? 2 * (1 + cfg.withdrawalStages.length) : 2,
|
maxWithdrawals: cfg.stagesSameDay === true ? 2 * (1 + cfg.withdrawalStages.length) : 2,
|
||||||
payout: {
|
payout: {
|
||||||
|
|||||||
+10
-7
@@ -10,7 +10,7 @@ import { obfuscationDayBooked } from '@/lib/auto-trade';
|
|||||||
import type { AccountState } from '@/types';
|
import type { AccountState } from '@/types';
|
||||||
import {
|
import {
|
||||||
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, tradeDateFor,
|
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, tradeDateFor,
|
||||||
obfuscationStopDollars, dllStopDollars, botStopsActive, stageInputsFor,
|
obfuscationStopDollars, dllStopDollars, botStopsActive, stageInputsFor, stagePayouts,
|
||||||
maxLossRoom, lastStand as isLastStand, shortestNextLeg,
|
maxLossRoom, lastStand as isLastStand, shortestNextLeg,
|
||||||
LEG_COMPLETION_EPSILON,
|
LEG_COMPLETION_EPSILON,
|
||||||
} from '@/lib/trading-logic';
|
} from '@/lib/trading-logic';
|
||||||
@@ -63,11 +63,11 @@ export async function GET() {
|
|||||||
const isDead = autoLiqThreshold !== 0 && cash.amount <= autoLiqThreshold;
|
const isDead = autoLiqThreshold !== 0 && cash.amount <= autoLiqThreshold;
|
||||||
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
||||||
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
||||||
// Stage = 1 + number of withdrawals (negative fund transactions),
|
// Stage = 1 + number of payouts (negative fund transactions, or
|
||||||
// or of today's payouts for a type whose stages reset each day.
|
// the client's own list where a row holds several), or of
|
||||||
const stage = 1 + (cfg?.stagesSameDay
|
// today's for a type whose stages reset each day.
|
||||||
? (client.sameDayClaims?.(acc.id) ?? 0)
|
const stageInputs = stageInputsFor(client, acc.id, cfg ?? {});
|
||||||
: allFundTxns.filter((f) => f.amount < 0).length);
|
const stage = 1 + stagePayouts(allFundTxns, stageInputs);
|
||||||
// Stage-resolved config: a drain stage swaps in its own min-day
|
// Stage-resolved config: a drain stage swaps in its own min-day
|
||||||
// (default $5) and no profit target.
|
// (default $5) and no profit target.
|
||||||
const effective = cfg ? resolveEffectiveConfig(
|
const effective = cfg ? resolveEffectiveConfig(
|
||||||
@@ -79,7 +79,7 @@ export async function GET() {
|
|||||||
priorProfit,
|
priorProfit,
|
||||||
allFundTxns,
|
allFundTxns,
|
||||||
cfg.minDayPnL,
|
cfg.minDayPnL,
|
||||||
stageInputsFor(client, acc.id, cfg),
|
stageInputs,
|
||||||
) : null;
|
) : null;
|
||||||
// Days traded counts only days that hit minDayPnL (when set) — that's what
|
// Days traded counts only days that hit minDayPnL (when set) — that's what
|
||||||
// the firm requires toward the min trading day rule.
|
// 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 —
|
// The combined day's P&L, not just the current session's —
|
||||||
// on the Tuesday after a holiday this includes Sun/Mon.
|
// on the Tuesday after a holiday this includes Sun/Mon.
|
||||||
realizedPnL: combined.realizedPnL,
|
realizedPnL: combined.realizedPnL,
|
||||||
|
// The firm's raw trade date, which the account page
|
||||||
|
// projects the payouts from.
|
||||||
|
tradeDate,
|
||||||
daysTraded,
|
daysTraded,
|
||||||
// An account the bot puts stops on whose day booked red via
|
// An account the bot puts stops on whose day booked red via
|
||||||
// its own stop — the personal DLL. Firm DLL lockouts show
|
// its own stop — the personal DLL. Firm DLL lockouts show
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ function collectFirmStats() {
|
|||||||
fundTransactions: fundTxns,
|
fundTransactions: fundTxns,
|
||||||
balance: cash.amount,
|
balance: cash.amount,
|
||||||
realizedPnL: cash.realizedPnL,
|
realizedPnL: cash.realizedPnL,
|
||||||
|
rawTradeDate: rawToday,
|
||||||
stageInputs: stageInputsFor(client, acc.id, cfg),
|
stageInputs: stageInputsFor(client, acc.id, cfg),
|
||||||
payout: {
|
payout: {
|
||||||
withdrawalPct: cfg.withdrawalPct,
|
withdrawalPct: cfg.withdrawalPct,
|
||||||
|
|||||||
+28
-8
@@ -37,6 +37,19 @@ export interface StageInputs {
|
|||||||
dailyLossLimit?: number;
|
dailyLossLimit?: number;
|
||||||
stagesSameDay?: boolean;
|
stagesSameDay?: boolean;
|
||||||
sameDayWithdrawals?: number;
|
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
|
// 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
|
// day starts back at stage 1 — the room a payout frees lasts only
|
||||||
// until the firm's next daily reset.
|
// until the firm's next daily reset.
|
||||||
const withdrawalCount = inputs.stagesSameDay
|
const withdrawalCount = stagePayouts(fundTransactions, inputs);
|
||||||
? Math.max(0, Math.floor(inputs.sameDayWithdrawals ?? 0))
|
|
||||||
: fundTransactions.filter((f) => f.amount < 0).length;
|
|
||||||
if (withdrawalCount === 0) {
|
if (withdrawalCount === 0) {
|
||||||
return { profitTarget, consistency, minTradingDays, minDayPnL, drain: false, dailyLossLimit }; // Stage 1 = base values
|
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. */
|
/** Defaults to the current time. */
|
||||||
now?: Date;
|
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[] {
|
}): ProjectedDay[] {
|
||||||
const { accountId, cfg } = opts;
|
const { accountId, cfg } = opts;
|
||||||
const horizon = opts.horizonDays ?? 40;
|
const horizon = opts.horizonDays ?? 40;
|
||||||
@@ -490,7 +504,7 @@ export function projectPerfectPath(opts: {
|
|||||||
const dd = Math.max(0, opts.payout?.drawdown ?? 0);
|
const dd = Math.max(0, opts.payout?.drawdown ?? 0);
|
||||||
const everyDay = cfg.tradesEveryDay === true;
|
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
|
// 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
|
// 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
|
// 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;
|
const sameDay = opts.stageInputs?.stagesSameDay === true;
|
||||||
/** The path's own payouts, by the date they are claimed on. */
|
/** The path's own payouts, by the date they are claimed on. */
|
||||||
const paidOn = new Map<string, number>();
|
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
|
/** Payouts before the stage traded on `day`: that day's own for a
|
||||||
* same-day type, else every withdrawal so far. */
|
* same-day type, else every withdrawal so far. */
|
||||||
const paidBefore = (day: string): number => (sameDay
|
const paidBefore = (day: string): number => (sameDay
|
||||||
? (day === today ? Math.max(0, Math.floor(opts.stageInputs?.sameDayWithdrawals ?? 0)) : 0) + (paidOn.get(day) ?? 0)
|
? (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
|
/** 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) —
|
* (runClaims claims, then trades again on the room the claim freed) —
|
||||||
* once through its stages a day: the last one repeats, and the path
|
* 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(
|
const effectiveOn = (day: string) => resolveEffectiveConfig(
|
||||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||||
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
|
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
|
/** The payout parameters of the stage being completed: per-stage
|
||||||
* overrides when set, else the type defaults. */
|
* 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;
|
export const DLL_MIN_STOP = 50;
|
||||||
|
|
||||||
/** The stage inputs for one account: the type's daily loss limit, and for a
|
/** 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(
|
export function stageInputsFor(
|
||||||
client: { sameDayClaims?: (accountId: number) => number },
|
client: { sameDayClaims?: (accountId: number) => number; payoutDays?: (accountId: number) => string[] | undefined },
|
||||||
accountId: number,
|
accountId: number,
|
||||||
cfg: { dailyLossLimit?: number; stagesSameDay?: boolean },
|
cfg: { dailyLossLimit?: number; stagesSameDay?: boolean },
|
||||||
): StageInputs {
|
): StageInputs {
|
||||||
const sameDay = cfg.stagesSameDay === true;
|
const sameDay = cfg.stagesSameDay === true;
|
||||||
|
const payouts = !sameDay && typeof client.payoutDays === 'function' ? client.payoutDays(accountId)?.length : undefined;
|
||||||
return {
|
return {
|
||||||
dailyLossLimit: cfg.dailyLossLimit ?? 0,
|
dailyLossLimit: cfg.dailyLossLimit ?? 0,
|
||||||
stagesSameDay: sameDay,
|
stagesSameDay: sameDay,
|
||||||
sameDayWithdrawals: sameDay && typeof client.sameDayClaims === 'function' ? client.sameDayClaims(accountId) : 0,
|
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,
|
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,
|
dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget,
|
||||||
currentTradeDateCT, foldReason, withinFirmCaps, personSizeFactor, personHoldHours, PERSON_SIZE_SPREAD, machineSide,
|
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,
|
legCloseNote,
|
||||||
} from '../lib/trading-logic.ts';
|
} 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);
|
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', () => {
|
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
|
// 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
|
// 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 { botOpenedPositions, loadAccountMeta, saveAccountMeta } = await import('../lib/db.ts');
|
||||||
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts');
|
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts');
|
||||||
const { encodeContractName } = await import('../lib/dxtrade-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 { isClientReady } = await import('../lib/platform-client.ts');
|
||||||
|
|
||||||
const iso = (s) => Date.parse(s);
|
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();
|
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;
|
active: boolean;
|
||||||
amount: number;
|
amount: number;
|
||||||
realizedPnL: 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;
|
daysTraded: number;
|
||||||
positionDirection: 'long' | 'short' | null;
|
positionDirection: 'long' | 'short' | null;
|
||||||
/** Balance floor from Tradovate's auto-liquidation profile (0 = not set) */
|
/** 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
|
* consistency and minimum days all met (cycleCompletion 'today'). Only
|
||||||
* the final day; the firm may lock or remove the account afterwards. */
|
* the final day; the firm may lock or remove the account afterwards. */
|
||||||
completed: boolean;
|
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;
|
stage: number;
|
||||||
/** True when the current stage is a drain stage (spam-withdraw mode). */
|
/** True when the current stage is a drain stage (spam-withdraw mode). */
|
||||||
stageDrain: boolean;
|
stageDrain: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user