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 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-30 10:07:21 -05:00
co-authored by Claude Opus 5.5
parent 8a5b2a64e7
commit 8c863bdc1d
6 changed files with 59 additions and 18 deletions
+5
View File
@@ -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[];
+6
View File
@@ -443,6 +443,12 @@ export function keptRuleIdsField(keptRuleIds: string[] | null): Pick<FirmStats,
return keptRuleIds ? { keptRuleIds } : {};
}
/** FirmStats.vestAppNumbers: on a Vest firm, that its account names end in
* the number the Vest app shows (vest-helpers.ts vestAccountName). */
export function vestAppNumbersField(platform: string): Pick<FirmStats, 'vestAppNumbers'> {
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
+3 -2
View File
@@ -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),
};
});
}
+22 -8
View File
@@ -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 ────────────────────────────────────────────────────
+1 -1
View File
@@ -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
+22 -7
View File
@@ -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);