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);