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:
co-authored by
Claude Opus 5.5
parent
6dd2528fe2
commit
5eb0ea7760
@@ -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`);
|
||||
});
|
||||
Reference in New Issue
Block a user