Session open: a random 0–180 minute delay, kept across restarts
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7866d67676
commit
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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1559,7 +1559,7 @@ function centralClock(now: Date): { day: string; minutes: number; resetMinutes:
|
|||||||
* 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: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)
|
* "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
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ test('CME trading hours are unchanged, across both DST regimes', () => {
|
|||||||
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,18 @@ 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('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)', () => {
|
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