diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index 6a37e9c..57713df 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -122,11 +122,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 +// 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. +const sessionOpenJitter = new Map(); + +function openJitterMinutes(centralDay: string): number { + let jitter = sessionOpenJitter.get(centralDay); + if (jitter === undefined) { + jitter = 1 + Math.floor(Math.random() * 20); // 1–20 inclusive + 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; +} + /** * Returns true when trading is not allowed based on the trading_hours setting. * * "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) + * + * The open side of each window is pushed back by a random 1–20 minutes per day + * (see openJitterMinutes) so sessions never start at the exact same time. */ function isInNoTradeWindow(): boolean { const now = new Date(); @@ -143,6 +162,10 @@ function isInNoTradeWindow(): boolean { timeZone: 'America/Chicago', }).format(now); // "Sun", "Mon", ... "Sat" const timeMinutes = hour * 60 + minute; // minutes since midnight + const centralDay = new Intl.DateTimeFormat('en-CA', { + timeZone: 'America/Chicago', + }).format(now); // "2026-09-06" + const jitter = openJitterMinutes(centralDay); const mode = getSetting('trading_hours') ?? 'full_cme'; @@ -150,7 +173,7 @@ function isInNoTradeWindow(): boolean { // Equity hours: 8:30 AM – 3:00 PM Central with 5 min buffer = 8:35 AM – 2:55 PM // Weekdays only if (day === 'Sat' || day === 'Sun') return true; - const open = 8 * 60 + 35; // 8:35 AM + const open = 8 * 60 + 35 + jitter; // 8:35 AM + daily jitter const close = 14 * 60 + 55; // 2:55 PM return timeMinutes < open || timeMinutes >= close; } @@ -161,8 +184,8 @@ function isInNoTradeWindow(): boolean { // Saturday — market closed all day if (day === 'Sat') return true; - // Sunday — market opens at 5:05 PM Central - const cmeOpen = 17 * 60 + 5; // 5:05 PM + // Sunday — market opens at 5:05 PM Central (+ daily jitter) + const cmeOpen = 17 * 60 + 5 + jitter; // 5:05 PM + daily jitter if (day === 'Sun') return timeMinutes < cmeOpen; // Friday — stop trading at 2:55 PM Central