From b5939a06a034a5bcb244e48aa36f2399b20fe1a9 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 27 Oct 2026 14:51:00 -0500 Subject: [PATCH] Vest: only a long closure holds entries back; the maintenance hour is ridden The close gate held every Vest entry within four holds of any close, which with a 2 h hold meant no weekday entry from about 9 AM to 5 PM ET for the one-hour maintenance break. That hour reopens before the reset, as CME's daily break does for futures legs, so only a weekend or holiday closure gates an entry now. Co-Authored-By: Claude Opus 5.5 --- lib/platform-client.ts | 7 ++++--- lib/vest-class.ts | 18 +++++++++++------- scripts/check-vest-client.mjs | 17 +++++++++++++---- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/lib/platform-client.ts b/lib/platform-client.ts index 5f7230a..fe617d3 100644 --- a/lib/platform-client.ts +++ b/lib/platform-client.ts @@ -48,9 +48,10 @@ export interface PlatformContract { /** Margin posted per dollar of notional (0.2 = 5:1 leverage), where the * broker says — caps a position at available funds ÷ this. */ marginRate?: number; - /** When the instrument's market next closes (epoch ms), where the - * platform says: no leg starts that wouldn't be done by then - * (clearsClose), each account's at its own size (fireLeg). */ + /** When the instrument's market next closes for long (epoch ms; Vest: a + * weekend or holiday, not the maintenance hour), where the platform + * says: no leg starts that wouldn't be done by then (clearsClose), each + * account's at its own size (fireLeg). */ closesAt?: number; } diff --git a/lib/vest-class.ts b/lib/vest-class.ts index 96739d1..fceefe4 100644 --- a/lib/vest-class.ts +++ b/lib/vest-class.ts @@ -1625,10 +1625,11 @@ export class VestClient implements PlatformClient { if (known.length > 0) await this.refreshMarks(known); const vols = await this.volatility(known); const now = Date.now(); - // A leg its symbol's market close (the weekend, the daily maintenance - // 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. + // A leg a long closure (a weekend, a holiday) 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 one — the + // daily reset's clearance. The daily maintenance hour reopens before + // the reset and is ridden, as futures legs ride CME's daily break. // 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. @@ -1648,8 +1649,11 @@ export class VestClient implements PlatformClient { const mark = this.marks.get(symbol); const vol = vols.get(symbol); let hours: { open: boolean; closesAt: number | null }; + let gateAt: number | null; try { - hours = marketOpen(await this.marketHours(spec.assetId), now); + const periods = await this.marketHours(spec.assetId); + hours = marketOpen(periods, now); + gateAt = hours.closesAt !== null && longClosure(periods, hours.closesAt) ? hours.closesAt : null; } catch (err) { board.push({ symbol, score: null, note: `market hours unknown (${errorMessage(err)})` }); continue; @@ -1673,7 +1677,7 @@ 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); + const closing = legCloseNote(gateAt, hold, now); board.push({ symbol, score, @@ -1692,7 +1696,7 @@ export class VestClient implements PlatformClient { spreadFraction: spread, minContracts: 1, marginRate: spec.marginRate || undefined, - closesAt: hours.closesAt ?? undefined, + closesAt: gateAt ?? undefined, }, }); } diff --git a/scripts/check-vest-client.mjs b/scripts/check-vest-client.mjs index 03a832f..204e5ad 100644 --- a/scripts/check-vest-client.mjs +++ b/scripts/check-vest-client.mjs @@ -137,22 +137,31 @@ test('the market-close guard closes a bot leg whose open came back unknown, not } }); -test('a symbol the close gate holds back stays on the board, and isn\'t picked', async () => { +test('a symbol the close gate holds back stays on the board, and isn\'t picked; the maintenance hour is ridden', async () => { const client = testClient((method, path) => { throw new Error(`no gateway in this test: ${method} ${path}`); }); client.refreshMarks = async () => {}; client.volatility = async (symbols) => new Map(symbols.map((s) => [s, { vol: 0.004, candles: 168 }])); + const marked = () => { for (const s of ['NDX-USD', 'BTC-PERP']) client.marks.set(s, { mark: s === 'NDX-USD' ? 20000 : 70000, at: Date.now() }); }; try { - // Thursday 2 PM ET: NDX's maintenance hour is 3 h off, crypto never closes. + // Thursday 2 PM ET: NDX's maintenance hour is 3 h off, and reopens + // before the reset — no gate. clockAt('2026-10-29T18:00:00Z'); - for (const s of ['NDX-USD', 'BTC-PERP']) client.marks.set(s, { mark: s === 'NDX-USD' ? 20000 : 70000, at: Date.now() }); + marked(); + await client.pickCandidate('Auto', ['NDX-USD', 'BTC-PERP'], false); + const thursday = client.lastCandidates.find((c) => c.id === 'NDX-USD'); + assert.equal(thursday.closesAt, undefined); + + // Friday 2 PM ET: the weekly close is 3 h off; crypto never closes. + clockAt('2026-10-30T18:00:00Z'); + marked(); const pick = await client.pickCandidate('Auto', ['NDX-USD', 'BTC-PERP'], false); assert.equal(pick.id, 'BTC-PERP'); assert.equal(pick.closesAt, undefined); assert.deepEqual(client.lastCandidates.map((c) => c.id).sort(), ['BTC-PERP', 'NDX-USD']); const ndx = client.lastCandidates.find((c) => c.id === 'NDX-USD'); - assert.equal(ndx.closesAt, iso('2026-10-29T21:00:00Z')); + assert.equal(ndx.closesAt, iso('2026-10-30T21:00:00Z')); // The survey keeps it up for the dashboard, though nothing is picked. assert.equal(await client.pickCandidate('survey', ['NDX-USD'], false), null); assert.deepEqual(client.lastCandidates.map((c) => c.id), ['NDX-USD']);