Vest: a claim names the completed cycle it ends, a day's second and one after the reset too

Two of the cycles a claim ends never got a completion entry. A Vest
day's second cycle is one: the auto-claim at the next tick beats the 30 s
report, and the payout books into that day's fund row, so the latest fund
date never moves for the catch-up. A cycle claimed right after the 20:00
ET reset is the other: the reset puts it back on stage 1, and all of its
P&L sits in the previous claim's day row, so endedCycleCompletion reads
none of it.

Before a claim goes out, claimedCycle (trading-logic) checks whether the
cycle stands complete: cycleCompletion 'today' or 'before' on the
client's numbers. It keys the cycle the way the final day would
(completionPlace on the payout list), so the master dedupes the two.
afterClaim keeps the result in account_meta. The reporter names each kept
cycle for the catch-up's week. It lists them last in the slice, newest
first, under the master's per-slice cap, and adds no counter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 16:08:17 -05:00
co-authored by Claude Opus 5.5
parent 4a8e027056
commit e02838409b
7 changed files with 348 additions and 15 deletions
+61
View File
@@ -1413,3 +1413,64 @@ test('endedCycleCompletion: a same-day type\'s ended cycle is judged on its day\
// Any other type: judged on stage 2's $3,000, as before.
assert.equal(endedCycleCompletion({ ...opts, cfg: vestFunded }), null);
});
import { claimedCycle, withinEndedCycleWindow } from '../lib/trading-logic.ts';
test('claimedCycle: a claim names the complete cycle it ends, where the final day and the catch-up can\'t', () => {
const cfg = { ...vestFunded, stagesSameDay: true, dailyLossLimit: 1500 };
const activated = { date: '2026-10-20', amount: 50000 };
/** A Vest client's numbers for account 7, just before a claim. */
const client = (o) => ({
accountCashBalances: { 7: o.cash }, dailyPnL: { 7: o.cycle ?? [] }, fundTransactions: { 7: o.rows },
lastFundDates: { 7: o.last }, priorProfit: { 7: 0 },
tradeDate: () => o.day, sameDayClaims: () => o.today, payoutDays: o.payouts && (() => o.payouts),
});
// Vest day 10-27, 6 PM ET: claim 1 took stage 1's $1,500 at 3 PM, and
// stage 2 has made its $3,000 since. The auto-claim at the next tick
// beats the 30 s report, and its payout books into 10-27's row: the
// latest fund date never moves for the catch-up.
const claimedOnce = [activated, { date: '2026-10-27', amount: -1500 }];
const second = { cash: { amount: 53000, realizedPnL: 3000 }, rows: claimedOnce, last: '2026-10-27', day: '2026-10-27', today: 1, payouts: ['2026-10-27'] };
assert.deepEqual(claimedCycle(client(second), 7, cfg, cdt('2026-10-27T17:00:00')),
{ date: '2026-10-27', stage: 2, drain: false, cycle: '2026-10-27', cycleSeq: 0 });
// A Claim now short of stage 2's $3,000 names nothing.
assert.equal(claimedCycle(client({ ...second, cash: { amount: 51000, realizedPnL: 1000 } }), 7, cfg, cdt('2026-10-27T17:00:00')), null);
// The day's third cycle, after that claim, has a key of its own.
const twice = [activated, { date: '2026-10-27', amount: -4500 }];
assert.deepEqual(claimedCycle(client({ ...second, rows: twice, today: 2, payouts: ['2026-10-27', '2026-10-27'] }), 7, cfg, cdt('2026-10-27T17:00:00')),
{ date: '2026-10-27', stage: 3, drain: false, cycle: '2026-10-27', cycleSeq: 1 });
// Stage 2 made only $1,500 of its $3,000 by the 20:00 ET reset, which
// puts the account back on stage 1's $1,500: claimed at the first tick,
// 8:00:30 PM ET.
const afterReset = { cash: { amount: 51500, realizedPnL: 0 }, rows: claimedOnce, last: '2026-10-27', day: '2026-10-28', today: 0, payouts: ['2026-10-27'] };
assert.deepEqual(claimedCycle(client(afterReset), 7, cfg, cdt('2026-10-27T19:00:30')),
{ date: '2026-10-28', stage: 2, drain: false, cycle: '2026-10-27', cycleSeq: 0 });
// The catch-up after it reads none of that cycle's P&L, all in 10-27's
// row, which the cycle before owns.
assert.equal(endedCycleCompletion({
accountId: 7, rawTradeDate: '2026-10-28', fullDailyPnL: [{ date: '2026-10-27', pnl: 3000 }], cycleDailyPnL: [],
fundTransactions: [...claimedOnce, { date: '2026-10-28', amount: -1500 }], lastFundDate: '2026-10-28',
endedCycleStart: { fundDate: '2026-10-27', includesDay: false }, rawRealizedPnL: 0, equityProfit: 0, cfg, now: cdt('2026-10-27T19:10:00'),
}), null);
// Where the catch-up does name the cycle — its P&L on a day of its own —
// the claim names it by the same key, so the master keeps one.
const crossDay = { cash: { amount: 51500, realizedPnL: 1500 }, rows: claimedBefore, last: '2026-10-25', day: '2026-10-26', today: 0, payouts: ['2026-10-25'] };
assert.deepEqual(claimedCycle(client(crossDay), 7, cfg, cdt('2026-10-26T12:00:00')),
{ date: '2026-10-26', stage: 2, drain: false, cycle: '2026-10-25', cycleSeq: 0 });
// Without a payout list, one payout per negative fund row.
assert.equal(claimedCycle(client({ ...crossDay, payouts: undefined }), 7, cfg, cdt('2026-10-26T12:00:00')).stage, 2);
// Numbers not read yet: nothing.
assert.equal(claimedCycle({ ...client(crossDay), accountCashBalances: {} }, 7, cfg, cdt('2026-10-26T12:00:00')), null);
assert.equal(claimedCycle({ ...client(crossDay), fundTransactions: {} }, 7, cfg, cdt('2026-10-26T12:00:00')), null);
});
test('withinEndedCycleWindow: a week after the date, and never for one that doesn\'t parse', () => {
assert.equal(withinEndedCycleWindow('2026-10-21', '2026-10-28'), true);
assert.equal(withinEndedCycleWindow('2026-10-28', '2026-10-28'), true);
assert.equal(withinEndedCycleWindow('2026-10-20', '2026-10-28'), false);
assert.equal(withinEndedCycleWindow('', '2026-10-28'), false);
assert.equal(withinEndedCycleWindow('10/27', '2026-10-28'), false);
});
+105 -3
View File
@@ -2,7 +2,8 @@
// pure helpers show: the market-close guard closing a leg whose open came back
// unknown, and the candidate board keeping a symbol the close gate holds back;
// a sync older than the bot's own fill, an open a restart cut off, an outside
// trade beside an open of unknown outcome, and the client's day at 8 PM ET.
// trade beside an open of unknown outcome, and the client's day at 8 PM ET;
// the completed cycles a Vest day's claims end, named at each claim.
//
// npm run check
//
@@ -51,10 +52,12 @@ VestClient.prototype.request = async function (bucket, method, path, opts = {})
return gateway(method, path, opts);
};
const { putVestSession } = await import('../lib/vest-session.ts');
const { botOpenedPositions, loadAccountMeta, saveAccountMeta } = await import('../lib/db.ts');
const {
botOpenedPositions, loadAccountMeta, saveAccountMeta, saveFundTransactions, wipeAccountData, loadClaimedCycles, saveClaimedCycles,
} = 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, stageInputsFor, resolveEffectiveConfig } = await import('../lib/trading-logic.ts');
const { tradeDateFor, stageInputsFor, resolveEffectiveConfig, claimedCycle, endedCycleCompletion } = await import('../lib/trading-logic.ts');
const { isClientReady } = await import('../lib/platform-client.ts');
const iso = (s) => Date.parse(s);
@@ -417,3 +420,102 @@ test('two payouts on one Vest day share its fund row, and still count two toward
client.disconnect();
}
});
test('each claim of a Vest day names the completed cycle it ends, the second and the one after the reset too', async () => {
// acc-1, funded $50k, Vest's own numbers: its balance, and its reset balance since 20:00 ET.
let balance = 51500;
let reset = { at: '2026-10-27T00:00:00Z', balance: 50000 };
clockAt('2026-10-27T19:00:00Z'); // 3 PM ET, Vest day 10-27
wipeAccountData(A);
const client = await syncingClient((method, path, opts) => {
const read = syncReads(method, path, []);
if (read && path === '/v3/capital/accounts/active') {
read.data.accounts[0] = { ...read.data.accounts[0], daily_reset_at: reset.at, daily_reset_balance: String(reset.balance) };
}
if (read && path === '/v3/accounts') read.data.accounts[0] = { ...read.data.accounts[0], amount: String(balance) };
if (read) return read;
if (method === 'POST' && path === '/v3/capital/withdraw' && opts.body.account_id === 'acc-1') {
balance = Math.round((balance - Number(opts.body.amount)) * 100) / 100;
return { data: { trader_amount: opts.body.amount } };
}
return unanswered(method, path);
});
// The history read a claim starts isn't this test's: what it books is set here.
client.fetchDaysTraded = async () => ({ failedAccounts: 0 });
const cfg = {
profitTarget: 1500, consistency: 0, minDayPnL: 0, minTradingDays: 0, accountSize: 50000, targetSameEquity: false,
splitEnabled: true, payoutable: true, tradesEveryDay: true, combineCmeHolidays: false, stagesSameDay: true, dailyLossLimit: 1500,
withdrawalStages: [{ profit: 3000, consistency: 0, minTradingDays: 0, dailyLossLimit: 3000 }],
};
/** The day's trading so far, as the history read books it, and Vest's WITHDRAWs for the claims made. */
const history = (dayPnl) => {
client.fullDailyPnL[A] = [{ date: '2026-10-27', pnl: dayPnl }];
client.applyCycleSlice(A);
client.withdrawals.set(A, client.claimsOf(A).map((c) => ({ at: c.at, amount: -c.amount })));
};
const named = () => claimedCycle(client, A, cfg, new Date(Date.now()));
try {
saveFundTransactions(A, 'V50K-F-D3-1', [{ date: '2026-10-20', amount: 50000 }]);
saveAccountMeta(A, 'last_fund_date', '2026-10-20');
client.fundTransactions[A] = [{ date: '2026-10-20', amount: 50000 }];
client.lastFundDates[A] = '2026-10-20';
history(1500);
await client.requestSync();
// Stage 1's $1,500, claimed: the account's first cycle.
assert.deepEqual(named(), { date: '2026-10-27', stage: 1, drain: false, cycle: '2026-10-20', cycleSeq: 0 });
await client.claimProfit(A, 1500);
await client.syncRunning; // the sync the claim starts
// 6 PM ET: stage 2's $3,000 made, and claimed the same Vest day — its
// payout books into 10-27's row, and the latest fund date stays.
clockAt('2026-10-27T22:00:00Z');
balance = 53000;
history(4500);
await client.requestSync();
assert.deepEqual(named(), { date: '2026-10-27', stage: 2, drain: false, cycle: '2026-10-27', cycleSeq: 0 });
await client.claimProfit(A, 3000);
await client.syncRunning;
assert.equal(client.lastFundDates[A], '2026-10-27');
assert.deepEqual(client.fundTransactions[A].at(-1), { date: '2026-10-27', amount: -4500 });
// 6:30 PM ET: the day's third cycle, $1,500 of stage 2's $3,000 — not complete.
clockAt('2026-10-27T22:30:00Z');
balance = 51500;
history(6000);
await client.requestSync();
assert.equal(named(), null);
// 8:00:30 PM ET: the reset puts it back on stage 1's $1,500, met —
// claimed at the first tick.
clockAt('2026-10-28T00:00:30Z');
reset = { at: '2026-10-28T00:00:00Z', balance: 51500 };
await client.requestSync();
assert.equal(tradeDateFor(client, new Date(Date.now())), '2026-10-28');
assert.deepEqual(named(), { date: '2026-10-28', stage: 3, drain: false, cycle: '2026-10-27', cycleSeq: 1 });
await client.claimProfit(A, 1500);
await client.syncRunning;
assert.equal(client.lastFundDates[A], '2026-10-28');
// The catch-up names none of it: the cycle's P&L sits in 10-27's row,
// which the cycle before owns.
clockAt('2026-10-28T00:10:00Z');
await client.requestSync();
assert.equal(endedCycleCompletion({
accountId: A, rawTradeDate: tradeDateFor(client, new Date(Date.now())), client,
fullDailyPnL: client.fullDailyPnL[A], cycleDailyPnL: client.dailyPnL[A], fundTransactions: client.fundTransactions[A],
lastFundDate: client.lastFundDates[A], endedCycleStart: JSON.parse(loadAccountMeta(A, 'ended_cycle_start')),
rawRealizedPnL: client.accountCashBalances[A].realizedPnL, equityProfit: client.accountCashBalances[A].amount - 50000, cfg,
now: new Date(Date.now()),
}), null);
// Kept as the claims leave them; an entry that doesn't read is left out.
const kept = [{ date: '2026-10-27', stage: 2, drain: false, cycle: '2026-10-27', cycleSeq: 0 }];
saveClaimedCycles(A, [...kept, { date: '2026-10-28', stage: '3', drain: false, cycle: '2026-10-27', cycleSeq: 1 }]);
assert.deepEqual(loadClaimedCycles(A), kept);
saveAccountMeta(A, 'claimed_cycles', '{');
assert.deepEqual(loadClaimedCycles(A), []);
} finally {
client.disconnect();
}
});