Compare commits
2
Commits
7866d67676
...
db0ad99a76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db0ad99a76 | ||
|
|
ff97a59b49 |
+17
-5
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
+20
-6
@@ -1553,13 +1553,27 @@ 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 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,12 +113,12 @@ 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);
|
||||
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,23 @@ 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('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: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: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)', () => {
|
||||
const o = { everyDay: true, mode: 'full_cme', jitterMin: 7 };
|
||||
assert.equal(isNoTradeTime(cdt('2026-09-26T12:00:00'), o), false); // Saturday noon
|
||||
|
||||
Reference in New Issue
Block a user