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
+38
-6
@@ -97,6 +97,14 @@ const INBOUND_SILENCE_MS = 150_000;
|
||||
const STUCK_CLOSING_MS = 15_000;
|
||||
const TOKEN_RENEW_CHECK_MS = 2 * 60_000;
|
||||
const TOKEN_RENEW_WINDOW_MS = 15 * 60_000;
|
||||
/** Each token is renewed at a point drawn between these two lengths before
|
||||
* its expiry, so renewals keep no fixed rhythm (TOKEN_RENEW_WINDOW_MS is the
|
||||
* latest one may wait). */
|
||||
const TOKEN_RENEW_EARLIEST_MS = 45 * 60_000;
|
||||
/** With the expiry unknown or untrusted, renew this long after the token
|
||||
* arrived instead (drawn) — well inside Tradovate's 90-minute tokens. */
|
||||
const TOKEN_BLIND_RENEW_MIN_MS = 10 * 60_000;
|
||||
const TOKEN_BLIND_RENEW_MAX_MS = 25 * 60_000;
|
||||
/** A failed renewal this close to expiry logs in afresh instead of waiting. */
|
||||
const TOKEN_RELOGIN_WINDOW_MS = 2 * 60_000;
|
||||
/** A reconnect reuses the current token only if it has at least this long left. */
|
||||
@@ -426,6 +434,8 @@ export class TradovateClient {
|
||||
private workingOrders: { [accountId: number]: number } = {};
|
||||
private livenessInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private renewInFlight = false;
|
||||
/** When the current token is to be renewed (drawn in noteTokenExpiry()). */
|
||||
private renewAt = 0;
|
||||
/** The current token's expirationTime was (nearly) past on arrival: this
|
||||
* machine's clock is off, so the expiry is ignored (noteTokenExpiry()). */
|
||||
private tokenExpiryUntrusted = false;
|
||||
@@ -1843,17 +1853,26 @@ export class TradovateClient {
|
||||
* Tradovate's, not that the token is dead. Trusting that expiry would have
|
||||
* the watchdog "expire" every fresh token and log in again every 15s (a
|
||||
* rate-limit penalty, then a captcha, for every client on this IP).
|
||||
* Treat the expiry as unknown instead: renew every 2 minutes and log in on
|
||||
* reconnect, as the code did before it read the expiry at all.
|
||||
* Treat the expiry as unknown instead: renew 10–25 minutes after each
|
||||
* token arrives (this clock's differences still hold) and log in on
|
||||
* reconnect.
|
||||
*
|
||||
* Also draws when this token is renewed: a random 15–45 minutes before it
|
||||
* expires, so the renewals don't tick like a clock.
|
||||
*/
|
||||
private noteTokenExpiry(): void {
|
||||
const raw = this.accessInfo?.expirationTime;
|
||||
const now = Date.now();
|
||||
const t = raw ? new Date(raw).getTime() : NaN;
|
||||
const untrusted = Number.isFinite(t) && t - Date.now() < TOKEN_RENEW_WINDOW_MS;
|
||||
const untrusted = Number.isFinite(t) && t - now < TOKEN_RENEW_WINDOW_MS;
|
||||
if (untrusted && !this.tokenExpiryUntrusted) {
|
||||
console.warn(`${this.tag} new token expires ${raw}, which is (nearly) past by this machine's clock — check the system clock; ignoring the expiry`);
|
||||
}
|
||||
this.tokenExpiryUntrusted = untrusted;
|
||||
const expiresAt = this.tokenExpiresAt();
|
||||
this.renewAt = expiresAt > 0
|
||||
? expiresAt - TOKEN_RENEW_WINDOW_MS - Math.random() * (TOKEN_RENEW_EARLIEST_MS - TOKEN_RENEW_WINDOW_MS)
|
||||
: now + TOKEN_BLIND_RENEW_MIN_MS + Math.random() * (TOKEN_BLIND_RENEW_MAX_MS - TOKEN_BLIND_RENEW_MIN_MS);
|
||||
}
|
||||
|
||||
/** Holding a token known to stay valid for at least `ms` more. */
|
||||
@@ -1861,13 +1880,13 @@ export class TradovateClient {
|
||||
return !!this.accessInfo?.accessToken && this.tokenExpiresAt() > Date.now() + ms;
|
||||
}
|
||||
|
||||
/** The 2-minute renewal check. Never rejects, never overlaps itself. */
|
||||
/** The 2-minute renewal check: renews once the token's drawn renewal
|
||||
* time (noteTokenExpiry) has passed. Never rejects, never overlaps itself. */
|
||||
private maybeRenewToken(): void {
|
||||
if (this.intentionalDisconnect || this.connecting || this.renewInFlight || !this.accessInfo?.accessToken) return;
|
||||
const now = Date.now();
|
||||
const expiresAt = this.tokenExpiresAt();
|
||||
// An unparseable expiry renews anyway — an extra renewal beats a silent expiry.
|
||||
if (expiresAt > 0 && expiresAt - now > TOKEN_RENEW_WINDOW_MS) return;
|
||||
if (now < this.renewAt) return;
|
||||
// Already expired: renewal can't save it; the watchdog / next connect logs in.
|
||||
if (expiresAt > 0 && expiresAt <= now) return;
|
||||
|
||||
@@ -2278,6 +2297,19 @@ export class TradovateClient {
|
||||
for (const info of watchers) info.cancel?.('client disconnected');
|
||||
this.daysFetchInFlight = false;
|
||||
this.nextDaysFetchRetryAt = 0;
|
||||
// Logged out for good: a snapshot left in the pool (session rest)
|
||||
// must not be able to reach the broker. Blank, not removed — late
|
||||
// readers use accessInfo?.accessToken and see a logged-out client.
|
||||
if (this.accessInfo) this.accessInfo = { ...this.accessInfo, accessToken: '', mdAccessToken: '' };
|
||||
}
|
||||
|
||||
/** See PlatformClient: what the session still waits on besides positions
|
||||
* and working orders. */
|
||||
public restBlocker(): string | null {
|
||||
const pending = Object.keys(this.directEventCallbacks).length + this.indirectEventCallbacks.length;
|
||||
if (pending > 0) return `${pending} request(s) awaiting an answer`;
|
||||
if (this.daysFetchInFlight) return 'a report fetch is underway';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Register a one-time callback for when a fill arrives for a given orderId. */
|
||||
|
||||
Reference in New Issue
Block a user