Vest: close out trades left open on an account Vest stopped listing

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 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-26 20:00:43 -05:00
co-authored by Claude Opus 5.5
parent 71606ffc9e
commit a03bfb5605
2 changed files with 69 additions and 3 deletions
+6
View File
@@ -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()); `).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 /** Newest first (by close, else open); every account when accountIds is
* undefined. */ * undefined. */
export function loadTrades(o: { accountIds?: number[]; limit: number }): TradeRow[] { export function loadTrades(o: { accountIds?: number[]; limit: number }): TradeRow[] {
+63 -3
View File
@@ -42,7 +42,7 @@ import { currentTradeDateCT, rankForTarget, dllReserve } from './trading-logic';
import { sizingHoldHours } from './person-size'; import { sizingHoldHours } from './person-size';
import { import {
saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions, saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions,
saveEndedCycleStart, saveTrades, markTrade, getOrCreateSetting, TRADES_SINCE_SETTING, saveEndedCycleStart, saveTrades, markTrade, getOrCreateSetting, TRADES_SINCE_SETTING, openTradeAccounts,
} from './db'; } from './db';
import { getCachedFirm } from './firm-catalog'; import { getCachedFirm } from './firm-catalog';
import { firmInstruments } from './instruments'; import { firmInstruments } from './instruments';
@@ -90,6 +90,10 @@ const TXN_MAX_PAGES = 200;
/** Position history (GET /v3/positions) pages, for the trades table. */ /** Position history (GET /v3/positions) pages, for the trades table. */
const TRADES_PAGE = 50; const TRADES_PAGE = 50;
const TRADES_MAX_PAGES = 20; 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. */ /** Balance the books can't explain by this much holds the account. */
const BOOKS_TOLERANCE = 200; const BOOKS_TOLERANCE = 200;
const SUSPECT_SETTLE_MS = 90_000; 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. */ /** Open positions already in the trades table, by id → size stored. */
private recordedOpen = new Map<string, number>(); private recordedOpen = new Map<string, number>();
private flatAmounts = new Map<number, number>(); private flatAmounts = new Map<number, number>();
/** Accounts off Vest's active list whose leftover open trades were
* read once more this run (closeOutDropped). */
private closeOutTried = new Set<number>();
private closeOutInFlight = false;
/** When a relayed token last turned out to be one account's own. */ /** When a relayed token last turned out to be one account's own. */
private scopedRefusedAt = 0; 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'}` this.lastFetchRaw[`floors:${acc.name}`] = `dd ${a.maxDrawdownLimit ?? '?'}, daily ${this.dailyFloorOf(a, balance, now) ?? 'none'}`
+ `${a.targetEquity ? `, target ${a.targetEquity}` : ''}`; + `${a.targetEquity ? `, target ${a.targetEquity}` : ''}`;
} }
const shrank = this.accountList.some((a) => !list.some((b) => b.id === a.id));
this.accountList = list; this.accountList = list;
if (shrank) void this.closeOutDropped();
// An account gone from Vest's list (breached, closed) takes its // An account gone from Vest's list (breached, closed) takes its
// position with it — a stale one here would hold the lane. // position with it — a stale one here would hold the lane.
for (const key of Object.keys(this.positions)) { 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); for (const id of ids) this.fetchingIds.delete(id);
this.fetchDaysComplete = true; this.fetchDaysComplete = true;
} }
if (!onlyAccountIds) await this.closeOutDropped();
return { failedAccounts: failed }; 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<void> {
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. */ /** Every transaction on the account, newest page first, deduplicated. */
private async fetchTxns(uuid: string): Promise<VestTxn[]> { private async fetchTxns(uuid: string): Promise<VestTxn[]> {
const all = new Map<string, VestTxn>(); const all = new Map<string, VestTxn>();
@@ -963,13 +1023,13 @@ export class VestClient implements PlatformClient {
/** The account's position history — GET /v3/positions, the app's History /** The account's position history — GET /v3/positions, the app's History
* tab — into the trades table: every position, whoever opened it, with * tab — into the trades table: every position, whoever opened it, with
* how it closed. */ * how it closed. */
private async fetchTrades(account: PlatformAccount): Promise<void> { private async fetchTrades(account: PlatformAccount, viaLogin = false): Promise<void> {
const uuid = this.uuidById.get(account.id); const uuid = this.uuidById.get(account.id);
if (!uuid) return; if (!uuid) return;
const trades: VestTrade[] = []; const trades: VestTrade[] = [];
for (let page = 0; page < TRADES_MAX_PAGES; page++) { for (let page = 0; page < TRADES_MAX_PAGES; page++) {
const { data } = await this.request<{ positions?: unknown[] | null; total?: unknown }>('history', 'GET', '/v3/positions', { 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 ?? []; const rows = data?.positions ?? [];
for (const r of rows) { for (const r of rows) {