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:
co-authored by
Claude Opus 5.5
parent
ec3e6ea388
commit
ef15200e5e
@@ -15,6 +15,7 @@ import {
|
||||
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, clearsClose, RESET_CLEAR_HOLDS,
|
||||
legCloseNote,
|
||||
} from '../lib/trading-logic.ts';
|
||||
|
||||
// Deterministic PRNG so a failure reproduces.
|
||||
@@ -546,6 +547,20 @@ test('clearsClose: no leg starts under four holds of its market\'s close', () =>
|
||||
assert.equal(clearsClose({ closeInMs: 20 * h, holdHours: NaN }), false);
|
||||
});
|
||||
|
||||
test('legCloseNote: each account\'s leg is held to its market\'s close at its own size', () => {
|
||||
const h = 3_600_000;
|
||||
const now = Date.parse('2026-10-30T12:30:00Z');
|
||||
const close = now + 9 * h;
|
||||
// The batch's lead leg, 2 h: clear. Another account's, sized to 3.5 h: not.
|
||||
assert.equal(legCloseNote(close, 2, now), null);
|
||||
assert.equal(legCloseNote(close, 3.5, now), 'closes in 9.0h, under 4 holds of 3.5h');
|
||||
// A contract that names no close (futures, crypto) never holds a leg back.
|
||||
assert.equal(legCloseNote(undefined, 3.5, now), null);
|
||||
assert.equal(legCloseNote(null, 3.5, now), null);
|
||||
// An unknown hold doesn't open into a known close.
|
||||
assert.notEqual(legCloseNote(close, NaN, now), null);
|
||||
});
|
||||
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// Checks for the Vest client's own wiring (lib/vest-class.ts), past what its
|
||||
// pure helpers show: the market-close guard closing a leg whose open came back
|
||||
// unknown, and the candidate board keeping a symbol the close gate holds back.
|
||||
//
|
||||
// npm run check
|
||||
//
|
||||
// A scratch database, a fake clock, and every gateway call answered here —
|
||||
// nothing reaches Vest.
|
||||
import { after, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { registerHooks, stripTypeScriptTypes } from 'node:module';
|
||||
|
||||
// Resolve the extensionless local imports used by Next, without a build.
|
||||
function resolveTypes(specifier, context, nextResolve) {
|
||||
try {
|
||||
return nextResolve(specifier, context);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ERR_MODULE_NOT_FOUND' || !specifier.startsWith('.')) throw err;
|
||||
return nextResolve(`${specifier}.ts`, context);
|
||||
}
|
||||
}
|
||||
// The client's classes use parameter properties, which plain type
|
||||
// stripping refuses: transform them instead.
|
||||
function loadTypes(url, context, nextLoad) {
|
||||
if (!url.startsWith('file:') || !url.endsWith('.ts')) return nextLoad(url, context);
|
||||
const source = stripTypeScriptTypes(readFileSync(fileURLToPath(url), 'utf8'), { mode: 'transform' });
|
||||
return { format: 'module', source, shortCircuit: true };
|
||||
}
|
||||
registerHooks({ resolve: resolveTypes, load: loadTypes });
|
||||
|
||||
const directory = mkdtempSync(join(tmpdir(), 'autofirmer-vest-client-'));
|
||||
process.env.AUTOFIRMER_DB = join(directory, 'test.sqlite');
|
||||
delete process.env.NEXT_PHASE;
|
||||
after(() => rmSync(directory, { recursive: true, force: true }));
|
||||
|
||||
// No request leaves this process: axios refuses every one, and each client's
|
||||
// gateway is a function in this file — from construction on, since the
|
||||
// constructor starts a sync.
|
||||
const { default: axios } = await import('../lib/http-client.ts');
|
||||
axios.defaults.adapter = (config) => Promise.reject(new Error(`no network in this test: ${config.method} ${config.url}`));
|
||||
const { VestClient } = await import('../lib/vest-class.ts');
|
||||
let gateway = null;
|
||||
VestClient.prototype.request = async function (bucket, method, path, opts = {}) {
|
||||
if (!gateway) throw new Error(`no gateway in this test: ${method} ${path}`);
|
||||
return gateway(method, path, opts);
|
||||
};
|
||||
const { putVestSession } = await import('../lib/vest-session.ts');
|
||||
const { botOpenedPositions } = await import('../lib/db.ts');
|
||||
const { vestDayOf, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts');
|
||||
const { encodeContractName } = await import('../lib/dxtrade-helpers.ts');
|
||||
|
||||
const iso = (s) => Date.parse(s);
|
||||
const WALLET = '0x7ce10771819f80130e204ddb5ca1522e48a9b451';
|
||||
|
||||
// The clock the client reads (Date.now), set to an instant and running on.
|
||||
const realNow = Date.now;
|
||||
after(() => { Date.now = realNow; });
|
||||
let tokens = 0;
|
||||
function clockAt(when) {
|
||||
const start = realNow();
|
||||
const t0 = iso(when);
|
||||
Date.now = () => t0 + (realNow() - start);
|
||||
tokens += 1;
|
||||
assert.equal(putVestSession({ accessToken: `eyJ.test${tokens}.sig`, expiresAt: Date.now() + 3_600_000, primaryAddr: WALLET, userId: 'u' }), null);
|
||||
}
|
||||
|
||||
// The week of NDX hours from check-vest.mjs: maintenance at 21:00 UTC
|
||||
// (17:00 ET) Thursday, the weekly close at 21:00 UTC Friday.
|
||||
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'),
|
||||
];
|
||||
const spec = (symbol, assetId) => ({
|
||||
symbol, assetId, sizeDecimals: 3, priceDecimals: 2, tick: 0.01, takerFee: 0.0001, marginRate: 0.05, tradingStatus: 'TRADING', capitalTradingStatus: 'TRADING',
|
||||
});
|
||||
const specs = new Map([['NDX-USD', spec('NDX-USD', 7)], ['BTC-PERP', spec('BTC-PERP', 9)]]);
|
||||
|
||||
/** A client with no sync running, its gateway answered by `answer`. */
|
||||
function testClient(answer) {
|
||||
gateway = null;
|
||||
const client = new VestClient({ firmId: 'vest-test', firmName: 'Vest test', wallet: WALLET }, async () => {});
|
||||
clearInterval(client.syncInterval);
|
||||
clearInterval(client.watchdogInterval);
|
||||
gateway = answer;
|
||||
client.specs = specs;
|
||||
client.symbolSpecs = async () => specs;
|
||||
client.marketHours = async (assetId) => (assetId === 7 ? week : []);
|
||||
client.accountList = [{ id: 1, name: 'V50K-F-D3-1', active: true }, { id: 2, name: 'V50K-F-D3-2', active: true }];
|
||||
client.uuidById.set(1, 'acc-1');
|
||||
client.uuidById.set(2, 'acc-2');
|
||||
client.capital.set(1, { status: VEST_STATUS_ACTIVE });
|
||||
client.capital.set(2, { status: VEST_STATUS_ACTIVE });
|
||||
return client;
|
||||
}
|
||||
|
||||
test('the market-close guard closes a bot leg whose open came back unknown, not a hand trade', async () => {
|
||||
let positions = [];
|
||||
const closes = [];
|
||||
const client = testClient((method, path, opts) => {
|
||||
if (method === 'GET' && path === '/v3/positions/opened') return { data: { positions } };
|
||||
if (method === 'POST' && path === '/v3/positions/open') throw new Error('socket hang up');
|
||||
if (method === 'POST' && path === '/v3/positions/close') { closes.push({ account: opts.account, ...opts.body }); return { data: {} }; }
|
||||
throw new Error(`no gateway in this test: ${method} ${path}`);
|
||||
});
|
||||
try {
|
||||
// Friday 8:30 AM ET: the entry's send fails in transit, but it filled.
|
||||
clockAt('2026-10-30T12:30:00Z');
|
||||
await assert.rejects(client.sendOrder(1, encodeContractName('NDX-USD', 0.001), 500, 'Buy', 'Market'), /outcome unknown/);
|
||||
const raw = (positionId, accountId, openDate) => ({
|
||||
positionId, accountId, symbol: 'NDX-USD', side: 'LONG', quantity: '0.500', openPrice: '20000', collateral: '500', leverage: '20', openDate,
|
||||
stopLosses: [], takeProfits: [],
|
||||
});
|
||||
positions = [raw('9001', 'acc-1', iso('2026-10-30T12:30:01Z')), raw('9002', 'acc-2', iso('2026-10-30T11:00:00Z'))];
|
||||
client.positionsDetail.set(1, []);
|
||||
client.positionsDetail.set(2, [{ positionId: '9002', accountId: 'acc-2', symbol: 'NDX-USD', side: 'long', quantity: 0.5 }]);
|
||||
client.syncComplete = true;
|
||||
|
||||
// 4:56 PM ET: four minutes to the weekly close.
|
||||
clockAt('2026-10-30T20:56:00Z');
|
||||
await client.guardClose();
|
||||
assert.deepEqual(closes, [{ account: 'acc-1', positionId: '9001', symbol: 'NDX-USD', orderType: 'market' }]);
|
||||
assert.deepEqual([...botOpenedPositions(['9001', '9002'])], ['9001']);
|
||||
// A close just sent isn't sent again while a read may not show it.
|
||||
await client.guardClose();
|
||||
assert.equal(closes.length, 1);
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
test('a symbol the close gate holds back stays on the board, and isn\'t picked', 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 }]));
|
||||
try {
|
||||
// Thursday 2 PM ET: NDX's maintenance hour is 3 h off, crypto never closes.
|
||||
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() });
|
||||
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'));
|
||||
// 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']);
|
||||
// And the client's day is Vest's.
|
||||
assert.equal(client.tradeDate(iso('2026-10-27T22:30:00Z')), vestDayOf(iso('2026-10-27T22:30:00Z')));
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
});
|
||||
+42
-2
@@ -13,10 +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, longClosure,
|
||||
accountScopedToken, longClosure, unknownOpenFill,
|
||||
} 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';
|
||||
import { computeDayPlan, currentTradeDateCT, tradeDateFor, cycleCompletion } from '../lib/trading-logic.ts';
|
||||
|
||||
const iso = (s) => Date.parse(s);
|
||||
|
||||
@@ -128,6 +128,23 @@ const openPosition = {
|
||||
takeProfits: [],
|
||||
};
|
||||
|
||||
test('an open of unknown outcome is the position on its account, symbol and side opened around it', () => {
|
||||
const pos = (positionId, accountId, side, openedAt, symbol = 'NDX-USD') => ({
|
||||
positionId, accountId, symbol, side, quantity: 0.5, openPrice: 20000, collateral: 500, leverage: 20, openedAt, stopLosses: [], takeProfits: [],
|
||||
});
|
||||
const sent = { accountId: 'acc-1', symbol: 'NDX-USD', side: 'long', from: iso('2026-10-30T12:28:00Z'), until: iso('2026-10-30T12:32:15Z') };
|
||||
const filled = pos('9001', 'acc-1', 'long', iso('2026-10-30T12:30:01Z'));
|
||||
assert.equal(unknownOpenFill(sent, [pos('9002', 'acc-2', 'long', iso('2026-10-30T12:30:01Z')), filled]), filled);
|
||||
// Another side or symbol, a position from before it, or one opened after
|
||||
// the window (a hand trade later on): not it.
|
||||
assert.equal(unknownOpenFill(sent, [pos('9003', 'acc-1', 'short', iso('2026-10-30T12:30:01Z'))]), null);
|
||||
assert.equal(unknownOpenFill(sent, [pos('9004', 'acc-1', 'long', iso('2026-10-30T12:30:01Z'), 'SPX-USD')]), null);
|
||||
assert.equal(unknownOpenFill(sent, [pos('9005', 'acc-1', 'long', iso('2026-10-30T11:00:00Z'))]), null);
|
||||
assert.equal(unknownOpenFill(sent, [pos('9006', 'acc-1', 'long', iso('2026-10-30T14:00:00Z'))]), null);
|
||||
// No open time: can't be told from a hand trade.
|
||||
assert.equal(unknownOpenFill(sent, [pos('9007', 'acc-1', 'long', NaN)]), null);
|
||||
});
|
||||
|
||||
test('positions, their legs and unrealized P&L', () => {
|
||||
const p = parsePosition(openPosition);
|
||||
assert.equal(p.side, 'short');
|
||||
@@ -259,6 +276,29 @@ test("a Vest account plans on Vest's day: the 5 PM CT roll doesn't count today's
|
||||
assert.equal(plan(currentTradeDateCT(after)).remainingToday, 2050);
|
||||
});
|
||||
|
||||
test("a Vest account's completion is judged on Vest's day, and waits out Vest's own roll", () => {
|
||||
const vest = { tradeDate: vestDayOf };
|
||||
// $3,000 in 2 min days: day one made $3,100 and is flat. At 5:30 PM CT
|
||||
// its row is booked and its P&L is still today's realized P&L.
|
||||
const eff = { profitTarget: 3000, consistency: 0, minTradingDays: 2, minDayPnL: 0, drain: false, dailyLossLimit: 0 };
|
||||
const opts = {
|
||||
accountId: 7, cycleDailyPnL: [{ date: '2026-10-27', pnl: 3100 }], rawRealizedPnL: 3100, combineCmeHolidays: false,
|
||||
tradesEveryDay: true, effective: eff, equityProfit: 3100, splitEnabled: true, client: vest,
|
||||
};
|
||||
const evening = new Date('2026-10-27T22:30:00Z');
|
||||
// The Central date counts the one day twice and reports a pass.
|
||||
assert.equal(cycleCompletion({ ...opts, client: undefined, rawTradeDate: currentTradeDateCT(evening), now: evening }), 'today');
|
||||
assert.equal(cycleCompletion({ ...opts, rawTradeDate: tradeDateFor(vest, evening), now: evening }), 'none');
|
||||
// Two minutes past Vest's 8 PM ET roll the last sync's realized P&L can
|
||||
// still be the ended day's: no 'today' until the settle window passes.
|
||||
const justRolled = new Date('2026-10-28T00:02:00Z');
|
||||
assert.equal(tradeDateFor(vest, justRolled), '2026-10-28');
|
||||
assert.equal(cycleCompletion({ ...opts, rawTradeDate: '2026-10-28', now: justRolled }), 'none');
|
||||
// A real second day, once settled, finishes it.
|
||||
const settled = new Date('2026-10-28T00:10:00Z');
|
||||
assert.equal(cycleCompletion({ ...opts, rawTradeDate: '2026-10-28', rawRealizedPnL: 50, equityProfit: 3150, now: settled }), 'today');
|
||||
});
|
||||
|
||||
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}`;
|
||||
|
||||
Reference in New Issue
Block a user