Broker sessions rest while nothing needs them
A firm's client used to stay logged in around the clock, renewing on a fixed timer overnight and at weekends. Now, once a firm has nothing open, nothing pending and nothing left to trade before its next session for a random 15–45 minutes, its client disconnects and the dashboard shows it Resting. It logs back in a random 20–50 minutes before the next session opens, at a check-in every 3–7 Central daytime hours, or at once when trading starts, a manual leg, copy trade, manual order or AutoBuyer run needs it. Token renewals fall at random points (Tradovate 15–45 min before expiry, TopstepX every 45–150 min). Settings → Trading → Rest idle broker sessions (session_rest) turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6dd2528fe2
commit
5eb0ea7760
@@ -1789,6 +1789,106 @@ export function isPastSessionClose(now: Date, everyDay: boolean): boolean {
|
||||
return minutes >= 14 * 60 + 55 && minutes < 17 * 60;
|
||||
}
|
||||
|
||||
// ── broker session rest ──────────────────────────────────────────────────────
|
||||
//
|
||||
// A firm's broker session is logged out while nothing can trade on it (the
|
||||
// weekend, the daily break, a trade date whose accounts are all done) and
|
||||
// logged back in ahead of the next session. Every wait below is drawn at
|
||||
// random, so neither the logins nor the renewals in between keep a clock.
|
||||
// lib/clients.ts decides when a session is idle; these are the times.
|
||||
|
||||
/** A rested session logs back in this long before its next session opens. */
|
||||
export const REST_LEAD_MIN_MS = 20 * 60_000;
|
||||
export const REST_LEAD_MAX_MS = 50 * 60_000;
|
||||
/** An idle session stays up this long before it rests. */
|
||||
export const REST_LINGER_MIN_MS = 15 * 60_000;
|
||||
export const REST_LINGER_MAX_MS = 45 * 60_000;
|
||||
/** A rest shorter than this isn't worth a fresh login. */
|
||||
export const REST_MIN_MS = 45 * 60_000;
|
||||
/** A long rest is broken by a check-in this long after the last login, in
|
||||
* the Central daytime: it refreshes balances and finds new accounts. */
|
||||
export const CHECK_IN_GAP_MIN_MS = 3 * 60 * 60_000;
|
||||
export const CHECK_IN_GAP_MAX_MS = 7 * 60 * 60_000;
|
||||
const CHECK_IN_DAY_START_MIN = 8 * 60;
|
||||
const CHECK_IN_DAY_END_MIN = 22 * 60;
|
||||
/** A check-in that would fall this close to the wake is left out. */
|
||||
const CHECK_IN_WAKE_MARGIN_MS = 2 * 60 * 60_000;
|
||||
/** How far ahead nextSessionOpenAt looks. */
|
||||
const NEXT_OPEN_HORIZON_MS = 8 * 24 * 60 * 60_000;
|
||||
|
||||
/** A uniform draw in [min, max). */
|
||||
export function drawBetween(min: number, max: number, rand: () => number = Math.random): number {
|
||||
return min + rand() * (max - min);
|
||||
}
|
||||
|
||||
/**
|
||||
* The first whole minute at or after `from` when the firm may open a new
|
||||
* trade (isNoTradeTime false), or null if none within 8 days. With
|
||||
* `afterTradeDate`, only a minute on a later trade date counts: that day's
|
||||
* accounts are all done. `jitterFor` is the open delay for the Central day of
|
||||
* a moment; give 0 for a day whose delay isn't drawn yet, the earliest it
|
||||
* could open.
|
||||
*/
|
||||
export function nextSessionOpenAt(
|
||||
from: number,
|
||||
o: { everyDay: boolean; mode: string; jitterFor: (at: Date) => number },
|
||||
afterTradeDate: string | null = null,
|
||||
): number | null {
|
||||
const opens = (t: number): boolean => {
|
||||
const at = new Date(t);
|
||||
if (afterTradeDate !== null && currentTradeDateCT(at) <= afterTradeDate) return false;
|
||||
return !isNoTradeTime(at, { everyDay: o.everyDay, mode: o.mode, jitterMin: o.jitterFor(at) });
|
||||
};
|
||||
const STEP = 5 * 60_000;
|
||||
let t = Math.ceil(from / 60_000) * 60_000;
|
||||
if (opens(t)) return t;
|
||||
const limit = t + NEXT_OPEN_HORIZON_MS;
|
||||
// Sessions and breaks last hours, so a 5-minute stride can't step over
|
||||
// one; the minute it opened is then found inside the stride.
|
||||
for (; t < limit; t += STEP) {
|
||||
if (!opens(t + STEP)) continue;
|
||||
for (let m = t + 60_000; m < t + STEP; m += 60_000) if (opens(m)) return m;
|
||||
return t + STEP;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a check-in should break a rest that starts `now` and ends at `wakeAt`
|
||||
* (null: no planned end), or null for none: 3–7 hours on, moved to 8–11 AM
|
||||
* Central when that lands at night, and never within 2 hours of the wake.
|
||||
*/
|
||||
export function planCheckIn(now: number, wakeAt: number | null, rand: () => number = Math.random): number | null {
|
||||
let at = now + drawBetween(CHECK_IN_GAP_MIN_MS, CHECK_IN_GAP_MAX_MS, rand);
|
||||
const { minutes } = centralClock(new Date(at));
|
||||
if (minutes < CHECK_IN_DAY_START_MIN || minutes >= CHECK_IN_DAY_END_MIN) {
|
||||
const toMorning = ((CHECK_IN_DAY_START_MIN - minutes + 1440) % 1440) * 60_000;
|
||||
at += toMorning + drawBetween(0, 3 * 60 * 60_000, rand);
|
||||
// A clock change overnight puts the morning an hour off.
|
||||
const landed = centralClock(new Date(at)).minutes;
|
||||
if (landed < CHECK_IN_DAY_START_MIN) at += (CHECK_IN_DAY_START_MIN - landed) * 60_000;
|
||||
}
|
||||
if (wakeAt !== null && at > wakeAt - CHECK_IN_WAKE_MARGIN_MS) return null;
|
||||
return Math.round(at);
|
||||
}
|
||||
|
||||
/**
|
||||
* A rest starting `now` for a session whose next trading opens at `opensAt`
|
||||
* (null: nothing trades until someone starts it): when to log back in, a
|
||||
* check-in on the way, and the lead drawn — or null when the open is too
|
||||
* close for a rest to be worth a fresh login.
|
||||
*/
|
||||
export function planRest(
|
||||
now: number,
|
||||
opensAt: number | null,
|
||||
rand: () => number = Math.random,
|
||||
): { wakeAt: number | null; checkInAt: number | null; leadMs: number } | null {
|
||||
const leadMs = Math.round(drawBetween(REST_LEAD_MIN_MS, REST_LEAD_MAX_MS, rand));
|
||||
const wakeAt = opensAt === null ? null : opensAt - leadMs;
|
||||
if (wakeAt !== null && wakeAt - now < REST_MIN_MS) return null;
|
||||
return { wakeAt, checkInAt: planCheckIn(now, wakeAt, rand), leadMs };
|
||||
}
|
||||
|
||||
/**
|
||||
* A firm's cap on how many of its accounts trade at once, on top of the
|
||||
* machine's own max concurrent accounts: keeps `items` in order, at most
|
||||
|
||||
Reference in New Issue
Block a user