Vest: a crypto leg's stop holds back the engine's default slippage, not NDX's

Crypto (exchangeInfo asset "crypto": BTC, ETH, SOL, HYPE) trades on weekends
when NDX, SPX and GC are shut, but its stop slippage on Vest is unmeasured, and
its book is a fraction of NDX's: about $10M an hour on a weekend against $600M,
with a batch's stops firing together. Its stops now leave 0.1% of the
position unspent (the engine's default) instead of the 0.02% measured on NDX
stop-outs; every other symbol keeps 0.02%.

The share rides on the picked contract and is used wherever a Vest leg's stop
is sized: the batch's plan, the leg's own stop, the dashboard's next stop, and
the 7:55 PM ET reset guard, which now sums each open position's own share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-30 17:26:53 -05:00
co-authored by Claude Opus 5.5
parent 618976bf5f
commit 10da0ca83e
8 changed files with 94 additions and 16 deletions
+46 -4
View File
@@ -62,7 +62,7 @@ const { putVestSession, vestSessionFor, dropVestSession } = await import('../lib
const {
botOpenedPositions, loadAccountMeta, saveAccountMeta, saveFundTransactions, wipeAccountData, loadClaimedCycles, saveClaimedCycles,
} = await import('../lib/db.ts');
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts');
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE, resetSafeStop } = await import('../lib/vest-helpers.ts');
const { encodeContractName } = await import('../lib/dxtrade-helpers.ts');
const { tradeDateFor, stageInputsFor, resolveEffectiveConfig, claimedCycle, endedCycleCompletion } = await import('../lib/trading-logic.ts');
const { isClientReady, isOrderNotPlaced } = await import('../lib/platform-client.ts');
@@ -93,10 +93,10 @@ const week = [
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 spec = (symbol, assetId, asset = 'stock') => ({
symbol, assetId, sizeDecimals: 3, priceDecimals: 2, tick: 0.01, takerFee: 0.0001, marginRate: 0.05, tradingStatus: 'TRADING', capitalTradingStatus: 'TRADING', asset,
});
const specs = new Map([['NDX-USD', spec('NDX-USD', 7)], ['BTC-PERP', spec('BTC-PERP', 9)]]);
const specs = new Map([['NDX-USD', spec('NDX-USD', 7)], ['BTC-PERP', spec('BTC-PERP', 9, 'crypto')]]);
/** A client with no sync running, its gateway answered by `answer`, holding
* acc-1 and acc-2 under `ids` (the ids a sync gives them: A and B). */
@@ -221,6 +221,9 @@ test('a symbol the close gate holds back stays on the board, and isn\'t picked;
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-30T21:00:00Z'));
// Crypto's stops keep the engine's slippage share; NDX's is measured.
assert.equal(pick.stopSlippageFraction, 0.001);
assert.equal(ndx.stopSlippageFraction, 0.0002);
// 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']);
@@ -769,3 +772,42 @@ test('exits wait for the login session to be relayed again; an open goes now or
client.disconnect();
}
});
test("the reset guard holds each position's own slippage reserve: crypto's wider than NDX's", async () => {
// 7:56 PM ET: four minutes to Vest's reset. acc-1 holds BTC, acc-2 NDX,
// neither with a stop.
clockAt('2026-10-29T23:56:00Z');
const stops = [];
const positions = [
rawPosition('9201', 'acc-1', iso('2026-10-29T22:00:00Z'), { symbol: 'BTC-PERP', quantity: '5', openPrice: '70000' }),
rawPosition('9202', 'acc-2', iso('2026-10-29T22:00:00Z'), { quantity: '15', openPrice: '20000' }),
];
const client = testClient((method, path, opts) => {
if (method === 'GET' && path === '/v3/positions/opened') return { data: { positions } };
if (method === 'POST' && path === '/v3/positions/stop-loss') { stops.push({ account: opts.account, ...opts.body }); return { data: {} }; }
throw new Error(`no gateway in this test: ${method} ${path}`);
});
client.refreshMarks = async () => {};
client.marks.set('BTC-PERP', { mark: 70000, at: Date.now() });
client.marks.set('NDX-USD', { mark: 20000, at: Date.now() });
for (const id of [1, 2]) {
client.capital.set(id, { status: VEST_STATUS_ACTIVE, maxDailyLossPct: 0.03, maxDrawdownLimit: 47000 });
client.accountCashBalances[id] = { amount: 50000, realizedPnL: 0 };
}
client.syncComplete = true;
const fitted = (quantity, openPrice, reserve) => resetSafeStop({
side: 'long', quantity, openPrice, mark: openPrice, balance: 50000, floorBalance: 50000, dailyLossPct: 0.03,
maxDrawdownFloor: 47000, reserve, exitFeeRate: 0.0001, currentStop: null, tick: 0.01,
});
try {
await client.guardReset();
const by = Object.fromEntries(stops.map((s) => [s.account, Number(s.triggerPrice)]));
// BTC: 0.1% of $350k = $350 held back, over the flat $150.
assert.equal(by['acc-1'], fitted(5, 70000, 350).price);
assert.notEqual(by['acc-1'], fitted(5, 70000, 150).price);
// NDX: 0.02% of $300k is $60, so the flat $150 stands.
assert.equal(by['acc-2'], fitted(15, 20000, 150).price);
} finally {
client.disconnect();
}
});