Vest (next.vestmarkets.com) is a perps prop firm: index, metal and energy
perps on CME hours, plus stocks, FX and crypto. It joins as a fourth
platform, 'vest', on the DXtrade engine paths. Those are now keyed by
isNotionalPlatform() instead of === 'dxtrade': the firm's own symbols,
percent fees, notional sizing from volatility, and a scheduler lane of its
own.
- Login is relayed, not stored. Vest signs people in with Google through
Privy and renews the session with a device key the browser won't export.
Extension 0.17.0's vest-relay.js reads the token the Vest app renews for
itself (~15 min). It renews nothing, so the tab is never logged out. The
background script posts that token to POST /api/vest/session, which holds
it in memory only. With no Vest tab open on a machine that trades Vest,
the extension opens one.
- The firm's credential is the Vest login's wallet address. The dashboard's
form offers the relayed wallet with one click.
- VestClient (lib/vest-class.ts), mapped from the web app's traffic and code
since Vest publishes no API:
- Reads the capital accounts, balances (free amount plus position
collateral), positions and open orders with the user token.
- Mints per-account tokens for orders and transactions.
- Names accounts for their rules (V50K-E-D3-T10-34, V50K-F-D3-34,
V25K-I-7) so account-type prefixes can match.
- Takes Vest's own max-drawdown and daily-loss floors. dayStartBalance =
floor + the type's limit, so the engine's room is never more than
Vest's.
- Uses each symbol's taker fee and margin rate from exchangeInfo, and
hourly volatility over in-session hours only.
- Places take-profit and stop as position legs, which Vest cancels as an
OCO pair.
- Daily P&L and fund rows come from /v3/transactions by Vest's 20:00 ET
day. A books check holds an account, naming the transaction types, when
the balance and the transactions disagree.
- scripts/check-vest.mjs (in npm run check): Vest's day across DST, account
names, parsing, volatility gaps, market hours, the session store.
Not yet run against Vest with a live session: the order and exit bodies
come from the app's code, and the transaction classification from its enum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
14 KiB
JavaScript
228 lines
14 KiB
JavaScript
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts) and the
|
||
// relayed-session store (lib/vest-session.ts).
|
||
//
|
||
// npm run check
|
||
//
|
||
// Both 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';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
isVestWalletAddress, vestAccountId, num, toMs, formatDecimal, vestDayOf, lastVestResetMs, parseCapitalAccount,
|
||
vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN,
|
||
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS,
|
||
} from '../lib/vest-helpers.ts';
|
||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions } from '../lib/vest-session.ts';
|
||
|
||
const iso = (s) => Date.parse(s);
|
||
|
||
test('wallet addresses and account ids', () => {
|
||
assert.equal(isVestWalletAddress('0x7ce10771819f80130e204ddb5ca1522e48a9b451'), true);
|
||
assert.equal(isVestWalletAddress('0x7CE10771819F80130E204DDB5CA1522E48A9B451'), true);
|
||
assert.equal(isVestWalletAddress('0x7ce1'), false);
|
||
assert.equal(isVestWalletAddress(''), false);
|
||
const id = vestAccountId('843f60e2-e013-5a0e-8b6c-face84bc5509');
|
||
assert.equal(id, vestAccountId('843F60E2-E013-5A0E-8B6C-FACE84BC5509'));
|
||
assert.notEqual(id, vestAccountId('102566b2-515a-51c5-9ad7-4a7eae0a6a2a'));
|
||
assert.ok(id >= 2 ** 50 && Number.isSafeInteger(id));
|
||
});
|
||
|
||
test('numbers, times and decimals as the gateway writes them', () => {
|
||
assert.equal(num('49968.511115'), 49968.511115);
|
||
assert.equal(num(''), undefined);
|
||
assert.equal(num(null), undefined);
|
||
assert.equal(toMs(1793035089), 1793035089000);
|
||
assert.equal(toMs(1793035089277), 1793035089277);
|
||
assert.equal(toMs('2026-10-26T18:41:02.886Z'), iso('2026-10-26T18:41:02.886Z'));
|
||
assert.ok(Number.isNaN(toMs(undefined)));
|
||
assert.equal(formatDecimal(29554.5, 2), '29554.5');
|
||
assert.equal(formatDecimal(50, 2), '50');
|
||
assert.equal(formatDecimal(3 * 0.0001, 4), '0.0003');
|
||
assert.equal(formatDecimal(42.65449999, 4), '42.6545');
|
||
assert.equal(formatDecimal(1e-7, 4), '0');
|
||
});
|
||
|
||
test("Vest's day turns at 20:00 New York, summer and winter", () => {
|
||
// EDT (UTC−4): 20:00 ET is 00:00 UTC.
|
||
assert.equal(vestDayOf(iso('2026-10-26T23:59:00Z')), '2026-10-26');
|
||
assert.equal(vestDayOf(iso('2026-10-27T00:00:00Z')), '2026-10-27');
|
||
assert.equal(vestDayOf(iso('2026-10-26T22:05:00Z')), '2026-10-26'); // the bots' 6:05 PM ET open
|
||
// EST (UTC−5): 20:00 ET is 01:00 UTC.
|
||
assert.equal(vestDayOf(iso('2026-12-01T00:30:00Z')), '2026-11-30');
|
||
assert.equal(vestDayOf(iso('2026-12-01T01:00:00Z')), '2026-12-01');
|
||
// Month and year ends.
|
||
assert.equal(vestDayOf(iso('2026-12-31T23:00:00Z')), '2026-12-31');
|
||
assert.equal(vestDayOf(iso('2027-01-01T01:30:00Z')), '2027-01-01');
|
||
|
||
assert.equal(lastVestResetMs(iso('2026-10-26T18:00:00Z')), iso('2026-10-26T00:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-10-27T00:00:00Z')), iso('2026-10-27T00:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-12-01T00:30:00Z')), iso('2026-11-30T01:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-12-01T01:00:01Z')), iso('2026-12-01T01:00:00Z'));
|
||
// Across the November switch back (Nov 1 2026, 2 AM ET).
|
||
assert.equal(lastVestResetMs(iso('2026-11-01T12:00:00Z')), iso('2026-11-01T00:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-11-02T12:00:00Z')), iso('2026-11-02T01:00:00Z'));
|
||
});
|
||
|
||
const evalAccount = {
|
||
payment_id: 'tailoredpay:vest-markets:production:12727557177',
|
||
id: 'e7f69717-bd18-5cb7-9466-673cb283d0b2',
|
||
plan_id: '86bb03f9-7422-513c-8d5c-c546cb8d221d',
|
||
account_type: 2, stage: 1, status: 2,
|
||
initial_capital: '50000.00000000', max_drawdown_pct: '0.060000', max_leverage: '5.0000', max_profit_split_pct: '0.8000',
|
||
target_equity: '55000.00000000', max_drawdown_limit: '47000.00000000', max_daily_loss_pct: '0.030000',
|
||
daily_reset_balance: '50000.00000000000000', daily_reset_equity: '50000.00000000000000',
|
||
daily_reset_base_limit: '1500.000000000000000', daily_loss_floor: '48500.00000000000000', daily_reset_at: 1793035043842,
|
||
origin: 'eval_purchase', plan_name: 'Vest Capital Evaluation Fee | 10% Goal | 3% Daily Loss | 80/20 | Emerald',
|
||
plan_product_type: 'eval', step: 1, steps_total: 1, root_account_id: 'e7f69717-bd18-5cb7-9466-673cb283d0b2',
|
||
stages: [
|
||
{ ordinal: 1, kind: 'eval', initial_capital: '50000.00000000', profit_target: '5000.00000000', max_drawdown_pct: '0.060000' },
|
||
{ ordinal: 2, kind: 'funded', initial_capital: '50000.00000000', profit_target: '', max_drawdown_pct: '0.060000' },
|
||
],
|
||
attempt_index: 34, activated_at: 1793035043834, created_at: 1793035043756, updated_at: 1793035043843,
|
||
};
|
||
|
||
test('capital accounts parse, and are named for their rules', () => {
|
||
const a = parseCapitalAccount(evalAccount);
|
||
assert.equal(a.id, evalAccount.id);
|
||
assert.equal(a.kind, 'eval');
|
||
assert.equal(a.status, 2);
|
||
assert.equal(a.initialCapital, 50000);
|
||
assert.equal(a.profitTarget, 5000);
|
||
assert.equal(a.maxDrawdownLimit, 47000);
|
||
assert.equal(a.dailyLossFloor, 48500);
|
||
assert.equal(a.maxDailyLossPct, 0.03);
|
||
assert.equal(a.dailyResetAt, 1793035043842);
|
||
assert.equal(vestAccountName(a), 'V50K-E-D3-T10-34');
|
||
|
||
assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, stage: 2 })), 'V50K-F-D3-34');
|
||
assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, max_daily_loss_pct: '0' })), 'V50K-E-D0-T10-34');
|
||
assert.equal(vestAccountName(parseCapitalAccount({
|
||
...evalAccount, initial_capital: '5000.00000000', attempt_index: 12, stage: 2, steps_total: 2,
|
||
stages: [
|
||
{ ordinal: 1, kind: 'eval', profit_target: '500.00000000' },
|
||
{ ordinal: 2, kind: 'eval', profit_target: '250.00000000' },
|
||
{ ordinal: 3, kind: 'funded', profit_target: '' },
|
||
],
|
||
})), 'V5K-E-D3-T5-12');
|
||
assert.equal(vestAccountName(parseCapitalAccount({
|
||
...evalAccount, plan_product_type: 'instant_funded', initial_capital: '25000.00000000', max_daily_loss_pct: '0', attempt_index: 7,
|
||
stages: [{ ordinal: 1, kind: 'funded', profit_target: '' }],
|
||
})), 'V25K-I-7');
|
||
assert.equal(vestAccountName(parseCapitalAccount({
|
||
...evalAccount, plan_product_type: 'instant_funded', initial_capital: '500.00000000', attempt_index: 1,
|
||
stages: [{ ordinal: 1, kind: 'funded', profit_target: '' }],
|
||
})), 'V500-I-1');
|
||
assert.equal(parseCapitalAccount({ nope: true }), null);
|
||
});
|
||
|
||
const openPosition = {
|
||
positionId: 'afe96d7e-f5ac-4b8a-aae8-f5b63b74e508', accountId: '843f60e2-e013-5a0e-8b6c-face84bc5509', accountType: 2,
|
||
symbol: 'NDX-USD-PERP', side: 'short', quantity: '42.6545', openPrice: '29529.250000000', closePrice: '290.250000000',
|
||
openDate: 1793035089277, pnl: '0', fee: '31.488885', funding: '-5.423271000000000000', collateral: '24944.027426', leverage: '50',
|
||
createdAt: 1793035089244, updatedAt: 1793037621104,
|
||
stopLosses: [{ id: 'e22311d2-fbd9-4ab8-b65f-c2921f58fefc', triggerPrice: '29554.500000000', sizeMode: 'full_position', executionType: 'market', status: 'awaiting' }],
|
||
takeProfits: [],
|
||
};
|
||
|
||
test('positions, their legs and unrealized P&L', () => {
|
||
const p = parsePosition(openPosition);
|
||
assert.equal(p.side, 'short');
|
||
assert.equal(p.quantity, 42.6545);
|
||
assert.equal(p.collateral, 24944.027426);
|
||
assert.equal(p.openedAt, 1793035089277);
|
||
assert.equal(awaitingLegs(p).length, 1);
|
||
assert.equal(awaitingLegs({ ...p, stopLosses: [{ ...p.stopLosses[0], status: 'triggered' }] }).length, 0);
|
||
// Short 42.6545 from 29529.25, marked at 29477.50: +51.75 points each.
|
||
assert.ok(Math.abs(unrealizedPnl(p, 29477.5) - 42.6545 * 51.75) < 1e-6);
|
||
assert.equal(unrealizedPnl(p, NaN), 0);
|
||
assert.equal(parsePosition({ ...openPosition, quantity: '0' }), null);
|
||
assert.equal(parseOpenOrder({ order_id: 'o1', account_id: 'a1', position_id: 'p1', symbol: 'GC-PERP', created_at: 1 }).positionId, 'p1');
|
||
assert.equal(parseOpenOrder({ symbol: 'x' }), null);
|
||
});
|
||
|
||
test('transactions book into daily P&L and fund rows by Vest day', () => {
|
||
assert.equal(txnKind(VEST_TXN.FEE), 'trade');
|
||
assert.equal(txnKind(VEST_TXN.REALIZED_PNL), 'trade');
|
||
assert.equal(txnKind(VEST_TXN.FUNDING), 'trade');
|
||
assert.equal(txnKind(VEST_TXN.WITHDRAW), 'fund');
|
||
assert.equal(txnKind(VEST_TXN.FILL_MARGIN), 'margin');
|
||
assert.equal(txnKind(VEST_TXN.ORDER_MARGIN_RELEASE), 'margin');
|
||
|
||
const w = parseTxn({ account_type: 3, action_id: 'a9', amount: '1200', created_at: iso('2026-10-26T15:00:00Z'), transaction_type: 2 });
|
||
assert.equal(w.amount, -1200);
|
||
assert.equal(w.kind, 'fund');
|
||
assert.equal(parseTxn({ amount: 'x', transaction_type: 9, created_at: 1 }), null);
|
||
|
||
const rows = [
|
||
{ action_id: 'd', amount: '50000', created_at: iso('2026-10-20T14:00:00Z'), transaction_type: 1 },
|
||
{ action_id: 'o', amount: '-24944.02', created_at: iso('2026-10-26T17:18:00Z'), transaction_type: 6 },
|
||
{ action_id: 'f1', amount: '-31.49', created_at: iso('2026-10-26T17:18:00Z'), transaction_type: 8 },
|
||
// After 20:00 ET on the 26th: the 27th's day.
|
||
{ action_id: 'r1', amount: '812.5', created_at: iso('2026-10-27T00:30:00Z'), transaction_type: 9 },
|
||
{ action_id: 'f2', amount: '-31.2', created_at: iso('2026-10-27T00:30:00Z'), transaction_type: 8 },
|
||
{ action_id: 'u1', amount: '-5.42', created_at: iso('2026-10-27T01:00:00Z'), transaction_type: 10 },
|
||
].map(parseTxn);
|
||
const { daily, funds } = bookTxns(rows);
|
||
assert.deepEqual([...funds.entries()], [['2026-10-20', 50000]]);
|
||
assert.ok(Math.abs(daily.get('2026-10-26').pnl - -31.49) < 1e-9);
|
||
assert.ok(Math.abs(daily.get('2026-10-27').pnl - (812.5 - 31.2 - 5.42)) < 1e-9);
|
||
assert.ok(Math.abs(daily.get('2026-10-27').fees - 31.2) < 1e-9);
|
||
});
|
||
|
||
test('hourly volatility leaves out the gaps', () => {
|
||
const start = iso('2026-10-19T00:00:00Z');
|
||
const kl = [];
|
||
let price = 29000;
|
||
for (let h = 0; h < 60; h++) {
|
||
price *= h % 2 === 0 ? 1.001 : 1 / 1.001;
|
||
kl.push([start + h * 3_600_000, '0', '0', '0', String(price), '1']);
|
||
}
|
||
const base = hourlyVolatility(kl);
|
||
assert.ok(base > 0.0009 && base < 0.0011, `σ ${base}`);
|
||
// A weekend gap with a 3% jump: not an hour's move, so σ doesn't change.
|
||
const gapped = [...kl, [start + 120 * 3_600_000, '0', '0', '0', String(price * 1.03), '1']];
|
||
assert.ok(Math.abs(hourlyVolatility(gapped) - base) < 1e-12);
|
||
// Any order in, the same σ out.
|
||
assert.equal(hourlyVolatility([...kl].reverse()), base);
|
||
assert.equal(hourlyVolatility(kl.slice(0, MIN_HOURLY_RETURNS)), null);
|
||
});
|
||
|
||
test('market hours: open stretches, closures and crypto', () => {
|
||
const periods = [
|
||
{ fromMs: 0, toMs: 100, state: { session: 'unspecified', closure: 'weekly', tradingStatus: 'CLOSE', price: { allow: false } } },
|
||
{ fromMs: 100, toMs: 200, state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
{ fromMs: 200, toMs: 300, state: { session: 'regular', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
{ fromMs: 300, toMs: 400, state: { session: 'unspecified', closure: 'maintenance', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
{ fromMs: 400, toMs: 500, state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
];
|
||
assert.deepEqual(marketOpen(periods, 50), { open: false, closesAt: null });
|
||
assert.deepEqual(marketOpen(periods, 150), { open: true, closesAt: 300 });
|
||
assert.deepEqual(marketOpen(periods, 350), { open: false, closesAt: null });
|
||
assert.deepEqual(marketOpen(periods, 450), { open: true, closesAt: 500 });
|
||
assert.deepEqual(marketOpen(periods, 600), { open: false, closesAt: null });
|
||
assert.deepEqual(marketOpen([], 600), { open: true, closesAt: null });
|
||
});
|
||
|
||
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}`;
|
||
const wallet = '0x7CE10771819F80130E204DDB5CA1522E48A9B451';
|
||
assert.equal(putVestSession({ accessToken: 'nope', expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), 'accessToken is not a JWT');
|
||
assert.equal(putVestSession({ accessToken: jwt(1), expiresAt: now + 10_000, primaryAddr: wallet, userId: 'u' }, now), 'token already expired');
|
||
assert.equal(putVestSession({ accessToken: jwt(1), expiresAt: now + 900_000, primaryAddr: '0x12', userId: 'u' }, now), 'primaryAddr is not a wallet address');
|
||
assert.equal(putVestSession({ accessToken: jwt(1), expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), null);
|
||
// An older token relayed late doesn't replace the fresher one.
|
||
assert.equal(putVestSession({ accessToken: jwt(0), expiresAt: now + 300_000, primaryAddr: wallet, userId: 'u' }, now), null);
|
||
assert.equal(vestSessionFor(wallet.toLowerCase(), now).accessToken, jwt(1));
|
||
assert.equal(vestSessionFor(wallet, now + 890_000), null); // under 30s left
|
||
const listed = listVestSessions();
|
||
assert.equal(listed.length, 1);
|
||
assert.equal(listed[0].primaryAddr, wallet.toLowerCase());
|
||
assert.equal('accessToken' in listed[0], false);
|
||
dropVestSession(wallet, jwt(0)); // not the held one: kept
|
||
assert.ok(vestSessionFor(wallet, now));
|
||
dropVestSession(wallet, jwt(1));
|
||
assert.equal(vestSessionFor(wallet, now), null);
|
||
});
|