// Checks for the broker session rest times in lib/trading-logic.ts: when a // rested session's next session opens, when it logs back in, and its // check-ins (lib/clients.ts decides when a session is idle). // // npm run check import { test } from 'node:test'; import assert from 'node:assert/strict'; import { nextSessionOpenAt, planCheckIn, planRest, isNoTradeTime, currentTradeDateCT, REST_LEAD_MIN_MS, REST_LEAD_MAX_MS, REST_MIN_MS, CHECK_IN_GAP_MIN_MS, CHECK_IN_GAP_MAX_MS, } from '../lib/trading-logic.ts'; const MIN = 60_000; const HOUR = 60 * MIN; const at = (iso) => Date.parse(iso); const iso = (ms) => new Date(ms).toISOString(); const fullCme = { everyDay: false, mode: 'full_cme', jitterFor: () => 0 }; const equity = { everyDay: false, mode: 'equity_hours', jitterFor: () => 0 }; const everyDay = { everyDay: true, mode: 'full_cme', jitterFor: () => 0 }; function centralMinutes(ms) { const parts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: 'numeric', hour12: false, timeZone: 'America/Chicago', }).formatToParts(new Date(ms)); const get = (t) => Number(parts.find((p) => p.type === t)?.value); return (get('hour') % 24) * 60 + get('minute'); } // Deterministic PRNG so a failure reproduces. function rng(seed) { let s = seed >>> 0; return () => { s = (s + 0x6d2b79f5) >>> 0; let t = s; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } // ── nextSessionOpenAt ─────────────────────────────────────────────────────── test('full CME: a Saturday rest opens Sunday 5:45 PM CT', () => { // Sat 2026-10-24 12:00 CDT assert.equal(iso(nextSessionOpenAt(at('2026-10-24T17:00:00Z'), fullCme)), '2026-10-25T22:45:00.000Z'); }); test("full CME: the day's open delay moves the open", () => { const jitterFor = () => 37; assert.equal(iso(nextSessionOpenAt(at('2026-10-24T17:00:00Z'), { ...fullCme, jitterFor })), '2026-10-25T23:22:00.000Z'); }); test('full CME: Friday after the close waits for Sunday', () => { // Fri 2026-10-23 3:30 PM CDT assert.equal(iso(nextSessionOpenAt(at('2026-10-23T20:30:00Z'), fullCme)), '2026-10-25T22:45:00.000Z'); }); test('full CME: the daily break opens at 5:45 PM the same evening', () => { // Mon 2026-10-19 3:00 PM CDT assert.equal(iso(nextSessionOpenAt(at('2026-10-19T20:00:00Z'), fullCme)), '2026-10-19T22:45:00.000Z'); }); test('full CME: an open market is open now (to the minute)', () => { // Tue 2026-10-20 10:00:30 CDT assert.equal(iso(nextSessionOpenAt(at('2026-10-20T15:00:30Z'), fullCme)), '2026-10-20T15:01:00.000Z'); }); test('full CME: every account done for the trade date waits for the next one', () => { // Mon 2026-10-19 11 PM CDT is trade date 10-20; done → Tue 5:45 PM (trade date 10-21). const from = at('2026-10-20T04:00:00Z'); assert.equal(currentTradeDateCT(new Date(from)), '2026-10-20'); const open = nextSessionOpenAt(from, fullCme, '2026-10-20'); assert.equal(iso(open), '2026-10-20T22:45:00.000Z'); assert.equal(currentTradeDateCT(new Date(open)), '2026-10-21'); }); test('full CME: a Friday trade date done waits for Sunday', () => { // Fri 2026-10-23 9 AM CDT, trade date 10-23 done. assert.equal(iso(nextSessionOpenAt(at('2026-10-23T14:00:00Z'), fullCme, '2026-10-23')), '2026-10-25T22:45:00.000Z'); }); test('full CME: across the DST change the open stays 5:45 PM Central', () => { // Sat 2026-10-31 noon CDT → Sun 2026-11-01 5:45 PM CST (23:45Z). assert.equal(iso(nextSessionOpenAt(at('2026-10-31T17:00:00Z'), fullCme)), '2026-11-01T23:45:00.000Z'); }); test('equity hours: Friday afternoon waits for Monday 8:35 AM', () => { assert.equal(iso(nextSessionOpenAt(at('2026-10-23T20:30:00Z'), equity)), '2026-10-26T13:35:00.000Z'); }); test('equity hours: a done day waits for the next morning', () => { // Tue 2026-10-20 10 AM CDT, trade date 10-20 done → Wed 8:35 AM. assert.equal(iso(nextSessionOpenAt(at('2026-10-20T15:00:00Z'), equity, '2026-10-20')), '2026-10-21T13:35:00.000Z'); }); test('every-day firm: open on Saturday; done waits for 5:05 PM CT', () => { const sat = at('2026-10-24T17:00:00Z'); assert.equal(nextSessionOpenAt(sat, everyDay), sat); assert.equal(iso(nextSessionOpenAt(sat, everyDay, '2026-10-24')), '2026-10-24T22:05:00.000Z'); // In CST the firm resets at 4 PM, but entries still reopen at 5:05 PM. assert.equal(iso(nextSessionOpenAt(at('2026-11-07T18:00:00Z'), everyDay, '2026-11-07')), '2026-11-07T23:05:00.000Z'); }); test('nextSessionOpenAt lands on the first open minute, on a later trade date when asked', () => { const rand = rng(7); const start = at('2026-10-18T00:00:00Z'); for (let i = 0; i < 120; i++) { const from = start + Math.floor(rand() * 21 * 24 * HOUR); for (const o of [fullCme, equity, everyDay]) { const after = rand() < 0.5 ? currentTradeDateCT(new Date(from)) : null; const open = nextSessionOpenAt(from, o, after); assert.ok(open !== null && open >= from, `${iso(from)} → ${open}`); const opens = (t) => !isNoTradeTime(new Date(t), { everyDay: o.everyDay, mode: o.mode, jitterMin: 0 }) && (after === null || currentTradeDateCT(new Date(t)) > after); assert.ok(opens(open), `${iso(from)} → ${iso(open)} isn't open`); if (open - MIN >= from) assert.ok(!opens(open - MIN), `${iso(from)} → ${iso(open)} opened earlier`); } } }); // ── planCheckIn / planRest ────────────────────────────────────────────────── test('a check-in comes 3–7 hours on, in the Central daytime, never close to the wake', () => { const rand = rng(11); const start = at('2026-10-19T00:00:00Z'); for (let i = 0; i < 2000; i++) { const now = start + Math.floor(rand() * 14 * 24 * HOUR); const checkIn = planCheckIn(now, null, rand); assert.ok(checkIn !== null); assert.ok(checkIn >= now + CHECK_IN_GAP_MIN_MS, `${iso(now)} → ${iso(checkIn)} too soon`); const minutes = centralMinutes(checkIn); assert.ok(minutes >= 8 * 60 && minutes < 22 * 60, `${iso(checkIn)} is at night`); // Within the gap, or moved to the next morning. if (checkIn > now + CHECK_IN_GAP_MAX_MS) assert.ok(minutes < 11 * 60 + 1, `${iso(checkIn)} moved past the morning`); } const now = at('2026-10-19T15:00:00Z'); // Mon 10 AM CDT assert.equal(planCheckIn(now, now + 4 * HOUR, () => 0), null); // 3h on is within 2h of the wake assert.equal(planCheckIn(now, now + 6 * HOUR, () => 0), now + 3 * HOUR); }); test('a check-in drawn at night moves to 8–11 AM Central', () => { const now = at('2026-10-20T01:00:00Z'); // Mon 8 PM CDT // +5h = 1 AM → next morning 8 AM + half of 3h = 9:30 AM CDT. assert.equal(iso(planCheckIn(now, null, () => 0.5)), '2026-10-20T14:30:00.000Z'); }); test('planRest: logs back in 20–50 min before the open, or not at all when it is too close', () => { const now = at('2026-10-24T17:00:00Z'); const opensAt = at('2026-10-25T22:45:00Z'); const early = planRest(now, opensAt, () => 0); assert.equal(early.wakeAt, opensAt - REST_LEAD_MIN_MS); assert.equal(early.leadMs, REST_LEAD_MIN_MS); const late = planRest(now, opensAt, () => 0.999999); assert.ok(Math.abs(late.wakeAt - (opensAt - REST_LEAD_MAX_MS)) < 1_000); // An open within lead + REST_MIN_MS isn't worth a rest. assert.equal(planRest(now, now + REST_LEAD_MIN_MS + REST_MIN_MS - MIN, () => 0), null); assert.ok(planRest(now, now + REST_LEAD_MIN_MS + REST_MIN_MS, () => 0) !== null); // Nothing scheduled: rests until a command, with a check-in. const open = planRest(now, null, () => 0); assert.equal(open.wakeAt, null); assert.ok(open.checkInAt !== null); }); test('rests drawn for the same open log back in at different times', () => { const rand = rng(3); const opensAt = at('2026-10-25T22:45:00Z'); const wakes = new Set(); for (let i = 0; i < 50; i++) wakes.add(Math.round(planRest(at('2026-10-24T17:00:00Z'), opensAt, rand).wakeAt / MIN)); assert.ok(wakes.size >= 20, `only ${wakes.size} distinct wake minutes`); });