Broker sessions rest while nothing needs them

A firm's client used to stay logged in around the clock, renewing on a fixed
timer overnight and at weekends. Now, once a firm has nothing open, nothing
pending and nothing left to trade before its next session for a random
15–45 minutes, its client disconnects and the dashboard shows it Resting.
It logs back in a random 20–50 minutes before the next session opens, at a
check-in every 3–7 Central daytime hours, or at once when trading starts, a
manual leg, copy trade, manual order or AutoBuyer run needs it. Token
renewals fall at random points (Tradovate 15–45 min before expiry, TopstepX
every 45–150 min). Settings → Trading → Rest idle broker sessions
(session_rest) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-21 13:28:47 -05:00
co-authored by Claude Opus 5.5
parent 6dd2528fe2
commit 5eb0ea7760
18 changed files with 872 additions and 41 deletions
+170
View File
@@ -0,0 +1,170 @@
// 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`);
});