Vest: report on Vest's day, and close a leg whose open came back unknown

The reporter and the dashboard take each firm's own trade date
(tradeDateFor), as the engine does: between 5 PM CT and Vest's 8 PM ET
reset the Central date counted the open Vest day as a prior day and as
today's P&L both, and could report a pass the master keeps for good. The
completion check's settle window now follows the client's own roll.

An open whose outcome came back unknown is marked the bot's once a read
shows the position it made, so the market-close guard closes it before a
long closure; an unmarked leg is no longer logged as a hand trade.

A symbol the close gate holds back stays on the candidate board for the
dashboard and the report, and fireLeg holds each account's leg to the
close at its own size, not just the batch lead's. check-vest-client.mjs
drives the guard and the board with every gateway call answered locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 14:41:20 -05:00
co-authored by Claude Opus 5.5
parent ec3e6ea388
commit ef15200e5e
11 changed files with 345 additions and 42 deletions
+60 -21
View File
@@ -38,7 +38,7 @@
import { randomUUID } from 'crypto';
import axios from './http-client';
import { currentTradeDateCT, rankForTarget, dllReserve, resetClearStop, clearsClose, RESET_CLEAR_HOLDS } from './trading-logic';
import { currentTradeDateCT, rankForTarget, dllReserve, resetClearStop, legCloseNote } from './trading-logic';
import { sizingHoldHours } from './person-size';
import {
saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions,
@@ -52,7 +52,7 @@ import { encodeContractName, decodeContractName, roundToStep, scoreCandidate } f
import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from './vest-session';
import {
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, longClosure, num, toMs, formatDecimal, resetSafeStop,
parseTxn, bookTxns, awaitingLegs, unknownOpenFill, unrealizedPnl, hourlyVolatility, marketOpen, longClosure, num, toMs, formatDecimal, resetSafeStop,
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_TXN, VEST_STOP_SLIPPAGE_FRACTION,
} from './vest-helpers';
import type { VestCapitalAccount, VestPosition, VestOpenOrder, VestTxn, VestTrade } from './vest-helpers';
@@ -112,6 +112,9 @@ const RESET_ENTRY_BLOCK_MS = RESET_GUARD_MS + 2 * 60_000;
/** How long a stop or close the guard just sent counts before a read of
* open positions shows it. */
const GUARD_ECHO_MS = 30_000;
/** An open of unknown outcome is the position opened this close to its send
* and its error — the gateway's clock and lag (unknownOpenFill). */
const UNKNOWN_OPEN_SLACK_MS = 2 * 60_000;
/** account_meta key of the profit claims this client made, per account. */
const CLAIMS_META = 'vest_claims';
/** A claim shows in Vest's transactions as a WITHDRAW of the same amount
@@ -223,6 +226,10 @@ export class VestClient implements PlatformClient {
closes: new Map<string, number>(), named: new Map<string, number>(), hoursWarnedAt: new Map<string, number>(), noSessionFor: 0,
};
private closeGuardInFlight = false;
/** Opens whose outcome came back unknown, by account: the position one
* made is the bot's once a read shows it (adoptUnknownOpen) — no exits
* went on it, so it's the leg guardClose most needs to see. */
private unknownOpens = new Map<number, { accountId: string; symbol: string; side: 'long' | 'short'; from: number; until: number }>();
private capital = new Map<number, VestCapitalAccount>();
private positionsDetail = new Map<number, VestPosition[]>();
private openOrders = new Map<number, VestOpenOrder[]>();
@@ -603,6 +610,7 @@ export class VestClient implements PlatformClient {
this.positionsDetail.set(id, mine);
this.openOrders.set(id, orders.filter((o) => o.accountId === a.id));
this.applyPosition(acc, mine);
this.adoptUnknownOpen(id, mine, startedAt);
const amount = free.get(a.id);
if (amount === undefined) { complete = false; continue; }
@@ -1082,9 +1090,10 @@ export class VestClient implements PlatformClient {
* stop. No leg starts under RESET_CLEAR_HOLDS holds of a close
* (pickCandidate), so this only meets one that outlived them.
*
* Only positions the bot's own order opened (markOwn): one without that
* mark — a hand trade, or one from before marking began — is left as it
* is and named once in the log. The daily maintenance hour reopens
* Only positions the bot's own order opened (markOwn, or an open of
* unknown outcome that turned out filled: adoptUnknownOpen): one without
* that mark — a hand trade, or one from before marking began — is left
* as it is and named once in the log. The daily maintenance hour reopens
* before the reset, and is ridden.
*/
private async guardClose(): Promise<void> {
@@ -1137,8 +1146,11 @@ export class VestClient implements PlatformClient {
}
return;
}
const readAt = Date.now();
const { data } = await this.request<{ positions?: unknown[] }>('trade', 'GET', '/v3/positions/opened');
const open = (data?.positions ?? []).map(parsePosition).filter((p): p is VestPosition => p !== null && closing.has(p.symbol));
const all = (data?.positions ?? []).map(parsePosition).filter((p): p is VestPosition => p !== null);
for (const id of [...this.unknownOpens.keys()]) this.adoptUnknownOpen(id, all, readAt);
const open = all.filter((p) => closing.has(p.symbol));
const mine = botOpenedPositions(open.map((p) => p.positionId));
for (const acc of this.accountList) {
const uuid = this.uuidById.get(acc.id);
@@ -1149,7 +1161,7 @@ export class VestClient implements PlatformClient {
if (!mine.has(pos.positionId)) {
if (guard.named.get(pos.positionId) !== closesAt) {
guard.named.set(pos.positionId, closesAt);
console.error(`${this.tag} ${acc.name}: ${pos.side} ${pos.symbol} wasn't opened by the bot and stays open — its market closes in ${inMin} min until past an 8 PM ET reset; close it on Vest if it shouldn't carry the closure and the reopen's gap`);
console.error(`${this.tag} ${acc.name}: ${pos.side} ${pos.symbol} isn't marked as the bot's (a hand trade, or a bot leg from before marking or a restart mid-entry) and stays open — its market closes in ${inMin} min until past an 8 PM ET reset; close it on Vest if it shouldn't carry the closure and the reopen's gap`);
}
continue;
}
@@ -1170,6 +1182,23 @@ export class VestClient implements PlatformClient {
}
}
/** An open of unknown outcome that a read (started at `readAt`) shows
* filled after all: the position is the bot's (markOwn). One the read
* doesn't show by then never filled — a market open is IOC. */
private adoptUnknownOpen(accountId: number, positions: VestPosition[], readAt: number): void {
const sent = this.unknownOpens.get(accountId);
if (!sent) return;
const pos = unknownOpenFill(sent, positions);
if (pos) {
this.unknownOpens.delete(accountId);
this.markOwn('opened', accountId, pos);
const name = this.accountList.find((a) => a.id === accountId)?.name ?? String(accountId);
console.warn(`${this.tag} ${name}: the open of unknown outcome filled after all — ${pos.side} ${pos.quantity} ${pos.symbol} (${pos.positionId}) marked as the bot's`);
} else if (readAt > sent.until) {
this.unknownOpens.delete(accountId);
}
}
private settleSuspects(): void {
const now = Date.now();
for (const [id, since] of this.suspectAccounts) {
@@ -1600,13 +1629,12 @@ export class VestClient implements PlatformClient {
// hour) catches open can't be exited until the market reopens, and
// meets the reopen's gap with its stop: none starts under
// RESET_CLEAR_HOLDS holds of the close — the daily reset's clearance.
const tooSoon = (closesAt: number | null, holdHours: number) =>
closesAt !== null && !clearsClose({ closeInMs: closesAt - now, holdHours });
const closingNote = (closesAt: number, holdHours: number) =>
`closes in ${((closesAt - now) / 3_600_000).toFixed(1)}h, under ${RESET_CLEAR_HOLDS} holds of ${holdHours.toFixed(1)}h`;
// Such a symbol stays on the board (the dashboard's candidates) but
// isn't picked (legCloseNote); fireLeg holds each account's own leg
// to it too.
const hold = ctx?.targetHoldHours ?? targetHold;
const board: { symbol: string; score: number | null; note: string; contract?: PlatformContract; closesAt?: number | null }[] = [];
const board: { symbol: string; score: number | null; note: string; contract?: PlatformContract; closing?: boolean }[] = [];
// No new position this close to the reset: nothing picked, and the
// last survey's candidates stay up for the dashboard.
const toReset = nextVestResetMs(now) - now;
@@ -1631,11 +1659,10 @@ export class VestClient implements PlatformClient {
const spread = mark && spec.tick > 0 ? (ASSUMED_SPREAD_TICKS * spec.tick) / mark.mark : NaN;
const rejection = spec.capitalTradingStatus !== 'TRADING' ? `capital trading ${spec.capitalTradingStatus || 'unknown'}`
: !hours.open ? 'market closed'
: tooSoon(hours.closesAt, hold) ? closingNote(hours.closesAt!, hold)
: !mark || now - mark.at > 5 * 60_000 ? 'no mark price'
: !(spec.tick > 0) ? 'no tick size'
: (vol?.candles ?? 0) < 25 ? `only ${vol?.candles ?? 0} hourly candles`
: null;
: !mark || now - mark.at > 5 * 60_000 ? 'no mark price'
: !(spec.tick > 0) ? 'no tick size'
: (vol?.candles ?? 0) < 25 ? `only ${vol?.candles ?? 0} hourly candles`
: null;
if (rejection) { board.push({ symbol, score: null, note: rejection }); continue; }
const pct = spec.takerFee > 0 ? spec.takerFee * 100 : firm?.commissionPct ?? 0;
const score = scoreCandidate({
@@ -1646,13 +1673,14 @@ export class VestClient implements PlatformClient {
volatilityPerHour: vol?.vol ?? null,
});
if (score === null) { board.push({ symbol, score: null, note: 'no volatility' }); continue; }
const closing = legCloseNote(hours.closesAt, hold, now);
board.push({
symbol,
score,
note: `fee ${(pct * 100).toFixed(2)}bp${vol?.vol ? `, σ ${(vol.vol * Math.sqrt(24) * 100).toFixed(2)}%/day` : ''}`
+ (spec.marginRate ? `, ${+(1 / spec.marginRate).toFixed(1)}:1` : '')
+ (hours.closesAt ? `, open ${((hours.closesAt - now) / 3_600_000).toFixed(1)}h more` : ''),
closesAt: hours.closesAt,
+ (closing ? `, ${closing}` : hours.closesAt ? `, open ${((hours.closesAt - now) / 3_600_000).toFixed(1)}h more` : ''),
closing: closing !== null,
contract: {
id: symbol,
name: encodeContractName(symbol, step),
@@ -1664,11 +1692,12 @@ export class VestClient implements PlatformClient {
spreadFraction: spread,
minContracts: 1,
marginRate: spec.marginRate || undefined,
closesAt: hours.closesAt ?? undefined,
},
});
}
let ranked = board.filter((b) => b.contract).sort((a, b) => a.score! - b.score!);
let ranked = board.filter((b) => b.contract && !b.closing).sort((a, b) => a.score! - b.score!);
if (ctx) {
const sized = ranked.flatMap((b) => {
const ev = ctx.evaluate(b.contract!);
@@ -1676,7 +1705,8 @@ export class VestClient implements PlatformClient {
b.note += `, $${Math.round(ev.notional / 1000)}k → ${ev.holdHours.toFixed(1)}h, ~$${Math.round(ev.cost)}`;
// Held short of the target hold (a leverage cap), the leg
// runs longer: it has to clear the close as well.
if (tooSoon(b.closesAt ?? null, ev.holdHours)) { b.note += `, ${closingNote(b.closesAt!, ev.holdHours)}`; return []; }
const closing = legCloseNote(b.contract!.closesAt, ev.holdHours, now);
if (closing) { b.note += `, ${closing}`; return []; }
return [{ b, holdHours: ev.holdHours, cost: ev.cost }];
});
ranked = rankForTarget(sized, ctx.targetHoldHours).map((i) => sized[i].b);
@@ -1854,6 +1884,15 @@ export class VestClient implements PlatformClient {
for (const w of this.fillWaiters.filter((x) => x.orderId === orderNumber)) w.callback(result);
this.fillWaiters = this.fillWaiters.filter((x) => x.orderId !== orderNumber);
return result;
} catch (err) {
// An open that may have gone in: no exits will go on it, so the
// position it made, once a read shows it, is marked the bot's.
if (opening && !isOrderNotPlaced(err)) {
this.unknownOpens.set(accountId, {
accountId: uuid, symbol, side, from: sentAt - UNKNOWN_OPEN_SLACK_MS, until: Date.now() + UNKNOWN_OPEN_SLACK_MS,
});
}
throw err;
} finally {
this.pendingFills--;
if (opening) {