Vest: no leg opens into a market close, and accounts plan on Vest's day

Market close: a Vest entry now needs its symbol's market to stay open
for RESET_CLEAR_HOLDS holds of the leg (clearsClose), the clearance the
8 PM ET reset already gets: at the target hold, and at the leg's sized
hold when a leverage cap stretches it. That covers the weekly close and
the daily maintenance hour as Vest's market-hours periods list them.
Before a closure that runs past an 8 PM ET reset (the weekend, a
holiday: longClosure), the bot's own legs still open in the last 5
minutes are closed at market, loudly; a position the bot didn't open is
named in the log and left alone.

Vest's day: Vest accounts are planned, and their legs and entries
counted, on Vest's day (20:00 ET) instead of the Central date
(tradeDateFor, PlatformClient.tradeDate). Between 5 PM CT and the reset
the Central date counted the open Vest day as a prior day and as today's
realized P&L both ($3,000 to go, $800 made: $2,050 left, not $1,900) and
started the leg count over. Other platforms keep the Central date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 14:12:27 -05:00
co-authored by Claude Opus 5.5
parent beddbdcf6c
commit ec3e6ea388
8 changed files with 290 additions and 32 deletions
+20 -1
View File
@@ -14,7 +14,7 @@ import {
projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM,
dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget,
currentTradeDateCT, foldReason, withinFirmCaps, personSizeFactor, personHoldHours, PERSON_SIZE_SPREAD, machineSide,
resolveEffectiveConfig, stageInputsFor, floorRoom, resetClearStop, shortestNextLeg, computeDayPlan,
resolveEffectiveConfig, stageInputsFor, floorRoom, resetClearStop, shortestNextLeg, computeDayPlan, clearsClose, RESET_CLEAR_HOLDS,
} from '../lib/trading-logic.ts';
// Deterministic PRNG so a failure reproduces.
@@ -527,6 +527,25 @@ test('resetClearStop: the widest stop a leg may carry and still close before the
assert.ok(resetClearStop({ leg: 500, resetInMs: 16 * h, holdHours: 1.5 }) - 1400 > 2604.24);
});
test('clearsClose: no leg starts under four holds of its market\'s close', () => {
const h = 3_600_000;
assert.equal(RESET_CLEAR_HOLDS, 4);
// The weekly close 7.5 h off, legs sized for 2 h: no entry.
assert.equal(clearsClose({ closeInMs: 7.5 * h, holdHours: 2 }), false);
// Exactly four holds off, or more: the clearance the daily reset gets.
assert.equal(clearsClose({ closeInMs: 8 * h, holdHours: 2 }), true);
assert.equal(clearsClose({ closeInMs: 8 * h - 1, holdHours: 2 }), false);
assert.equal(clearsClose({ closeInMs: 20 * h, holdHours: 2 }), true);
// A leg its leverage cap holds to 3.5 h needs 14 h, where a 2 h one is clear.
assert.equal(clearsClose({ closeInMs: 10 * h, holdHours: 3.5 }), false);
// It agrees with resetClearStop, which gives no stop at all under four holds.
for (const off of [5.9, 6, 6.1, 12]) {
assert.equal(clearsClose({ closeInMs: off * h, holdHours: 1.5 }), resetClearStop({ leg: 500, resetInMs: off * h, holdHours: 1.5 }) > 0);
}
// An unknown hold never clears.
assert.equal(clearsClose({ closeInMs: 20 * h, holdHours: NaN }), false);
});
test('planLegSize: a split day\'s next leg, with the day\'s gain on its stop, sizes up for the same hold', () => {
// Dingo's 2026-10-27 evals: up $2,604.24 on a $1,400 day, $2,395.76 to
// pass, NDX at ~$29.5k on Vest (1.31%/day), sized for 1.5 h.
+58 -4
View File
@@ -1,9 +1,10 @@
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts) and the
// relayed-session store (lib/vest-session.ts).
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts), the
// relayed-session store (lib/vest-session.ts), and the trade date the engine
// plans Vest accounts on (lib/trading-logic.ts).
//
// npm run check
//
// Both files import only Node built-ins, so Node strips their types and loads
// These files import only Node built-ins, so Node strips their types and loads
// them directly; .mjs keeps this file out of Next's type check. The samples are
// shaped like the gateway's real answers (captured 2026-10-26).
import { test } from 'node:test';
@@ -12,9 +13,10 @@ import {
isVestWalletAddress, vestAccountId, num, toMs, formatDecimal, vestDayOf, lastVestResetMs, parseCapitalAccount,
vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN,
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
accountScopedToken,
accountScopedToken, longClosure,
} from '../lib/vest-helpers.ts';
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
import { computeDayPlan, currentTradeDateCT, tradeDateFor } from '../lib/trading-logic.ts';
const iso = (s) => Date.parse(s);
@@ -205,6 +207,58 @@ test('market hours: open stretches, closures and crypto', () => {
assert.deepEqual(marketOpen([], 600), { open: true, closesAt: null });
});
test('market hours: a closure past an 8 PM ET reset is a long one, the maintenance hour is not', () => {
const open = (from, to) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } });
const shut = (from, to, closure) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'unspecified', closure, tradingStatus: closure === 'weekly' ? 'CLOSE' : 'OPEN', price: { allow: closure !== 'weekly' } } });
const week = [
open('2026-10-28T22:00:00Z', '2026-10-29T21:00:00Z'),
shut('2026-10-29T21:00:00Z', '2026-10-29T22:00:00Z', 'maintenance'),
open('2026-10-29T22:00:00Z', '2026-10-30T21:00:00Z'),
shut('2026-10-30T21:00:00Z', '2026-11-01T23:00:00Z', 'weekly'),
open('2026-11-01T23:00:00Z', '2026-11-02T22:00:00Z'),
shut('2026-11-02T22:00:00Z', '2026-11-02T23:00:00Z', 'maintenance'),
open('2026-11-02T23:00:00Z', '2026-11-03T22:00:00Z'),
];
// The weekend: shut through Friday's and Saturday's 8 PM ET resets.
const friday = marketOpen(week, iso('2026-10-30T20:57:00Z'));
assert.deepEqual(friday, { open: true, closesAt: iso('2026-10-30T21:00:00Z') });
assert.equal(longClosure(week, friday.closesAt), true);
// The maintenance hour reopens before the evening's reset, in summer and winter.
const thursday = marketOpen(week, iso('2026-10-29T20:57:00Z'));
assert.equal(thursday.closesAt, iso('2026-10-29T21:00:00Z'));
assert.equal(longClosure(week, thursday.closesAt), false);
assert.equal(longClosure(week, iso('2026-11-02T22:00:00Z')), false);
// A gap in the periods is shut too (a holiday), and this one outlasts the reset.
const early = [open('2026-11-25T14:00:00Z', '2026-11-25T18:00:00Z'), open('2026-11-26T23:00:00Z', '2026-11-27T18:00:00Z')];
assert.equal(longClosure(early, iso('2026-11-25T18:00:00Z')), true);
// Shut as far as the periods go: long. Periods that just end: no close known.
assert.equal(longClosure(week.slice(0, 4), iso('2026-10-30T21:00:00Z')), true);
assert.equal(longClosure(week.slice(0, 3), iso('2026-10-30T21:00:00Z')), false);
assert.equal(longClosure(undefined, iso('2026-10-30T21:00:00Z')), false);
});
test("a Vest account plans on Vest's day: the 5 PM CT roll doesn't count today's P&L twice", () => {
const vest = { tradeDate: vestDayOf };
// 3:30 and 5:30 PM CT (4:30 / 6:30 PM ET): the Central date rolls at
// 5 PM CT, Vest's day only at 8 PM ET.
const before = new Date('2026-10-27T20:30:00Z');
const after = new Date('2026-10-27T22:30:00Z');
assert.equal(currentTradeDateCT(after), '2026-10-28');
assert.equal(tradeDateFor(vest, after), '2026-10-27');
assert.equal(tradeDateFor(vest, new Date('2026-10-28T00:00:00Z')), '2026-10-28');
// Every other platform keeps the Central date.
assert.equal(tradeDateFor(undefined, after), '2026-10-28');
assert.equal(tradeDateFor({}, after), '2026-10-28');
// $3,000 to go, 3 min days of $150, $800 made today: Vest's row for its
// day, and the realized P&L since its reset.
const rows = [{ date: '2026-10-27', pnl: 800 }];
const plan = (date) => computeDayPlan(7, date, 3000, 0, rows, 150, 3, 800, 800);
assert.equal(plan(currentTradeDateCT(before)).remainingToday, 1900);
assert.deepEqual(plan(tradeDateFor(vest, after)), plan(currentTradeDateCT(before)));
// On the Central date the $800 is a prior min day and today's P&L both.
assert.equal(plan(currentTradeDateCT(after)).remainingToday, 2050);
});
test('relayed sessions: taken, kept fresher, dropped, never listed with tokens', () => {
const now = iso('2026-10-26T18:40:00Z');
const jwt = (n) => `eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIke${n}In0.sig${n}`;