From 8c863bdc1d1217b75b8e79cf28efe8120e39400d Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Fri, 30 Oct 2026 10:07:21 -0500 Subject: [PATCH] Vest: an account's name ends in the number the Vest app shows Vest's app shows attempt_index + 1 ("Account 73"), padded below 10 ("Account 01"); the name ended in attempt_index, one lower, so the dashboard's V50K-E-D3-T10-73 was Vest's Account 74. It now ends in the app's number, and 0 for an account Vest gives no attempt index (its "Legacy Account"). Account types match on the prefix, and everything saved locally is keyed by account id, so nothing else moves. Reports mark a Vest firm vestAppNumbers. The master (634d52f) renames what it saved under the old numbers on the first such report from a person, and from then on leaves out a Vest firm reported under the old ones. Co-Authored-By: Claude Opus 5.5 --- lib/master-protocol.ts | 5 +++++ lib/platform-client.ts | 6 ++++++ lib/reporter.ts | 5 +++-- lib/vest-helpers.ts | 30 ++++++++++++++++++++++-------- scripts/check-vest-client.mjs | 2 +- scripts/check-vest.mjs | 29 ++++++++++++++++++++++------- 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/lib/master-protocol.ts b/lib/master-protocol.ts index 8b92192..fbf8adf 100644 --- a/lib/master-protocol.ts +++ b/lib/master-protocol.ts @@ -159,6 +159,11 @@ export interface FirmStats { /** ISO time the firm's last good sync finished (statusVersion builds), sent * once synced. The statuses count only while it is recent. Optional. */ syncedAt?: string; + /** 1 on a Vest firm: its account names end in the number the Vest app + * shows (Account 73 = V50K-E-D3-T10-73). Older builds' were one lower; on + * the first report with this, the master renames what it saved under + * those. */ + vestAppNumbers?: 1; /** Per account type, summing to the firm's own totals. Optional so a master * on an older build simply ignores it. */ accountTypes?: AccountTypeStats[]; diff --git a/lib/platform-client.ts b/lib/platform-client.ts index 9e7f227..7aeeb2e 100644 --- a/lib/platform-client.ts +++ b/lib/platform-client.ts @@ -443,6 +443,12 @@ export function keptRuleIdsField(keptRuleIds: string[] | null): Pick { + return platform === 'vest' ? { vestAppNumbers: 1 } : {}; +} + /** * FirmStats for a firm with no accounts to walk — no client yet, or an empty * broker list. accounts is [] only once synced: before that, a firm whose diff --git a/lib/reporter.ts b/lib/reporter.ts index fcae57e..f65012e 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -28,7 +28,7 @@ import type { AccountTypeStats, FirmStats, InstrumentStats, SymbolStats } from ' import { accountNextLeg, accountLegShape } from './leg-estimate'; import { sizingHoldHours } from './person-size'; import { instrumentStatsFor, isFuturesFirm } from './instruments'; -import { accountStatusFields, brokerFacts, emptyFirmStats, firmStatusFields, historyFresh, keptRuleIdsField } from './platform-client'; +import { accountStatusFields, brokerFacts, emptyFirmStats, firmStatusFields, historyFresh, keptRuleIdsField, vestAppNumbersField } from './platform-client'; function collectFirmStats() { // Only firms this machine has credentials for. A firm it cannot log into @@ -48,7 +48,7 @@ function collectFirmStats() { // Absent (null) while this machine's catalog can't vouch for them. const keptRuleIds = getKeptRuleIdsForFirm(f.id); // accounts: [] only once synced (see emptyFirmStats). - if (!client || client.accountList.length === 0) return emptyFirmStats(f, client, keptRuleIds); + if (!client || client.accountList.length === 0) return { ...emptyFirmStats(f, client, keptRuleIds), ...vestAppNumbersField(f.platform) }; // One raw trade date per firm, the one its accounts trade on // (tradeDateFor — Vest's turns at 20:00 ET, not 5 PM CT): the // combined views below roll it forward, the completion check needs @@ -491,6 +491,7 @@ function collectFirmStats() { mix: tradeMixFor(f, client), ...firmStatusFields(f.id, client.syncComplete, client.lastSyncAt), ...keptRuleIdsField(keptRuleIds), + ...vestAppNumbersField(f.platform), }; }); } diff --git a/lib/vest-helpers.ts b/lib/vest-helpers.ts index 9d23bdf..d6cf11c 100644 --- a/lib/vest-helpers.ts +++ b/lib/vest-helpers.ts @@ -223,6 +223,7 @@ export interface VestCapitalAccount { /** The current stage: 'eval' or 'funded'. */ kind: string; status: number; + /** Vest's attempt_index, from 0; -1 when Vest leaves it out. */ attemptIndex: number; initialCapital: number; /** The current stage's profit target in dollars (0 = none). */ @@ -258,7 +259,7 @@ export function parseCapitalAccount(raw: unknown): VestCapitalAccount | null { blocked: Boolean(a.blocked_at), kind: typeof current.kind === 'string' ? current.kind : '', status: num(a.status) ?? -1, - attemptIndex: num(a.attempt_index) ?? 0, + attemptIndex: num(a.attempt_index) ?? -1, initialCapital: num(a.initial_capital) ?? 0, profitTarget: num(current.profit_target) ?? 0, targetEquity: num(a.target_equity), @@ -312,23 +313,36 @@ export function awaitingActivation(a: VestCapitalAccount): boolean { const pct = (fraction: number) => Math.round(fraction * 1000) / 10; +/** + * The number the Vest app shows for the account: attempt_index + 1, padded + * below 10 ("Account 01"). An account without an attempt index (the app's + * "Legacy Account") gets 0. + */ +export function vestAccountNumber(a: VestCapitalAccount): string { + if (!(a.attemptIndex >= 0)) return '0'; + return `${a.attemptIndex < 9 ? '0' : ''}${a.attemptIndex + 1}`; +} + /** * What the dashboard calls the account and what account-type prefixes match * against. Vest itself only says "Account 34", so the name carries the rules - * that tell its types apart: - * V50K-E-D3-T10-34 evaluation, $50k, 3% daily loss, 10% target, attempt 34 + * that tell its types apart, ending in that same number: + * V50K-E-D3-T10-34 evaluation, $50k, 3% daily loss, 10% target, Account 34 * V50K-F-D3-34 the funded stage of an evaluation account - * V25K-I-7 instant funded (no daily loss limit, no target) + * V25K-I-07 instant funded (no daily loss limit, no target) * D0 = no daily loss limit. The target is the current stage's (a 2-Step's - * second stage reads T5). + * second stage reads T5). Older builds ended the name in attempt_index, one + * lower; the master renames what it saved under those numbers when a report + * marks the firm vestAppNumbers. */ export function vestAccountName(a: VestCapitalAccount): string { const size = a.initialCapital >= 1000 ? `${+(a.initialCapital / 1000).toFixed(1)}K` : `${Math.round(a.initialCapital)}`; const dll = `D${pct(a.maxDailyLossPct)}`; - if (/instant/i.test(a.productType)) return `V${size}-I-${a.attemptIndex}`; - if (a.kind === 'funded') return `V${size}-F-${dll}-${a.attemptIndex}`; + const n = vestAccountNumber(a); + if (/instant/i.test(a.productType)) return `V${size}-I-${n}`; + if (a.kind === 'funded') return `V${size}-F-${dll}-${n}`; const target = a.initialCapital > 0 && a.profitTarget > 0 ? `-T${pct(a.profitTarget / a.initialCapital)}` : ''; - return `V${size}-E-${dll}${target}-${a.attemptIndex}`; + return `V${size}-E-${dll}${target}-${n}`; } // ── positions and orders ──────────────────────────────────────────────────── diff --git a/scripts/check-vest-client.mjs b/scripts/check-vest-client.mjs index 78de1b4..1535096 100644 --- a/scripts/check-vest-client.mjs +++ b/scripts/check-vest-client.mjs @@ -462,7 +462,7 @@ test("a passed evaluation doesn't hold the sync, and its funded account is asked await client.requestSync(); assert.deepEqual(client.activatable(), []); answer({ data: { account: funded } }); - assert.deepEqual(await asking, { funded: 'V50K-F-D3-81' }); + assert.deepEqual(await asking, { funded: 'V50K-F-D3-82' }); // attempt_index 81 = Account 82 assert.deepEqual(sent, [{ account: 'acc-2', body: { eval_account_id: 'acc-2' } }]); // Answered: never asked again, while Vest's list is slow to move it diff --git a/scripts/check-vest.mjs b/scripts/check-vest.mjs index 691b152..345562c 100644 --- a/scripts/check-vest.mjs +++ b/scripts/check-vest.mjs @@ -11,7 +11,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { isVestWalletAddress, vestAccountId, num, toMs, formatDecimal, vestDayOf, lastVestResetMs, parseCapitalAccount, - vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN, + vestAccountName, vestAccountNumber, 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, readClaims, payoutDaysOf, payoutsSince, longClosure, unknownOpenFill, awaitingActivation, vestStatusName, @@ -98,10 +98,11 @@ test('capital accounts parse, and are named for their rules', () => { assert.equal(a.dailyLossFloor, 48500); assert.equal(a.maxDailyLossPct, 0.03); assert.equal(a.dailyResetAt, 1793035043842); - assert.equal(vestAccountName(a), 'V50K-E-D3-T10-34'); + // attempt_index 34 is what the Vest app shows as Account 35. + assert.equal(vestAccountName(a), 'V50K-E-D3-T10-35'); - assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, stage: 2 })), 'V50K-F-D3-34'); - assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, max_daily_loss_pct: '0' })), 'V50K-E-D0-T10-34'); + assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, stage: 2 })), 'V50K-F-D3-35'); + assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, max_daily_loss_pct: '0' })), 'V50K-E-D0-T10-35'); assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, initial_capital: '5000.00000000', attempt_index: 12, stage: 2, steps_total: 2, stages: [ @@ -109,18 +110,32 @@ test('capital accounts parse, and are named for their rules', () => { { ordinal: 2, kind: 'eval', profit_target: '250.00000000' }, { ordinal: 3, kind: 'funded', profit_target: '' }, ], - })), 'V5K-E-D3-T5-12'); + })), 'V5K-E-D3-T5-13'); assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, plan_product_type: 'instant_funded', initial_capital: '25000.00000000', max_daily_loss_pct: '0', attempt_index: 7, stages: [{ ordinal: 1, kind: 'funded', profit_target: '' }], - })), 'V25K-I-7'); + })), 'V25K-I-08'); assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, plan_product_type: 'instant_funded', initial_capital: '500.00000000', attempt_index: 1, stages: [{ ordinal: 1, kind: 'funded', profit_target: '' }], - })), 'V500-I-1'); + })), 'V500-I-02'); assert.equal(parseCapitalAccount({ nope: true }), null); }); +test('an account is numbered as the Vest app numbers it', () => { + // The app: `Account ${attempt_index < 9 ? '0' : ''}${attempt_index + 1}`, + // and "Legacy Account" without an attempt index. + const numbered = (attempt) => vestAccountNumber(parseCapitalAccount({ ...evalAccount, attempt_index: attempt })); + assert.equal(numbered(0), '01'); + assert.equal(numbered(8), '09'); + assert.equal(numbered(9), '10'); + assert.equal(numbered(72), '73'); + assert.equal(numbered(-1), '0'); + const { attempt_index: _, ...legacy } = evalAccount; + assert.equal(vestAccountNumber(parseCapitalAccount(legacy)), '0'); + assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, attempt_index: 72 })), 'V50K-E-D3-T10-73'); +}); + test('a passed evaluation waits on its funded account, as the Vest app decides', () => { const passed = { ...evalAccount, status: VEST_STATUS.GOAL_PASSED }; const a = parseCapitalAccount(passed);