Merge branch 'vest-fix-timing' into vest-fixes
# Conflicts: # lib/reporter.ts # lib/vest-class.ts # scripts/check-vest.mjs
This commit is contained in:
+98
-4
@@ -1,9 +1,10 @@
|
||||
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts) and the
|
||||
// relayed-session store (lib/vest-session.ts).
|
||||
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts), the
|
||||
// relayed-session store (lib/vest-session.ts), and the trade date the engine
|
||||
// plans Vest accounts on (lib/trading-logic.ts).
|
||||
//
|
||||
// npm run check
|
||||
//
|
||||
// Both files import only Node built-ins, so Node strips their types and loads
|
||||
// These 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';
|
||||
@@ -13,9 +14,10 @@ import {
|
||||
vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN,
|
||||
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
|
||||
accountScopedToken, VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, claimWithdrawal, settleClaims,
|
||||
claimFundRows, readClaims, payoutDaysOf, payoutsSince,
|
||||
claimFundRows, readClaims, payoutDaysOf, payoutsSince, longClosure, unknownOpenFill,
|
||||
} from '../lib/vest-helpers.ts';
|
||||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
|
||||
import { computeDayPlan, currentTradeDateCT, tradeDateFor, cycleCompletion } from '../lib/trading-logic.ts';
|
||||
|
||||
const iso = (s) => Date.parse(s);
|
||||
|
||||
@@ -127,6 +129,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');
|
||||
@@ -343,6 +362,81 @@ test('market hours: open stretches, closures and crypto', () => {
|
||||
assert.deepEqual(marketOpen([], 600), { open: true, closesAt: null });
|
||||
});
|
||||
|
||||
test('market hours: a closure past an 8 PM ET reset is a long one, the maintenance hour is not', () => {
|
||||
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'),
|
||||
shut('2026-11-02T22:00:00Z', '2026-11-02T23:00:00Z', 'maintenance'),
|
||||
open('2026-11-02T23:00:00Z', '2026-11-03T22:00:00Z'),
|
||||
];
|
||||
// The weekend: shut through Friday's and Saturday's 8 PM ET resets.
|
||||
const friday = marketOpen(week, iso('2026-10-30T20:57:00Z'));
|
||||
assert.deepEqual(friday, { open: true, closesAt: iso('2026-10-30T21:00:00Z') });
|
||||
assert.equal(longClosure(week, friday.closesAt), true);
|
||||
// The maintenance hour reopens before the evening's reset, in summer and winter.
|
||||
const thursday = marketOpen(week, iso('2026-10-29T20:57:00Z'));
|
||||
assert.equal(thursday.closesAt, iso('2026-10-29T21:00:00Z'));
|
||||
assert.equal(longClosure(week, thursday.closesAt), false);
|
||||
assert.equal(longClosure(week, iso('2026-11-02T22:00:00Z')), false);
|
||||
// A gap in the periods is shut too (a holiday), and this one outlasts the reset.
|
||||
const early = [open('2026-11-25T14:00:00Z', '2026-11-25T18:00:00Z'), open('2026-11-26T23:00:00Z', '2026-11-27T18:00:00Z')];
|
||||
assert.equal(longClosure(early, iso('2026-11-25T18:00:00Z')), true);
|
||||
// Shut as far as the periods go: long. Periods that just end: no close known.
|
||||
assert.equal(longClosure(week.slice(0, 4), iso('2026-10-30T21:00:00Z')), true);
|
||||
assert.equal(longClosure(week.slice(0, 3), iso('2026-10-30T21:00:00Z')), false);
|
||||
assert.equal(longClosure(undefined, iso('2026-10-30T21:00:00Z')), false);
|
||||
});
|
||||
|
||||
test("a Vest account plans on Vest's day: the 5 PM CT roll doesn't count today's P&L twice", () => {
|
||||
const vest = { tradeDate: vestDayOf };
|
||||
// 3:30 and 5:30 PM CT (4:30 / 6:30 PM ET): the Central date rolls at
|
||||
// 5 PM CT, Vest's day only at 8 PM ET.
|
||||
const before = new Date('2026-10-27T20:30:00Z');
|
||||
const after = new Date('2026-10-27T22:30:00Z');
|
||||
assert.equal(currentTradeDateCT(after), '2026-10-28');
|
||||
assert.equal(tradeDateFor(vest, after), '2026-10-27');
|
||||
assert.equal(tradeDateFor(vest, new Date('2026-10-28T00:00:00Z')), '2026-10-28');
|
||||
// Every other platform keeps the Central date.
|
||||
assert.equal(tradeDateFor(undefined, after), '2026-10-28');
|
||||
assert.equal(tradeDateFor({}, after), '2026-10-28');
|
||||
// $3,000 to go, 3 min days of $150, $800 made today: Vest's row for its
|
||||
// day, and the realized P&L since its reset.
|
||||
const rows = [{ date: '2026-10-27', pnl: 800 }];
|
||||
const plan = (date) => computeDayPlan(7, date, 3000, 0, rows, 150, 3, 800, 800);
|
||||
assert.equal(plan(currentTradeDateCT(before)).remainingToday, 1900);
|
||||
assert.deepEqual(plan(tradeDateFor(vest, after)), plan(currentTradeDateCT(before)));
|
||||
// On the Central date the $800 is a prior min day and today's P&L both.
|
||||
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