From ff97a59b496256cc3198cc9d7c61c3464af8da2f Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sun, 27 Sep 2026 17:30:01 -0500 Subject: [PATCH] =?UTF-8?q?Session=20open:=20a=20random=200=E2=80=93180=20?= =?UTF-8?q?minute=20delay,=20kept=20across=20restarts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The day's delay was 1–20 minutes. It is now 0–180, so the evening open lands anywhere from 5:05 to 8:05 PM CT. The day's roll is saved as a setting: with a three-hour window, a restart during the wait (an update, a crash) would otherwise roll again and could start the session early. Co-Authored-By: Claude Opus 5.5 --- lib/auto-trade.ts | 22 +++++++++++++++++----- lib/trading-logic.ts | 2 +- scripts/check-trading-logic.mjs | 14 +++++++++++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index 5a8e471..232fcbf 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -705,18 +705,30 @@ function evaluateAccountForTrade(client: any, acc: { id: number; name: string; a // ── helpers ─────────────────────────────────────────────────────────────────── -// Random 1–20 minute delay added to the session open, picked once per Central +/** The longest random delay added to the session open, in minutes. */ +const MAX_OPEN_DELAY_MIN = 180; + +// Random 0–180 minute delay added to the session open, picked once per Central // calendar day. Re-rolling on every check would move the open boundary between // polls, letting a trade slip through a moment the next check would reject. -// Held in runtime() so every bundle of this module agrees on the day's roll. +// Held in runtime() so every bundle of this module agrees on the day's roll, +// and saved as a setting so a restart during the wait (an update, a crash) +// keeps the day's delay instead of rolling a new one. function openJitterMinutes(centralDay: string): number { const sessionOpenJitter = runtime().sessionOpenJitter; let jitter = sessionOpenJitter.get(centralDay); if (jitter === undefined) { - jitter = 1 + Math.floor(Math.random() * 20); // 1–20 inclusive + const [savedDay, savedMin] = (getSetting('session_open_delay') ?? '').split('|'); + const saved = savedDay === centralDay ? Number(savedMin) : NaN; + if (Number.isInteger(saved) && saved >= 0 && saved <= MAX_OPEN_DELAY_MIN) { + jitter = saved; + } else { + jitter = Math.floor(Math.random() * (MAX_OPEN_DELAY_MIN + 1)); // 0–180 inclusive + setSetting('session_open_delay', `${centralDay}|${jitter}`); + console.log(`[trading-hours] session open delayed ${jitter} min today`); + } sessionOpenJitter.clear(); // only today's entry is ever needed sessionOpenJitter.set(centralDay, jitter); - console.log(`[trading-hours] session open delayed ${jitter} min today`); } return jitter; } @@ -740,7 +752,7 @@ function isAfterSessionClose(firm: { tradesEveryDay?: boolean }, now: Date = new /** * Whether the firm takes no new trades right now (see isNoTradeTime). CME * firms follow the trading_hours setting — "full_cme" or "equity_hours" — with - * the open pushed back by the day's 1–20 minute jitter (openJitterMinutes), so + * the open pushed back by the day's 0–180 minute delay (openJitterMinutes), so * sessions never start at the exact same time. Every-day firms ignore the * setting and only stand down around the daily roll. */ diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index 157758b..a401203 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -1559,7 +1559,7 @@ function centralClock(now: Date): { day: string; minutes: number; resetMinutes: * CME firms follow the trading_hours setting: * "full_cme" — Sun 5:05 PM – Fri 2:55 PM Central (5 min buffer on each side) * "equity_hours" — 8:35 AM – 2:55 PM Central, Mon–Fri (5 min buffer on each side) - * with the open pushed back by the day's 1–20 minute jitter. + * with the open pushed back by the day's 0–180 minute delay. * * Every-day firms ignore trading_hours and close only around the day's roll: * from 5 minutes before the earlier of their 22:00 UTC reset and 5 PM CT, to diff --git a/scripts/check-trading-logic.mjs b/scripts/check-trading-logic.mjs index 34e6654..8bb4442 100644 --- a/scripts/check-trading-logic.mjs +++ b/scripts/check-trading-logic.mjs @@ -118,7 +118,7 @@ test('CME trading hours are unchanged, across both DST regimes', () => { for (let m = 0; m < 7 * 24 * 60; m += 7) { const now = new Date(start + m * 60_000); for (const mode of ['full_cme', 'equity_hours']) { - for (const jitter of [1, 13, 20]) { + for (const jitter of [0, 1, 13, 20, 95, 180]) { assert.equal(isNoTradeTime(now, { everyDay: false, mode, jitterMin: jitter }), oldNoTrade(now, mode, jitter), `${now.toISOString()} ${mode} ${jitter}`); } } @@ -133,6 +133,18 @@ const ct = (iso, offsetHours) => new Date(Date.parse(`${iso}Z`) + offsetHours * const cdt = (iso) => ct(iso, 5); // Central daylight: UTC−5 const cst = (iso) => ct(iso, 6); // Central standard: UTC−6 +test('a 180-minute open delay: the evening session starts at 8:05 PM CT', () => { + const o = { everyDay: false, mode: 'full_cme', jitterMin: 180 }; + assert.equal(isNoTradeTime(cdt('2026-09-28T17:05:00'), o), true); // Monday, the usual open + assert.equal(isNoTradeTime(cdt('2026-09-28T20:04:00'), o), true); + assert.equal(isNoTradeTime(cdt('2026-09-28T20:05:00'), o), false); + assert.equal(isNoTradeTime(cdt('2026-09-27T20:04:00'), o), true); // Sunday too + assert.equal(isNoTradeTime(cdt('2026-09-27T20:05:00'), o), false); + assert.equal(isNoTradeTime(cdt('2026-09-29T10:00:00'), o), false); // the day session is untouched + // No delay: 5:05 PM as always. + assert.equal(isNoTradeTime(cdt('2026-09-28T17:05:00'), { ...o, jitterMin: 0 }), false); +}); + test('every-day firms close only around the roll (CDT)', () => { const o = { everyDay: true, mode: 'full_cme', jitterMin: 7 }; assert.equal(isNoTradeTime(cdt('2026-09-26T12:00:00'), o), false); // Saturday noon