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)', () => {