From db0ad99a767804ed502358237ccf5be83d552f5e Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sun, 27 Sep 2026 17:45:08 -0500 Subject: [PATCH] CME evening session: first entry at 5:45 PM CT, not 5:05 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tradovate moves each account's cash record to the new trade date on its own schedule, an idle one perhaps not until it trades, and the bot holds any account still on the old day until 5:45. Opening at 5:05 fired the ready accounts alone (TopstepX, the early rollers) while the rest, released at 5:45, waited an hour or more for them to go flat. A 5:45 open puts every account in the first batch and skips the open's widest spreads. The day's 0–180 minute delay still comes on top: first entries land 5:45–8:45 PM. Every-day firms keep their own 5:05 PM end of the roll guard; the equity hours open is unchanged. The draw's session progress now starts at 5:45. Co-Authored-By: Claude Opus 5.5 --- lib/instrument-specs.ts | 9 +++++---- lib/trading-logic.ts | 24 +++++++++++++++++++----- scripts/check-instruments.mjs | 7 ++++--- scripts/check-trading-logic.mjs | 25 +++++++++++++++---------- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/lib/instrument-specs.ts b/lib/instrument-specs.ts index 5458b9f..03c6d90 100644 --- a/lib/instrument-specs.ts +++ b/lib/instrument-specs.ts @@ -135,8 +135,9 @@ function centralMinutes(now: Date): { day: string; minutes: number } { /** * How far through the trading session `now` is: 0 at the open, 1 at the last - * entry. full_cme runs 17:05 → 14:55 CT the next day; equity_hours 8:35 → - * 14:55. null outside the session. + * entry. full_cme runs 17:45 → 14:55 CT the next day (trading-logic's + * CME_EVENING_OPEN_MIN — no imports here); equity_hours 8:35 → 14:55. null + * outside the session. */ export function sessionProgress(now: Date, mode: string): number | null { const { day, minutes } = centralMinutes(now); @@ -144,8 +145,8 @@ export function sessionProgress(now: Date, mode: string): number | null { if (day === 'Sat' || day === 'Sun' || minutes < 515 || minutes > 895) return null; return (minutes - 515) / (895 - 515); } - const open = 17 * 60 + 5; - const length = 24 * 60 - open + 14 * 60 + 55; // 21h50m + const open = 17 * 60 + 45; + const length = 24 * 60 - open + 14 * 60 + 55; // 21h10m const since = (minutes - open + 1440) % 1440; if (since > length) return null; if (day === 'Sat' || (day === 'Fri' && minutes > 895) || (day === 'Sun' && minutes < open)) return null; diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index a401203..4aadabe 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -1553,11 +1553,25 @@ function centralClock(now: Date): { day: string; minutes: number; resetMinutes: return { day: get('weekday'), minutes, resetMinutes }; } +/** + * The CME evening session's first entry, in Central minutes: 5:45 PM, 45 + * minutes after the 5 PM roll. Tradovate moves each account's cash record to + * the new trade date on its own schedule — an idle one perhaps not until it + * trades — and the bot holds any account still on the old day until 5:45 + * (tradovate-class ROLL_WINDOW_END_MIN). Opening then puts every account in + * the first batch, instead of the ready ones firing alone and the rest + * waiting for them to go flat, and skips the open's widest spreads. + */ +export const CME_EVENING_OPEN_MIN = 17 * 60 + 45; +/** Every-day firms' roll guard ends here (plus the day's delay): they have + * no Tradovate roll to wait for. */ +const EVERY_DAY_OPEN_MIN = 17 * 60 + 5; + /** * Whether no new trade may open right now. * * CME firms follow the trading_hours setting: - * "full_cme" — Sun 5:05 PM – Fri 2:55 PM Central (5 min buffer on each side) + * "full_cme" — Sun 5:45 PM – Fri 2:55 PM Central (see CME_EVENING_OPEN_MIN) * "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 0–180 minute delay. * @@ -1569,11 +1583,11 @@ function centralClock(now: Date): { day: string; minutes: number; resetMinutes: */ export function isNoTradeTime(now: Date, o: { everyDay: boolean; mode: string; jitterMin: number }): boolean { const { day, minutes, resetMinutes } = centralClock(now); - const cmeOpen = 17 * 60 + 5 + o.jitterMin; + const cmeOpen = CME_EVENING_OPEN_MIN + o.jitterMin; if (o.everyDay) { const guardStart = Math.min(resetMinutes, 17 * 60) - ROLL_GUARD_MIN; - return minutes >= guardStart && minutes < cmeOpen; + return minutes >= guardStart && minutes < EVERY_DAY_OPEN_MIN + o.jitterMin; } if (o.mode === 'equity_hours') { @@ -1583,12 +1597,12 @@ export function isNoTradeTime(now: Date, o: { everyDay: boolean; mode: string; j return minutes < open || minutes >= close; } - // Full CME: Sun 5:00 PM – Fri 4:00 PM Central with 5 min buffer. + // Full CME: Sun 5:45 PM (plus the delay) – Fri 2:55 PM Central. if (day === 'Sat') return true; if (day === 'Sun') return minutes < cmeOpen; const cmeClose = 14 * 60 + 55; if (day === 'Fri' && minutes >= cmeClose) return true; - // Mon–Thu: block 2:55 PM – 5:05 PM Central (early stop + daily halt + buffer). + // Mon–Thu: block 2:55 PM – 5:45 PM Central (early stop, daily halt, Tradovate's roll). return minutes >= cmeClose && minutes < cmeOpen; } diff --git a/scripts/check-instruments.mjs b/scripts/check-instruments.mjs index f4c3759..d23c390 100644 --- a/scripts/check-instruments.mjs +++ b/scripts/check-instruments.mjs @@ -63,11 +63,12 @@ test('daily move from hourly closes; contracts; expected hold', () => { assert.equal(expectedHoldHours({ leg: 1000, stopDollars: Infinity, contracts: 1, dailyMove: null }), null); }); -test('session progress: full CME 17:05 → 14:55 CT, equity hours 8:35 → 14:55', () => { +test('session progress: full CME 17:45 → 14:55 CT, equity hours 8:35 → 14:55', () => { // 2026-09-28 is a Monday; CDT is UTC−5. const ct = (iso) => new Date(`${iso}-05:00`); - assert.equal(sessionProgress(ct('2026-09-27T17:05'), 'full_cme'), 0); - assert.ok(Math.abs(sessionProgress(ct('2026-09-28T04:00'), 'full_cme') - 655 / 1310) < 1e-9); + assert.equal(sessionProgress(ct('2026-09-27T17:45'), 'full_cme'), 0); + assert.equal(sessionProgress(ct('2026-09-27T17:30'), 'full_cme'), null); // Sunday before the open + assert.ok(Math.abs(sessionProgress(ct('2026-09-28T04:00'), 'full_cme') - 615 / 1270) < 1e-9); assert.equal(sessionProgress(ct('2026-09-28T14:55'), 'full_cme'), 1); assert.equal(sessionProgress(ct('2026-09-28T15:30'), 'full_cme'), null); // maintenance assert.equal(sessionProgress(ct('2026-09-26T12:00'), 'full_cme'), null); // Saturday diff --git a/scripts/check-trading-logic.mjs b/scripts/check-trading-logic.mjs index 8bb4442..97e8baf 100644 --- a/scripts/check-trading-logic.mjs +++ b/scripts/check-trading-logic.mjs @@ -97,7 +97,7 @@ function oldNoTrade(now, mode, jitter) { return timeMinutes < open || timeMinutes >= close; } if (day === 'Sat') return true; - const cmeOpen = 17 * 60 + 5 + jitter; + const cmeOpen = 17 * 60 + 45 + jitter; if (day === 'Sun') return timeMinutes < cmeOpen; const cmeClose = 14 * 60 + 55; if (day === 'Fri' && timeMinutes >= cmeClose) return true; @@ -113,7 +113,7 @@ function oldAfterClose(now) { return minutes >= 14 * 60 + 55 && minutes < 17 * 60; } -test('CME trading hours are unchanged, across both DST regimes', () => { +test('CME trading hours: 5:45 PM plus the delay to 2:55 PM, across both DST regimes', () => { for (const start of [Date.UTC(2026, 8, 20), Date.UTC(2026, 0, 11)]) { // a September and a January week for (let m = 0; m < 7 * 24 * 60; m += 7) { const now = new Date(start + m * 60_000); @@ -133,16 +133,21 @@ 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', () => { +test('the evening session opens at 5:45 PM CT, and a 180-minute delay makes it 8:45', () => { 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-28T17:45:00'), o), true); // Monday + assert.equal(isNoTradeTime(cdt('2026-09-28T20:44:00'), o), true); + assert.equal(isNoTradeTime(cdt('2026-09-28T20:45:00'), o), false); + assert.equal(isNoTradeTime(cdt('2026-09-27T20:44:00'), o), true); // Sunday too + assert.equal(isNoTradeTime(cdt('2026-09-27T20:45: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); + // No delay: 5:45 PM — not 5:05, while Tradovate rolls the accounts' days. + const now = { ...o, jitterMin: 0 }; + assert.equal(isNoTradeTime(cdt('2026-09-28T17:05:00'), now), true); + assert.equal(isNoTradeTime(cdt('2026-09-28T17:44:00'), now), true); + assert.equal(isNoTradeTime(cdt('2026-09-28T17:45:00'), now), false); + // Every-day firms keep their own 5:05 PM end of the roll guard. + assert.equal(isNoTradeTime(cdt('2026-09-28T17:05:00'), { ...now, everyDay: true }), false); }); test('every-day firms close only around the roll (CDT)', () => {