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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ef15200e5e
commit
b5939a06a0
@@ -48,9 +48,10 @@ export interface PlatformContract {
|
|||||||
/** Margin posted per dollar of notional (0.2 = 5:1 leverage), where the
|
/** Margin posted per dollar of notional (0.2 = 5:1 leverage), where the
|
||||||
* broker says — caps a position at available funds ÷ this. */
|
* broker says — caps a position at available funds ÷ this. */
|
||||||
marginRate?: number;
|
marginRate?: number;
|
||||||
/** When the instrument's market next closes (epoch ms), where the
|
/** When the instrument's market next closes for long (epoch ms; Vest: a
|
||||||
* platform says: no leg starts that wouldn't be done by then
|
* weekend or holiday, not the maintenance hour), where the platform
|
||||||
* (clearsClose), each account's at its own size (fireLeg). */
|
* says: no leg starts that wouldn't be done by then (clearsClose), each
|
||||||
|
* account's at its own size (fireLeg). */
|
||||||
closesAt?: number;
|
closesAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -1625,10 +1625,11 @@ export class VestClient implements PlatformClient {
|
|||||||
if (known.length > 0) await this.refreshMarks(known);
|
if (known.length > 0) await this.refreshMarks(known);
|
||||||
const vols = await this.volatility(known);
|
const vols = await this.volatility(known);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// A leg its symbol's market close (the weekend, the daily maintenance
|
// A leg a long closure (a weekend, a holiday) catches open can't be
|
||||||
// hour) catches open can't be exited until the market reopens, and
|
// exited until the market reopens, and meets the reopen's gap with
|
||||||
// meets the reopen's gap with its stop: none starts under
|
// its stop: none starts under RESET_CLEAR_HOLDS holds of one — the
|
||||||
// RESET_CLEAR_HOLDS holds of the close — the daily reset's clearance.
|
// 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
|
// Such a symbol stays on the board (the dashboard's candidates) but
|
||||||
// isn't picked (legCloseNote); fireLeg holds each account's own leg
|
// isn't picked (legCloseNote); fireLeg holds each account's own leg
|
||||||
// to it too.
|
// to it too.
|
||||||
@@ -1648,8 +1649,11 @@ export class VestClient implements PlatformClient {
|
|||||||
const mark = this.marks.get(symbol);
|
const mark = this.marks.get(symbol);
|
||||||
const vol = vols.get(symbol);
|
const vol = vols.get(symbol);
|
||||||
let hours: { open: boolean; closesAt: number | null };
|
let hours: { open: boolean; closesAt: number | null };
|
||||||
|
let gateAt: number | null;
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
board.push({ symbol, score: null, note: `market hours unknown (${errorMessage(err)})` });
|
board.push({ symbol, score: null, note: `market hours unknown (${errorMessage(err)})` });
|
||||||
continue;
|
continue;
|
||||||
@@ -1673,7 +1677,7 @@ export class VestClient implements PlatformClient {
|
|||||||
volatilityPerHour: vol?.vol ?? null,
|
volatilityPerHour: vol?.vol ?? null,
|
||||||
});
|
});
|
||||||
if (score === null) { board.push({ symbol, score: null, note: 'no volatility' }); continue; }
|
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({
|
board.push({
|
||||||
symbol,
|
symbol,
|
||||||
score,
|
score,
|
||||||
@@ -1692,7 +1696,7 @@ export class VestClient implements PlatformClient {
|
|||||||
spreadFraction: spread,
|
spreadFraction: spread,
|
||||||
minContracts: 1,
|
minContracts: 1,
|
||||||
marginRate: spec.marginRate || undefined,
|
marginRate: spec.marginRate || undefined,
|
||||||
closesAt: hours.closesAt ?? undefined,
|
closesAt: gateAt ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
const client = testClient((method, path) => {
|
||||||
throw new Error(`no gateway in this test: ${method} ${path}`);
|
throw new Error(`no gateway in this test: ${method} ${path}`);
|
||||||
});
|
});
|
||||||
client.refreshMarks = async () => {};
|
client.refreshMarks = async () => {};
|
||||||
client.volatility = async (symbols) => new Map(symbols.map((s) => [s, { vol: 0.004, candles: 168 }]));
|
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 {
|
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');
|
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);
|
const pick = await client.pickCandidate('Auto', ['NDX-USD', 'BTC-PERP'], false);
|
||||||
assert.equal(pick.id, 'BTC-PERP');
|
assert.equal(pick.id, 'BTC-PERP');
|
||||||
assert.equal(pick.closesAt, undefined);
|
assert.equal(pick.closesAt, undefined);
|
||||||
assert.deepEqual(client.lastCandidates.map((c) => c.id).sort(), ['BTC-PERP', 'NDX-USD']);
|
assert.deepEqual(client.lastCandidates.map((c) => c.id).sort(), ['BTC-PERP', 'NDX-USD']);
|
||||||
const ndx = client.lastCandidates.find((c) => c.id === '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.
|
// The survey keeps it up for the dashboard, though nothing is picked.
|
||||||
assert.equal(await client.pickCandidate('survey', ['NDX-USD'], false), null);
|
assert.equal(await client.pickCandidate('survey', ['NDX-USD'], false), null);
|
||||||
assert.deepEqual(client.lastCandidates.map((c) => c.id), ['NDX-USD']);
|
assert.deepEqual(client.lastCandidates.map((c) => c.id), ['NDX-USD']);
|
||||||
|
|||||||
Reference in New Issue
Block a user