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
+40
-29
@@ -53,8 +53,8 @@ import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession }
|
||||
import {
|
||||
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
|
||||
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal, resetSafeStop,
|
||||
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_TXN, VEST_STOP_SLIPPAGE_FRACTION,
|
||||
VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, settleClaims, claimFundRows, payoutCount, payoutsSince,
|
||||
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_STOP_SLIPPAGE_FRACTION,
|
||||
VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, settleClaims, readClaims, payoutDaysOf, payoutsSince,
|
||||
} from './vest-helpers';
|
||||
import type { VestCapitalAccount, VestPosition, VestOpenOrder, VestTxn, VestTrade, OwnClaim } from './vest-helpers';
|
||||
|
||||
@@ -747,7 +747,11 @@ export class VestClient implements PlatformClient {
|
||||
const raw = JSON.parse(loadAccountMeta(accountId, CLAIMS_META) ?? '[]');
|
||||
list = Array.isArray(raw)
|
||||
? raw.filter((c) => Number.isFinite(c?.at) && Number.isFinite(c?.amount) && Number.isFinite(c?.base))
|
||||
.map((c): OwnClaim => ({ at: c.at, amount: c.amount, base: c.base, ...(c.unknown === true ? { unknown: true as const } : {}) }))
|
||||
.map((c): OwnClaim => ({
|
||||
at: c.at, amount: c.amount, base: c.base,
|
||||
...(Number.isFinite(c.sentAt) ? { sentAt: c.sentAt } : {}),
|
||||
...(c.unknown === true ? { unknown: true as const } : {}),
|
||||
}))
|
||||
: [];
|
||||
} catch {
|
||||
list = [];
|
||||
@@ -773,16 +777,19 @@ export class VestClient implements PlatformClient {
|
||||
* show yet. One of unknown outcome still not shown an hour on never went
|
||||
* in: it's dropped here (settleClaims). */
|
||||
private outstandingClaims(accountId: number, readAt: number): OwnClaim[] {
|
||||
const list = this.claimsOf(accountId);
|
||||
const { pending, lapsed } = settleClaims(list, this.withdrawals.get(accountId) ?? [], readAt);
|
||||
if (lapsed.length > 0) {
|
||||
const name = this.accountList.find((a) => a.id === accountId)?.name ?? accountId;
|
||||
this.saveClaims(accountId, list.filter((c) => !lapsed.includes(c)));
|
||||
for (const c of lapsed) console.warn(`${this.tag} ${name}: the claim of $${c.amount} with an unknown outcome never showed on Vest — taken as not made`);
|
||||
}
|
||||
const { pending, lapsed } = settleClaims(this.claimsOf(accountId), this.withdrawals.get(accountId) ?? [], readAt);
|
||||
this.forgetLapsed(accountId, lapsed);
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** Drop claims of unknown outcome that never went in (settleClaims). */
|
||||
private forgetLapsed(accountId: number, lapsed: OwnClaim[]): void {
|
||||
if (lapsed.length === 0) return;
|
||||
const name = this.accountList.find((a) => a.id === accountId)?.name ?? accountId;
|
||||
this.saveClaims(accountId, this.claimsOf(accountId).filter((c) => !lapsed.includes(c)));
|
||||
for (const c of lapsed) console.warn(`${this.tag} ${name}: the claim of $${c.amount} with an unknown outcome never showed on Vest — taken as not made`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Today's realized P&L as the day plan wants it: since the latest claim
|
||||
* this client made since the last reset — the plan's new cycle starts
|
||||
@@ -808,15 +815,15 @@ export class VestClient implements PlatformClient {
|
||||
return payoutsSince(this.withdrawals.get(accountId) ?? [], this.claimsOf(accountId), lastVestResetMs(Date.now()));
|
||||
}
|
||||
|
||||
/** See PlatformClient: one per payout — Vest's WITHDRAW transactions, and
|
||||
* this client's claims they don't show yet (payoutCount) — where a fund
|
||||
* row holds a whole Vest day's. Undefined until the account's history
|
||||
* is read. */
|
||||
public withdrawalCount(accountId: number): number | undefined {
|
||||
/** See PlatformClient: one per payout, on its Vest day — Vest's WITHDRAW
|
||||
* transactions, and this client's claims they don't show yet
|
||||
* (payoutDaysOf) — where a fund row holds a whole Vest day's. Undefined
|
||||
* until the account's history is read. */
|
||||
public payoutDays(accountId: number): string[] | undefined {
|
||||
const withdrawals = this.withdrawals.get(accountId);
|
||||
const fundRows = this.fundTransactions[accountId];
|
||||
if (!withdrawals || !fundRows || !this.historyLoadedIds.has(accountId)) return undefined;
|
||||
return payoutCount(withdrawals, this.claimsOf(accountId), fundRows);
|
||||
return payoutDaysOf(withdrawals, this.claimsOf(accountId), fundRows);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -851,6 +858,10 @@ export class VestClient implements PlatformClient {
|
||||
throw new Error(`$${claimed} is more than the $${round2(cash.amount - meta.initialCapital)} above Vest's starting capital of $${meta.initialCapital}`);
|
||||
}
|
||||
|
||||
// Taken before it goes out, which Vest dates it no earlier than: a
|
||||
// claim answered after 20:00 ET isn't the fresh day's payout until
|
||||
// Vest says so (payoutsSince).
|
||||
const sentAt = Date.now();
|
||||
let data: { trader_amount?: unknown } | undefined;
|
||||
try {
|
||||
({ data } = await this.request<{ trader_amount?: unknown }>('trade', 'POST', '/v3/capital/withdraw', {
|
||||
@@ -868,7 +879,7 @@ export class VestClient implements PlatformClient {
|
||||
// every further claim on the account until Vest's transactions
|
||||
// say either way (claimPending).
|
||||
if (!nothingWentIn(err)) {
|
||||
this.saveClaims(accountId, [...this.claimsOf(accountId), { at: Date.now(), amount: claimed, base: round2(cash.amount - claimed), unknown: true }]);
|
||||
this.saveClaims(accountId, [...this.claimsOf(accountId), { at: Date.now(), sentAt, amount: claimed, base: round2(cash.amount - claimed), unknown: true }]);
|
||||
this.claimSeq.set(accountId, this.syncGeneration);
|
||||
console.error(`${this.tag} ${account.name}: claim of $${claimed} — outcome unknown (${errorMessage(err)}); no further claim on it until Vest's transactions settle it`);
|
||||
void this.fetchDaysTraded([accountId]).catch(() => { /* the hourly read retries */ });
|
||||
@@ -877,7 +888,7 @@ export class VestClient implements PlatformClient {
|
||||
}
|
||||
const received = num(data?.trader_amount) ?? null;
|
||||
const now = Date.now();
|
||||
this.saveClaims(accountId, [...this.claimsOf(accountId), { at: now, amount: claimed, base: round2(cash.amount - claimed) }]);
|
||||
this.saveClaims(accountId, [...this.claimsOf(accountId), { at: now, sentAt, amount: claimed, base: round2(cash.amount - claimed) }]);
|
||||
this.claimSeq.set(accountId, this.syncGeneration);
|
||||
|
||||
// Book it now: today's fund row (with whatever else that day holds),
|
||||
@@ -1328,23 +1339,23 @@ export class VestClient implements PlatformClient {
|
||||
.sort((x, y) => x.date.localeCompare(y.date));
|
||||
saveDailyPnL(account.id, account.name, fresh);
|
||||
|
||||
const withdrawals = txns.filter((t) => t.type === VEST_TXN.WITHDRAW).map((t) => ({ at: t.time, amount: t.amount }));
|
||||
this.withdrawals.set(account.id, withdrawals);
|
||||
const readAt = Date.now();
|
||||
const read = readClaims({
|
||||
txns, funds, claims: this.claimsOf(account.id), stored: loadFundTransactions(account.id), readAt,
|
||||
opening: hasDeposit ? 0 : a?.initialCapital ?? 0,
|
||||
});
|
||||
this.withdrawals.set(account.id, read.withdrawals);
|
||||
this.historyReadAt.set(account.id, readAt);
|
||||
// Settled here, not only in claimPending: its callers wait on
|
||||
// historyReady, which a claim that never went in would hold below
|
||||
// for good.
|
||||
const outstanding = this.outstandingClaims(account.id, readAt);
|
||||
this.forgetLapsed(account.id, read.lapsed);
|
||||
|
||||
// A claim Vest dates on the other side of 20:00 ET than it was booked
|
||||
// here leaves that day's row to this client alone: taken back, so the
|
||||
// payout counts once, on Vest's day.
|
||||
const { rows: fundRows, dropped } = claimFundRows({
|
||||
funds, claims: this.claimsOf(account.id), withdrawals, stored: loadFundTransactions(account.id),
|
||||
});
|
||||
if (dropped.length > 0) this.dropClaimDays(account, dropped);
|
||||
this.applyFundRows(account, fundRows);
|
||||
if (read.dropped.length > 0) this.dropClaimDays(account, read.dropped);
|
||||
this.applyFundRows(account, read.rows);
|
||||
|
||||
// Books check: the balance must be what the transactions add up to
|
||||
// (the opening capital included). A payout or credit booked under a
|
||||
@@ -1352,9 +1363,9 @@ export class VestClient implements PlatformClient {
|
||||
// cycle as if nothing happened.
|
||||
const cash = this.accountCashBalances[account.id];
|
||||
// A claim this client just made may not show in Vest's transactions
|
||||
// yet: count it until its WITHDRAW row does (same amount, near time).
|
||||
const unbooked = outstanding.reduce((s, c) => s + c.amount, 0);
|
||||
const expected = txns.reduce((s, t) => s + (t.kind === 'margin' ? 0 : t.amount), 0) + (hasDeposit ? 0 : a?.initialCapital ?? 0) - unbooked;
|
||||
// yet: readClaims counts it until its WITHDRAW row does (same amount,
|
||||
// near time).
|
||||
const expected = read.expected;
|
||||
if (account.active && cash && !this.positions[account.id] && Math.abs(cash.amount - expected) >= BOOKS_TOLERANCE) {
|
||||
this.booksHeld.add(account.id);
|
||||
this.lastFetchErrors[account.name] = `balance ${round2(cash.amount)} vs transactions ${round2(expected)} (types ${types.join(',')}) — held until they agree`;
|
||||
|
||||
Reference in New Issue
Block a user