Vest: a claim counts on the fresh day only once Vest dates it there, and a day's second cycle gets its own key
A claim's stage time is now taken before the request goes out (sentAt): Vest dates it no earlier, so a claim answered just after 20:00 ET no longer puts the fresh Vest day on stage 2 before the history read shows its WITHDRAW. One Vest dates after the turn reads as the day before until then, the cautious way round. Claims saved without it count from their answer. Completions count one per payout on both paths (completionPlace): the catch-up's stage no longer counts a Vest day's two claims as one, and a cycle a day's second claim started carries cycleSeq, so it can be told from that day's first. The master ignores it until it keys on it; old masters and old instances are unchanged. bookHistory's claim bookkeeping (WITHDRAWs, lapse, fund rows, the balance the books check expects) is now the pure readClaims, tested on the case that held an account for good: a claim that never went in, read an hour on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9fe7fc0ea3
commit
35512cd504
@@ -715,6 +715,39 @@ test('an every-day firm can finish its cycle on a Saturday', () => {
|
||||
assert.equal(cycleCompletion(opts), 'none');
|
||||
});
|
||||
|
||||
import { completionPlace } from '../lib/trading-logic.ts';
|
||||
|
||||
test('completionPlace: each payout counts, and a day\'s second cycle has a key of its own', () => {
|
||||
// Vest: activated on the 20th; claims c1 and c2 on the 27th share one
|
||||
// fund row; c3 on the 28th.
|
||||
const activated = { date: '2026-10-20', amount: 50000 };
|
||||
// Cycle A, the first, completes on the 27th ahead of any claim.
|
||||
assert.deepEqual(completionPlace({ payoutDays: [], fundTransactions: [activated], cycle: '2026-10-20' }), { stage: 1, cycleSeq: 0 });
|
||||
// c1 starts cycle B, which completes the same day.
|
||||
const b = completionPlace({ payoutDays: ['2026-10-27'], fundTransactions: [activated, { date: '2026-10-27', amount: -2250 }], cycle: '2026-10-27' });
|
||||
assert.deepEqual(b, { stage: 2, cycleSeq: 0 });
|
||||
// c2 starts cycle C on the same fund row, which completes on the 28th:
|
||||
// payout 3, and not B's key — the master would fold C into B.
|
||||
const both = [activated, { date: '2026-10-27', amount: -3750 }];
|
||||
const c = completionPlace({ payoutDays: ['2026-10-27', '2026-10-27'], fundTransactions: both, cycle: '2026-10-27' });
|
||||
assert.deepEqual(c, { stage: 3, cycleSeq: 1 });
|
||||
// C caught up after c3 ended it: the same stage and key as its final day.
|
||||
assert.deepEqual(completionPlace({
|
||||
payoutDays: ['2026-10-27', '2026-10-27', '2026-10-28'], fundTransactions: [...both, { date: '2026-10-28', amount: -1500 }],
|
||||
cycle: '2026-10-27', end: '2026-10-28',
|
||||
}), c);
|
||||
// One fund row per payout (every other platform): 1 + the rows before, no
|
||||
// cycleSeq — as the stage always read.
|
||||
const rows = [{ date: '2026-09-01', amount: -500 }, { date: '2026-09-15', amount: -800 }];
|
||||
const days = rows.map((f) => f.date);
|
||||
assert.deepEqual(completionPlace({ payoutDays: days, fundTransactions: rows, cycle: '2026-09-15' }), { stage: 3, cycleSeq: 0 });
|
||||
assert.deepEqual(completionPlace({ payoutDays: days, fundTransactions: rows, cycle: '2026-09-01', end: '2026-09-15' }), { stage: 2, cycleSeq: 0 });
|
||||
// The account's first cycle; and a claim on the day it was activated,
|
||||
// whose deposit row names the cycle it started.
|
||||
assert.deepEqual(completionPlace({ payoutDays: [], fundTransactions: [], cycle: '' }), { stage: 1, cycleSeq: 0 });
|
||||
assert.deepEqual(completionPlace({ payoutDays: ['2026-10-20'], fundTransactions: [{ date: '2026-10-20', amount: 47750 }], cycle: '2026-10-20' }), { stage: 2, cycleSeq: 1 });
|
||||
});
|
||||
|
||||
// ── DXTrade helpers (lib/dxtrade-helpers.ts) ─────────────────────────────────
|
||||
|
||||
import {
|
||||
|
||||
+52
-12
@@ -13,7 +13,7 @@ import {
|
||||
vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN,
|
||||
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
|
||||
accountScopedToken, VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, claimWithdrawal, settleClaims,
|
||||
claimFundRows, payoutCount, payoutsSince,
|
||||
claimFundRows, readClaims, payoutDaysOf, payoutsSince,
|
||||
} from '../lib/vest-helpers.ts';
|
||||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
|
||||
|
||||
@@ -192,6 +192,39 @@ test('a claim of unknown outcome that never shows lapses an hour on; one Vest ac
|
||||
assert.deepEqual(settleClaims([accepted], [], at + 48 * 3_600_000), { pending: [accepted], lapsed: [] });
|
||||
});
|
||||
|
||||
test('the books stop counting a claim that never went in once it lapses, so it holds the account no longer', () => {
|
||||
const at = iso('2026-10-27T15:00:00Z');
|
||||
const lost = { at, sentAt: at - 20_000, amount: 2250, base: 50150, unknown: true };
|
||||
const txns = [deposit, { action_id: 'r1', amount: '2400', created_at: at - 3_600_000, transaction_type: 9 }].map(parseTxn);
|
||||
const read = (claims, readAt) => readClaims({ txns, funds: bookTxns(txns).funds, claims, stored: [], readAt, opening: 0 });
|
||||
// It never went in: the balance is what the transactions add up to.
|
||||
const cash = 52400;
|
||||
// Half an hour on it may yet show, so the books still take it out (and hold).
|
||||
const early = read([lost], at + 30 * 60_000);
|
||||
assert.deepEqual([early.lapsed, early.outstanding], [[], [lost]]);
|
||||
assert.equal(cash - early.expected, 2250);
|
||||
// A read an hour on lapses it, and the books agree again.
|
||||
const late = read([lost], at + 61 * 60_000);
|
||||
assert.deepEqual([late.lapsed, late.outstanding], [[lost], []]);
|
||||
assert.equal(late.expected, cash);
|
||||
assert.deepEqual(late.rows, [{ date: '2026-10-20', amount: 50000 }]);
|
||||
// One Vest accepted that doesn't show yet: out of the balance and on its
|
||||
// day's row, however long it takes.
|
||||
const accepted = { at, sentAt: at - 20_000, amount: 2250, base: 50150 };
|
||||
const waiting = read([accepted], at + 48 * 3_600_000);
|
||||
assert.deepEqual([waiting.lapsed, waiting.outstanding], [[], [accepted]]);
|
||||
assert.equal(waiting.expected, cash - 2250);
|
||||
assert.deepEqual(waiting.rows, [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -2250 }]);
|
||||
// Once it shows, the WITHDRAW itself is in the sum.
|
||||
const shown = [...txns, parseTxn({ action_id: 'w1', amount: '2250', created_at: at + 2_000, transaction_type: 2 })];
|
||||
const settled = readClaims({ txns: shown, funds: bookTxns(shown).funds, claims: [accepted], stored: [], readAt: at + 3_600_000, opening: 0 });
|
||||
assert.deepEqual(settled.withdrawals, [{ at: at + 2_000, amount: -2250 }]);
|
||||
assert.deepEqual(settled.outstanding, []);
|
||||
assert.equal(settled.expected, cash - 2250);
|
||||
// An account Vest funded without a deposit row: its capital counts in.
|
||||
assert.equal(readClaims({ txns: txns.slice(1), funds: new Map(), claims: [], stored: [], readAt: at, opening: 50000 }).expected, cash);
|
||||
});
|
||||
|
||||
test('an account token that can\'t be minted means the order or claim was never sent', async () => {
|
||||
const path = '/v3/capital/withdraw';
|
||||
assert.equal(await tokenForRequest(path, async () => 'tok'), 'tok');
|
||||
@@ -216,9 +249,9 @@ test('an account token that can\'t be minted means the order or claim was never
|
||||
});
|
||||
|
||||
test('a claim Vest dates on the other side of 20:00 ET keeps no fund row of its own', () => {
|
||||
// Sent at 19:59:59.9 ET on the 26th and answered after 20:00: Vest dates
|
||||
// Sent at 19:59:59.8 ET on the 26th and answered after 20:00: Vest dates
|
||||
// its WITHDRAW on the 26th's day, the client booked it on the 27th's.
|
||||
const claim = { at: iso('2026-10-27T00:00:00.300Z'), amount: 2250, base: 50000 };
|
||||
const claim = { at: iso('2026-10-27T00:00:00.300Z'), sentAt: iso('2026-10-26T23:59:59.800Z'), amount: 2250, base: 50000 };
|
||||
const txns = [deposit, { action_id: 'w1', amount: '2250', created_at: iso('2026-10-26T23:59:59.900Z'), transaction_type: 2 }].map(parseTxn);
|
||||
const stored = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -2250 }];
|
||||
// Before Vest shows it, it stands where it was booked.
|
||||
@@ -229,10 +262,16 @@ test('a claim Vest dates on the other side of 20:00 ET keeps no fund row of its
|
||||
assert.deepEqual(claimFundRows({ funds: bookTxns(txns).funds, claims: [claim], withdrawals: withdrawalsOf(txns), stored }), {
|
||||
rows: [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-26', amount: -2250 }], dropped: ['2026-10-27'],
|
||||
});
|
||||
// The 27th starts with no payout on it — once Vest says so.
|
||||
// The 27th starts with no payout on it, before Vest shows the claim too:
|
||||
// it went out on the 26th, and Vest can't date it earlier.
|
||||
const reset = lastVestResetMs(iso('2026-10-27T15:00:00Z'));
|
||||
assert.equal(payoutsSince(withdrawalsOf(txns), [claim], reset), 0);
|
||||
assert.equal(payoutsSince([], [claim], reset), 1);
|
||||
assert.equal(payoutsSince([], [claim], reset), 0);
|
||||
// Dated after the turn instead, it's the 27th's once Vest shows it.
|
||||
const after = [deposit, { action_id: 'w3', amount: '2250', created_at: iso('2026-10-27T00:00:00.100Z'), transaction_type: 2 }].map(parseTxn);
|
||||
assert.equal(payoutsSince(withdrawalsOf(after), [claim], reset), 1);
|
||||
// One saved before the send time was kept counts from its answer.
|
||||
assert.equal(payoutsSince([], [{ at: claim.at, amount: claim.amount, base: claim.base }], reset), 1);
|
||||
|
||||
// Dated the same day: its row stays. A day Vest has other fund rows on
|
||||
// is Vest's, with a claim it doesn't show yet added.
|
||||
@@ -256,16 +295,17 @@ test('two claims on one Vest day share a fund row but count as two payouts', ()
|
||||
const withdrawals = withdrawalsOf(txns);
|
||||
const { rows } = claimFundRows({ funds: bookTxns(txns).funds, claims: [first, second], withdrawals, stored: [] });
|
||||
assert.deepEqual(rows.filter((f) => f.amount < 0), [{ date: '2026-10-27', amount: -3750 }]);
|
||||
assert.equal(payoutCount(withdrawals, [first, second], rows), 2);
|
||||
// The second not shown yet: still two.
|
||||
assert.equal(payoutCount(withdrawals.slice(0, 1), [first, second], rows), 2);
|
||||
assert.deepEqual(payoutDaysOf(withdrawals, [first, second], rows), ['2026-10-27', '2026-10-27']);
|
||||
// The second not shown yet: still two, on the day it was booked.
|
||||
assert.deepEqual(payoutDaysOf(withdrawals.slice(0, 1), [first, second], rows), ['2026-10-27', '2026-10-27']);
|
||||
// Made by hand, they show only as WITHDRAWs.
|
||||
assert.equal(payoutCount(withdrawals, [], rows), 2);
|
||||
assert.equal(payoutDaysOf(withdrawals, [], rows).length, 2);
|
||||
// Of unknown outcome, one counts only once it shows.
|
||||
assert.equal(payoutCount([], [{ ...first, unknown: true }], []), 0);
|
||||
assert.equal(payoutCount(withdrawals.slice(0, 1), [{ ...first, unknown: true }], rows), 1);
|
||||
assert.deepEqual(payoutDaysOf([], [{ ...first, unknown: true }], []), []);
|
||||
assert.equal(payoutDaysOf(withdrawals.slice(0, 1), [{ ...first, unknown: true }], rows).length, 1);
|
||||
// A day that nets negative with no WITHDRAW still counts, as it did.
|
||||
assert.equal(payoutCount([], [], [{ date: '2026-10-21', amount: -75 }, { date: '2026-10-22', amount: 20 }]), 1);
|
||||
assert.deepEqual(payoutDaysOf([], [], [{ date: '2026-10-21', amount: -75 }, { date: '2026-10-22', amount: 20 }]), ['2026-10-21']);
|
||||
assert.deepEqual(payoutDaysOf(withdrawals, [], [...rows, { date: '2026-10-21', amount: -75 }]), ['2026-10-21', '2026-10-27', '2026-10-27']);
|
||||
assert.equal(payoutsSince(withdrawals, [first, second], lastVestResetMs(second.at)), 2);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user