From a03bfb56057d3cc2d377af820399935f10c7ec30 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Mon, 26 Oct 2026 20:00:43 -0500 Subject: [PATCH] Vest: close out trades left open on an account Vest stopped listing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An account that leaves Vest's active list (breached, closed) was never read again, so a position it had open stayed open in the trades table. When the list shrinks, and on every full history pass, the client reads such an account's position history once more per run — its id from memory or, after a restart, from Vest's closed-account history (GET /v3/capital/accounts/history) — falling back to the login's token if the closed account gets none of its own. Co-Authored-By: Claude Opus 5.5 --- lib/db.ts | 6 +++++ lib/vest-class.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/lib/db.ts b/lib/db.ts index 63976f7..95f605d 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -682,6 +682,12 @@ export function markTrade( `).run(t.positionId, t.accountId, t.accountName, t.symbol, t.side, t.quantity, t.openPrice, t.openedAt, Date.now()); } +/** Accounts with a trade the table still shows open. */ +export function openTradeAccounts(): { accountId: number; accountName: string }[] { + return db.prepare('SELECT account_id AS accountId, MAX(account_name) AS accountName FROM trades WHERE closed_at IS NULL GROUP BY account_id') + .all() as { accountId: number; accountName: string }[]; +} + /** Newest first (by close, else open); every account when accountIds is * undefined. */ export function loadTrades(o: { accountIds?: number[]; limit: number }): TradeRow[] { diff --git a/lib/vest-class.ts b/lib/vest-class.ts index a05c9c4..2e58ff2 100644 --- a/lib/vest-class.ts +++ b/lib/vest-class.ts @@ -42,7 +42,7 @@ import { currentTradeDateCT, rankForTarget, dllReserve } from './trading-logic'; import { sizingHoldHours } from './person-size'; import { saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions, - saveEndedCycleStart, saveTrades, markTrade, getOrCreateSetting, TRADES_SINCE_SETTING, + saveEndedCycleStart, saveTrades, markTrade, getOrCreateSetting, TRADES_SINCE_SETTING, openTradeAccounts, } from './db'; import { getCachedFirm } from './firm-catalog'; import { firmInstruments } from './instruments'; @@ -90,6 +90,10 @@ const TXN_MAX_PAGES = 200; /** Position history (GET /v3/positions) pages, for the trades table. */ const TRADES_PAGE = 50; const TRADES_MAX_PAGES = 20; +/** Closed-account history (GET /v3/capital/accounts/history) pages: the + * app's own size, and how far back to look for one. */ +const CLOSED_PAGE = 8; +const CLOSED_MAX_PAGES = 30; /** Balance the books can't explain by this much holds the account. */ const BOOKS_TOLERANCE = 200; const SUSPECT_SETTLE_MS = 90_000; @@ -218,6 +222,10 @@ export class VestClient implements PlatformClient { /** Open positions already in the trades table, by id → size stored. */ private recordedOpen = new Map(); private flatAmounts = new Map(); + /** Accounts off Vest's active list whose leftover open trades were + * read once more this run (closeOutDropped). */ + private closeOutTried = new Set(); + private closeOutInFlight = false; /** When a relayed token last turned out to be one account's own. */ private scopedRefusedAt = 0; @@ -579,7 +587,9 @@ export class VestClient implements PlatformClient { this.lastFetchRaw[`floors:${acc.name}`] = `dd ${a.maxDrawdownLimit ?? '?'}, daily ${this.dailyFloorOf(a, balance, now) ?? 'none'}` + `${a.targetEquity ? `, target ${a.targetEquity}` : ''}`; } + const shrank = this.accountList.some((a) => !list.some((b) => b.id === a.id)); this.accountList = list; + if (shrank) void this.closeOutDropped(); // An account gone from Vest's list (breached, closed) takes its // position with it — a stale one here would hold the lane. for (const key of Object.keys(this.positions)) { @@ -940,9 +950,59 @@ export class VestClient implements PlatformClient { for (const id of ids) this.fetchingIds.delete(id); this.fetchDaysComplete = true; } + if (!onlyAccountIds) await this.closeOutDropped(); return { failedAccounts: failed }; } + /** + * Trades the table still shows open on accounts Vest no longer lists + * (breached, closed): read those accounts' position history once more, + * so they close out with how they ended. Their ids come from memory, or + * — after a restart — from Vest's closed-account history. Once per + * account per run; a failure is logged and left. + */ + private async closeOutDropped(): Promise { + if (this.closeOutInFlight || this.intentionalDisconnect || !this.userToken()) return; + const listed = new Set(this.accountList.map((a) => a.id)); + const stale = openTradeAccounts().filter((a) => !listed.has(a.accountId) && !this.closeOutTried.has(a.accountId)); + if (stale.length === 0) return; + this.closeOutInFlight = true; + try { + let cursor: string | null = null; + for (let page = 0; page < CLOSED_MAX_PAGES && stale.some((a) => !this.uuidById.has(a.accountId)); page++) { + const { data } = await this.request<{ accounts?: unknown[] | null; next?: unknown }>('history', 'GET', '/v3/capital/accounts/history', { + params: { limit: CLOSED_PAGE, ...(cursor ? { cursor } : {}) }, + }); + for (const raw of data?.accounts ?? []) { + const a = parseCapitalAccount(raw); + if (a && !this.uuidById.has(vestAccountId(a.id))) this.uuidById.set(vestAccountId(a.id), a.id); + } + cursor = typeof data?.next === 'string' && data.next ? data.next : null; + if (!cursor) break; + } + for (const a of stale) { + this.closeOutTried.add(a.accountId); + if (!this.uuidById.has(a.accountId)) { + console.warn(`${this.tag} ${a.accountName}: trades left open, but Vest lists no such account, active or closed — left as they are`); + continue; + } + try { + const acc = { id: a.accountId, name: a.accountName, active: false }; + // A closed account may get no token of its own: the + // login's reads it as the app's history tab does. + await this.fetchTrades(acc).catch(() => this.fetchTrades(acc, true)); + console.log(`${this.tag} ${a.accountName}: off Vest's active list — read its position history one last time`); + } catch (err) { + console.warn(`${this.tag} ${a.accountName}: off Vest's active list, and its position history read failed: ${errorMessage(err)}`); + } + } + } catch (err) { + console.warn(`${this.tag} closed-account history read failed: ${errorMessage(err)}`); + } finally { + this.closeOutInFlight = false; + } + } + /** Every transaction on the account, newest page first, deduplicated. */ private async fetchTxns(uuid: string): Promise { const all = new Map(); @@ -963,13 +1023,13 @@ export class VestClient implements PlatformClient { /** The account's position history — GET /v3/positions, the app's History * tab — into the trades table: every position, whoever opened it, with * how it closed. */ - private async fetchTrades(account: PlatformAccount): Promise { + private async fetchTrades(account: PlatformAccount, viaLogin = false): Promise { const uuid = this.uuidById.get(account.id); if (!uuid) return; const trades: VestTrade[] = []; for (let page = 0; page < TRADES_MAX_PAGES; page++) { const { data } = await this.request<{ positions?: unknown[] | null; total?: unknown }>('history', 'GET', '/v3/positions', { - account: uuid, params: { account_id: uuid, limit: TRADES_PAGE, offset: page * TRADES_PAGE }, + ...(viaLogin ? {} : { account: uuid }), params: { account_id: uuid, limit: TRADES_PAGE, offset: page * TRADES_PAGE }, }); const rows = data?.positions ?? []; for (const r of rows) {