Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db0ad99a76 | ||
|
|
ff97a59b49 |
+17
-5
@@ -705,18 +705,30 @@ function evaluateAccountForTrade(client: any, acc: { id: number; name: string; a
|
|||||||
|
|
||||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
// ── 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
|
// 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.
|
// 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 {
|
function openJitterMinutes(centralDay: string): number {
|
||||||
const sessionOpenJitter = runtime().sessionOpenJitter;
|
const sessionOpenJitter = runtime().sessionOpenJitter;
|
||||||
let jitter = sessionOpenJitter.get(centralDay);
|
let jitter = sessionOpenJitter.get(centralDay);
|
||||||
if (jitter === undefined) {
|
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.clear(); // only today's entry is ever needed
|
||||||
sessionOpenJitter.set(centralDay, jitter);
|
sessionOpenJitter.set(centralDay, jitter);
|
||||||
console.log(`[trading-hours] session open delayed ${jitter} min today`);
|
|
||||||
}
|
}
|
||||||
return jitter;
|
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
|
* Whether the firm takes no new trades right now (see isNoTradeTime). CME
|
||||||
* firms follow the trading_hours setting — "full_cme" or "equity_hours" — with
|
* 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
|
* sessions never start at the exact same time. Every-day firms ignore the
|
||||||
* setting and only stand down around the daily roll.
|
* 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
|
* 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 →
|
* entry. full_cme runs 17:45 → 14:55 CT the next day (trading-logic's
|
||||||
* 14:55. null outside the session.
|
* 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 {
|
export function sessionProgress(now: Date, mode: string): number | null {
|
||||||
const { day, minutes } = centralMinutes(now);
|
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;
|
if (day === 'Sat' || day === 'Sun' || minutes < 515 || minutes > 895) return null;
|
||||||
return (minutes - 515) / (895 - 515);
|
return (minutes - 515) / (895 - 515);
|
||||||
}
|
}
|
||||||
const open = 17 * 60 + 5;
|
const open = 17 * 60 + 45;
|
||||||
const length = 24 * 60 - open + 14 * 60 + 55; // 21h50m
|
const length = 24 * 60 - open + 14 * 60 + 55; // 21h10m
|
||||||
const since = (minutes - open + 1440) % 1440;
|
const since = (minutes - open + 1440) % 1440;
|
||||||
if (since > length) return null;
|
if (since > length) return null;
|
||||||
if (day === 'Sat' || (day === 'Fri' && minutes > 895) || (day === 'Sun' && minutes < open)) 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 };
|
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.
|
* Whether no new trade may open right now.
|
||||||
*
|
*
|
||||||
* CME firms follow the trading_hours setting:
|
* 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)
|
* "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:
|
* 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
|
* 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 {
|
export function isNoTradeTime(now: Date, o: { everyDay: boolean; mode: string; jitterMin: number }): boolean {
|
||||||
const { day, minutes, resetMinutes } = centralClock(now);
|
const { day, minutes, resetMinutes } = centralClock(now);
|
||||||
const cmeOpen = 17 * 60 + 5 + o.jitterMin;
|
const cmeOpen = CME_EVENING_OPEN_MIN + o.jitterMin;
|
||||||
|
|
||||||
if (o.everyDay) {
|
if (o.everyDay) {
|
||||||
const guardStart = Math.min(resetMinutes, 17 * 60) - ROLL_GUARD_MIN;
|
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') {
|
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;
|
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 === 'Sat') return true;
|
||||||
if (day === 'Sun') return minutes < cmeOpen;
|
if (day === 'Sun') return minutes < cmeOpen;
|
||||||
const cmeClose = 14 * 60 + 55;
|
const cmeClose = 14 * 60 + 55;
|
||||||
if (day === 'Fri' && minutes >= cmeClose) return true;
|
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;
|
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);
|
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.
|
// 2026-09-28 is a Monday; CDT is UTC−5.
|
||||||
const ct = (iso) => new Date(`${iso}-05:00`);
|
const ct = (iso) => new Date(`${iso}-05:00`);
|
||||||
assert.equal(sessionProgress(ct('2026-09-27T17:05'), 'full_cme'), 0);
|
assert.equal(sessionProgress(ct('2026-09-27T17:45'), '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: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-28T14:55'), 'full_cme'), 1);
|
||||||
assert.equal(sessionProgress(ct('2026-09-28T15:30'), 'full_cme'), null); // maintenance
|
assert.equal(sessionProgress(ct('2026-09-28T15:30'), 'full_cme'), null); // maintenance
|
||||||
assert.equal(sessionProgress(ct('2026-09-26T12:00'), 'full_cme'), null); // Saturday
|
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;
|
return timeMinutes < open || timeMinutes >= close;
|
||||||
}
|
}
|
||||||
if (day === 'Sat') return true;
|
if (day === 'Sat') return true;
|
||||||
const cmeOpen = 17 * 60 + 5 + jitter;
|
const cmeOpen = 17 * 60 + 45 + jitter;
|
||||||
if (day === 'Sun') return timeMinutes < cmeOpen;
|
if (day === 'Sun') return timeMinutes < cmeOpen;
|
||||||
const cmeClose = 14 * 60 + 55;
|
const cmeClose = 14 * 60 + 55;
|
||||||
if (day === 'Fri' && timeMinutes >= cmeClose) return true;
|
if (day === 'Fri' && timeMinutes >= cmeClose) return true;
|
||||||
@@ -113,12 +113,12 @@ function oldAfterClose(now) {
|
|||||||
return minutes >= 14 * 60 + 55 && minutes < 17 * 60;
|
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 (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) {
|
for (let m = 0; m < 7 * 24 * 60; m += 7) {
|
||||||
const now = new Date(start + m * 60_000);
|
const now = new Date(start + m * 60_000);
|
||||||
for (const mode of ['full_cme', 'equity_hours']) {
|
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}`);
|
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 cdt = (iso) => ct(iso, 5); // Central daylight: UTC−5
|
||||||
const cst = (iso) => ct(iso, 6); // Central standard: UTC−6
|
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)', () => {
|
test('every-day firms close only around the roll (CDT)', () => {
|
||||||
const o = { everyDay: true, mode: 'full_cme', jitterMin: 7 };
|
const o = { everyDay: true, mode: 'full_cme', jitterMin: 7 };
|
||||||
assert.equal(isNoTradeTime(cdt('2026-09-26T12:00:00'), o), false); // Saturday noon
|
assert.equal(isNoTradeTime(cdt('2026-09-26T12:00:00'), o), false); // Saturday noon
|
||||||
|
|||||||
Reference in New Issue
Block a user