WebSocket Connections

Real-time data is critical for modern trading applications. The Tradovate Partner API provides robust WebSocket connections for streaming user data, position updates, order status changes, and risk alerts.

Overview

WebSocket connections offer several advantages over REST endpoints for real-time data:

  • Low Latency - Direct TCP connection with minimal overhead
  • Bidirectional - Server can push data without client polling
  • Efficient - Single persistent connection for multiple data streams
  • Real-time - Immediate delivery of critical trading events

Connection Limits

Tradovate limits your simultaneous WebSocket usage in a few ways. When you exceed a limit, the server closes your oldest connection and revokes its token to make room for the new one.

LimitDefaultApplies to
Total connections per user50All WebSocket connections
Distinct devices (organization administrators)15Market-data connections
Distinct IP addresses5Market-data connections

Before closing a connection to enforce a limit, the server sends a shutdown event with a reasonCode (ConnectionQuotaReached, DeviceQuotaReached, IPQuotaReached, or Maintenance). Log these events so you can distinguish a deliberate server close from a network drop.

Disconnect Behavior

The server never sends a WebSocket close status code, so any server-initiated disconnect reaches your client as either 1005 (a graceful close, such as a quota or maintenance disconnect, where the server sent an empty close frame) or 1006 (an abrupt close, such as an idle timeout or network drop, where no close frame was sent). The server drops any connection that sends nothing for 15 seconds.

The reliable way to tell a deliberate disconnect from a network issue is the shutdown event: a quota or policy disconnect is always preceded by one carrying a reasonCode. If a close was preceded by a shutdown event, it was deliberate. If not, treat it as a heartbeat, idle, or network issue and reconnect.

Connection Management

Step 1: Define the WebSocket Client

First, we will set up our WebSocket client class. This class will handle the connection to the WebSocket server, the authentication, reconnection logic, and the sending and receiving of messages from the Tradovate WebSocket server.

The client class will use helper classes called TokenManager and ReconnectionManager to handle access token management and automatic reconnection with exponential backoff, along with some other helper .ts files for configuration and types.

View TokenManager.ts Source Code
// TokenManager.ts
import fs from "fs/promises";
import fetch from "node-fetch";
import {
TRADOVATE_CONFIG,
ENDPOINTS,
TOKEN_REFRESH_INTERVAL,
TOKEN_STORAGE_PATH,
} from "./config.js";
import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";
export class TokenManager {
private tokenInfo: StoredTokenInfo | null = null;
private refreshTimer: NodeJS.Timeout | null = null;
constructor() {
this.loadTokenFromFile();
}
/**
* Get a valid access token, refreshing if necessary
*/
async getAccessToken(): Promise<string> {
// Check if we have a valid token
if (this.tokenInfo && this.isTokenValid()) {
return this.tokenInfo.accessToken;
}
// Get a new token
console.log("๐Ÿ”„ Getting new access token...");
await this.fetchNewToken();
if (!this.tokenInfo) {
throw new Error("Failed to get access token");
}
return this.tokenInfo.accessToken;
}
/**
* Get user ID from stored token info
*/
getUserId(): number {
if (!this.tokenInfo) {
throw new Error("No token info available. Please get access token first.");
}
return this.tokenInfo.userId;
}
/**
* Check if current token is valid (not expired)
*/
private isTokenValid(): boolean {
if (!this.tokenInfo) {
return false;
}
const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();
const currentTime = Date.now();
const bufferTime = 5 * 60 * 1000; // 5 minute buffer
return currentTime < expirationTime - bufferTime;
}
/**
* Fetch a new access token from the API
*/
private async fetchNewToken(): Promise<void> {
const authRequest: AuthRequest = {
name: TRADOVATE_CONFIG.name,
password: TRADOVATE_CONFIG.password,
appId: TRADOVATE_CONFIG.appId,
appVersion: TRADOVATE_CONFIG.appVersion,
sec: TRADOVATE_CONFIG.sec,
cid: TRADOVATE_CONFIG.cid,
};
try {
console.log(`๐Ÿ“ก Making auth request to: ${ENDPOINTS.AUTH_URL}`);
const response = await fetch(ENDPOINTS.AUTH_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(authRequest),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Auth request failed: ${response.status} ${response.statusText} - ${errorText}`
);
}
const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;
// Store with retrieval timestamp
this.tokenInfo = {
...tokenResponse,
retrievedAt: Date.now(),
};
// Save to file
await this.saveTokenToFile();
// Setup auto-refresh
this.setupTokenRefresh();
console.log("โœ… Access token retrieved successfully");
console.log(`๐Ÿ‘ค User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);
console.log(`๐Ÿข Organization: ${this.tokenInfo.orgName}`);
console.log(`โฐ Expires: ${this.tokenInfo.expirationTime}`);
} catch (error) {
console.error("โŒ Failed to fetch access token:", error);
throw error;
}
}
/**
* Load token from storage file
*/
private async loadTokenFromFile(): Promise<void> {
try {
const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");
this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;
console.log("โœ… Loaded access token from storage");
// Setup refresh timer if token is still valid
if (this.isTokenValid()) {
this.setupTokenRefresh();
}
} catch (error) {
console.log("๐Ÿ“ No existing token file found or invalid token");
this.tokenInfo = null;
}
}
/**
* Save token to storage file
*/
private async saveTokenToFile(): Promise<void> {
if (!this.tokenInfo) {
return;
}
try {
await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));
console.log("๐Ÿ’พ Access token saved to storage");
} catch (error) {
console.error("โŒ Failed to save token to file:", error);
}
}
/**
* Setup automatic token refresh
*/
private setupTokenRefresh(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
console.log(`โฐ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);
this.refreshTimer = setTimeout(async () => {
console.log("๐Ÿ”„ Refreshing access token...");
try {
await this.fetchNewToken();
console.log("โœ… Token refreshed successfully");
} catch (error) {
console.error("โŒ Failed to refresh token:", error);
}
}, TOKEN_REFRESH_INTERVAL);
}
/**
* Clear the refresh timer
*/
public cleanup(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
console.log("๐Ÿงน Token refresh timer cleaned up");
}
}
}
View config.ts Source Code
// config.ts
import dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files
//The .env file for this sample is located in the root of the project, and looks like this:
// TRADOVATE_NAME="your_username"
// TRADOVATE_PASSWORD="your_password"
// TRADOVATE_APP_ID="your_app_id"
// TRADOVATE_APP_VERSION=1.0
// TRADOVATE_SEC="your_secret_key"
// TRADOVATE_CID=0
// Load environment variables from .env file
dotenv.config();
export interface TradovateCredentials {
name: string;
password: string;
appId: string;
appVersion: string;
sec: string;
cid: number;
}
// Load credentials from environment variables
export const TRADOVATE_CONFIG: TradovateCredentials = {
name: process.env.TRADOVATE_NAME || "",
password: process.env.TRADOVATE_PASSWORD || "",
appId: process.env.TRADOVATE_APP_ID || "",
appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",
sec: process.env.TRADOVATE_SEC || "",
cid: parseInt(process.env.TRADOVATE_CID || "0", 10),
};
export const ENDPOINTS = {
AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for development
WS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",
} as const;
export const TOKEN_REFRESH_INTERVAL = 75 * 60 * 1000; // 75 minutes in milliseconds (keep below the token's expirationTime)
export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
// types.ts
export interface AccessTokenResponse {
accessToken: string;
mdAccessToken: string;
expirationTime: string;
userStatus: string;
userId: number;
name: string;
hasLive: boolean;
hasSimPlus: boolean;
hasFunded: boolean;
hasMarketData: boolean;
outdatedLiquidationPolicy: boolean;
outdatedSentimentPolicy: boolean;
experience: string;
orgName: string;
}
export interface StoredTokenInfo extends AccessTokenResponse {
retrievedAt: number; // timestamp when token was retrieved
}
export interface AuthRequest {
name: string;
password: string;
appId: string;
appVersion: string;
sec: string;
cid: number;
}
export interface SocketMessage {
i?: number; // request ID
s?: number; // status code
e?: SocketEventName;
d?: any; // data payload
}
export type SocketEventName = "props" | "md" | "clock" | "shutdown";
export type PluralEntityName = string; // This would be dynamically generated from EntityName
export interface TdvCache {
[key: string]: any[];
}
export type MessageEventHandler = (data: SocketMessage[]) => void;
View reconnection-manager.ts Source Code
// reconnection-manager.ts
/**
* ReconnectionManager
*
* Handles automatic reconnection logic with exponential backoff for WebSocket connections.
*/
export class ReconnectionManager {
private reconnectAttempts: number = 0;
private readonly maxReconnectAttempts: number;
private readonly initialReconnectDelay: number;
private readonly maxReconnectDelay: number;
private reconnectTimer: NodeJS.Timeout | null = null;
private shouldReconnect: boolean = true;
private isReconnecting: boolean = false;
private onReconnectCallback: (() => Promise<void>) | null = null;
private onMaxAttemptsReached: (() => void) | null = null;
constructor(
options: {
maxReconnectAttempts?: number;
initialReconnectDelay?: number;
maxReconnectDelay?: number;
} = {}
) {
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
this.initialReconnectDelay = options.initialReconnectDelay ?? 1000; // 1 second
this.maxReconnectDelay = options.maxReconnectDelay ?? 60000; // 60 seconds
}
/**
* Set the callback to execute when attempting to reconnect
*/
public setOnReconnect(callback: () => Promise<void>): void {
this.onReconnectCallback = callback;
}
/**
* Set the callback to execute when max reconnection attempts are reached
*/
public setOnMaxAttemptsReached(callback: () => void): void {
this.onMaxAttemptsReached = callback;
}
/**
* Get the current reconnection state
*/
public getState(): {
isReconnecting: boolean;
reconnectAttempts: number;
shouldReconnect: boolean;
} {
return {
isReconnecting: this.isReconnecting,
reconnectAttempts: this.reconnectAttempts,
shouldReconnect: this.shouldReconnect,
};
}
/**
* Enable automatic reconnection
*/
public enable(): void {
this.shouldReconnect = true;
}
/**
* Disable automatic reconnection and cancel any pending attempts
*/
public disable(): void {
this.shouldReconnect = false;
this.cancel();
}
/**
* Reset the reconnection counter (useful after successful connection)
*/
public reset(): void {
this.reconnectAttempts = 0;
this.isReconnecting = false;
}
/**
* Calculate exponential backoff delay
* Formula: min(initialDelay * 2^attempt, maxDelay) + jitter
*/
private calculateBackoffDelay(): number {
const exponentialDelay = this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts);
const cappedDelay = Math.min(exponentialDelay, this.maxReconnectDelay);
// Add random jitter (0-10% of delay) to avoid thundering herd
const jitter = Math.random() * cappedDelay * 0.1;
return Math.floor(cappedDelay + jitter);
}
/**
* Schedule a reconnection attempt with exponential backoff
*/
public schedule(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error(
`โŒ Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`
);
if (this.onMaxAttemptsReached) {
this.onMaxAttemptsReached();
}
return;
}
const delay = this.calculateBackoffDelay();
console.log(
`๐Ÿ”„ Scheduling reconnection attempt ${this.reconnectAttempts + 1}/${
this.maxReconnectAttempts
} in ${delay}ms...`
);
this.reconnectTimer = setTimeout(() => {
this.attempt();
}, delay);
}
/**
* Attempt to reconnect immediately
*/
public async attempt(): Promise<void> {
if (!this.shouldReconnect) {
console.log("โน๏ธ Reconnection cancelled - reconnection disabled");
return;
}
if (!this.onReconnectCallback) {
console.error("โŒ No reconnection callback set");
return;
}
this.reconnectAttempts++;
this.isReconnecting = true;
console.log(
`๐Ÿ”„ Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`
);
try {
await this.onReconnectCallback();
// If successful, reset reconnection counter
this.reconnectAttempts = 0;
this.isReconnecting = false;
console.log("โœ… Reconnection successful!");
} catch (error) {
console.error(
`โŒ Reconnection attempt ${this.reconnectAttempts} failed:`,
(error as Error).message
);
this.isReconnecting = false;
// Schedule next attempt
this.schedule();
}
}
/**
* Cancel any pending reconnection attempts
*/
public cancel(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* Cleanup resources
*/
public cleanup(): void {
this.cancel();
this.onReconnectCallback = null;
this.onMaxAttemptsReached = null;
}
}

Initial boilerplate code for our WebSocket client:

// index.ts
import WebSocket from "ws";
import readline from "readline";
import { TokenManager } from "./token-manager.js";
import { ReconnectionManager } from "./reconnection-manager.js";
import { ENDPOINTS } from "./config.js";
import { SocketMessage } from "./types.js";
export class TradovateWebSocketClient {
private ws: WebSocket | null = null;
private tokenManager: TokenManager;
private reconnectionManager: ReconnectionManager;
private isAuthenticated: boolean = false;
private authenticationSent: boolean = false;
private heartbeatTimer: NodeJS.Timeout | null = null;
private readonly heartbeatInterval: number = 2500; // 2.5 seconds
private heartbeatsSent: number = 0;
private lastServerMessageTime: number = Date.now();
private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connection
private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;
private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)
private syncCompleted: boolean = false;
constructor() {
this.tokenManager = new TokenManager();
this.reconnectionManager = new ReconnectionManager({
maxReconnectAttempts: 10,
initialReconnectDelay: 1000, // 1 second
maxReconnectDelay: 60000, // 60 seconds
});
// Setup reconnection callbacks
this.reconnectionManager.setOnReconnect(async () => {
await this.handleReconnect();
});
this.reconnectionManager.setOnMaxAttemptsReached(() => {
this.cleanup();
});
}
async start(): Promise<void> {}
private async connectWebSocket(): Promise<void> {
return new Promise((resolve, reject) => {});
}
private async authenticate(): Promise<void> {}
private handleMessage(data: WebSocket.Data): void {}
private startHeartbeat(): void {}
private stopHeartbeat(): void {}
private resetHeartbeatTimer(): void {}
private sendHeartbeat(): void {}
private resetConnectionState(): void {}
private async handleReconnect(): Promise<void> {}
private cleanup(): void {}
}
// Handle unhandled rejections and errors
process.on("unhandledRejection", (reason, promise) => {
console.error("โš ๏ธ Unhandled Rejection:", reason);
// Don't exit, let reconnection logic handle it
});
process.on("uncaughtException", (error) => {
console.error("โš ๏ธ Uncaught Exception:", error.message);
// Don't exit, let reconnection logic handle it
});
// Handle process termination
process.on("SIGINT", () => {
console.log("\n๐Ÿ›‘ Received SIGINT, shutting down gracefully...");
process.exit(0);
});
process.on("SIGTERM", () => {
console.log("\n๐Ÿ›‘ Received SIGTERM, shutting down gracefully...");
process.exit(0);
});
// Start the application
const client = new TradovateWebSocketClient();
client.start().catch((error) => {
console.error("๐Ÿ’ฅ Fatal error:", error.message);
process.exit(1);
});

Step 2: Define our WebSocket Client Methods for basic functionality


start

The start method initializes the WebSocket client, obtains an access token, and establishes the WebSocket connection.

View start Source Code
async start(): Promise<void> {
console.log('๐Ÿš€ Tradovate WebSocket Client - TypeScript Version');
console.log('='.repeat(50));
try {
// Get access token (will fetch new one if needed)
await this.tokenManager.getAccessToken();
// Connect to WebSocket
await this.connectWebSocket();
} catch (error) {
console.error('โŒ Error starting client:', (error as Error).message);
this.cleanup();
}
}

connectWebSocket

The connectWebSocket method will connect to the WebSocket server and authenticate the client.

View connectWebSocket Source Code
private async connectWebSocket(): Promise<void> {
return new Promise((resolve, reject) => {
console.log('๐Ÿ”Œ Connecting to WebSocket...');
console.log(`๐Ÿ“ก URL: ${ENDPOINTS.WS_URL}`);
this.ws = new WebSocket(ENDPOINTS.WS_URL);
let settled = false; // Track if promise has been settled
const handleError = (error: Error) => {
console.error('โŒ WebSocket error:', error.message);
if (!settled) {
settled = true;
reject(error);
}
};
// Attach error handler
this.ws.on('error', handleError);
this.ws.on('open', () => {
console.log('โœ… WebSocket connection established');
this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connection
if (!settled) {
settled = true;
resolve();
}
});
this.ws.on('message', (data) => {
this.handleMessage(data);
});
this.ws.on('close', (code, reason) => {
console.log(`๐Ÿ”Œ WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);
this.resetConnectionState();
// Trigger reconnection if enabled
const reconnectState = this.reconnectionManager.getState();
if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {
this.reconnectionManager.schedule();
}
});
// Set connection timeout
setTimeout(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
if (!settled) {
settled = true;
reject(new Error('WebSocket connection timeout'));
}
}
}, 10000);
});
}

authenticate

The authenticate method will authenticate the client with the WebSocket server using our access token.

View authenticate Source Code
private async authenticate(): Promise<void> {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.error('โŒ WebSocket is not connected');
return;
}
if (this.authenticationSent) {
console.log('โญ๏ธ Authentication already sent, skipping');
return;
}
console.log('๐Ÿ” Preparing authentication message...');
try {
const accessToken = await this.tokenManager.getAccessToken();
// Correct Tradovate format: authorize\n[request ID]\n\n[access token]
const requestId = 0;
const authMessage = `authorize\n${requestId}\n\n${accessToken}`;
console.log('๐Ÿ“ค Sending authentication message...');
this.ws.send(authMessage);
this.authenticationSent = true;
console.log('โœ… Authentication message sent to WebSocket');
} catch (error) {
console.error('โŒ Failed to get access token for authentication:', error);
}
}

handleMessage

The handleMessage method will handle the incoming messages from the WebSocket server.

View handleMessage Source Code
private handleMessage(data: WebSocket.Data): void {
const rawMessage = data.toString();
// Update last server message timestamp
this.lastServerMessageTime = Date.now();
// Handle heartbeat messages (single character messages like "o")
if (rawMessage.length === 1) {
console.log('๐Ÿ’“ Received frame:', rawMessage);
// If this is the first 'o' and we haven't authenticated yet, authenticate immediately
if (rawMessage === 'o' && !this.authenticationSent) {
console.log('๐Ÿš€ Received server open frame - authenticating immediately');
this.authenticate();
return;
}
// Handle subsequent heartbeats only if authenticated
if (rawMessage === 'o' && this.isAuthenticated) {
this.sendHeartbeat();
this.resetHeartbeatTimer();
} else if (rawMessage === 'o' && !this.isAuthenticated) {
console.log('๐Ÿ’“ Heartbeat received but authentication still pending');
}
return;
}
// Handle messages with 'a' prefix (array responses)
if (rawMessage.startsWith('a[')) {
try {
// Extract JSON array from 'a[...]' format
const jsonPart = rawMessage.substring(1); // Remove 'a' prefix
const messageArray: SocketMessage[] = JSON.parse(jsonPart);
console.log('๐Ÿ“จ Received array response:', JSON.stringify(messageArray, null, 2));
// Check if this is specifically an authentication response (request ID 0)
if (Array.isArray(messageArray) && messageArray.length > 0) {
const response = messageArray[0];
// Authentication response has request ID 0
if (response.i === 0 && !this.isAuthenticated) {
if (response.s === 200) {
this.isAuthenticated = true;
console.log('โœ… Authentication successful!');
// Start heartbeat timer after successful authentication
this.startHeartbeat();
} else {
console.error('โŒ Authentication failed:', response);
this.cleanup();
}
}
// Other responses (auth/me, etc.) with different request IDs
else if (response.i && response.i > 1) {
console.log(`๐Ÿ“ฌ Response for request ID ${response.i}:`, response);
}
}
} catch (error) {
console.error('โŒ Error parsing array response:', (error as Error).message);
console.log('Raw message:', rawMessage);
}
return;
}
// Handle regular JSON messages
try {
const message: SocketMessage = JSON.parse(rawMessage);
console.log('๐Ÿ“จ Received JSON message:', JSON.stringify(message, null, 2));
} catch (error) {
console.log('๐Ÿ“ฆ Non-JSON message received:', rawMessage);
// Don't treat non-JSON messages as errors, just log them
}
}

startHeartbeat

The startHeartbeat method starts the heartbeat timer and timeout checker.

View startHeartbeat Source Code
private startHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
console.log('๐Ÿ’“ Starting heartbeat timer (2.5s interval) after authentication');
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatInterval);
// Start the timeout checker
this.startHeartbeatTimeoutChecker();
}

stopHeartbeat

The stopHeartbeat method will stop the heartbeats and timeout checker.

View stopHeartbeat Source Code
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
console.log('๐Ÿ’“ Heartbeat timer stopped');
}
// Stop the timeout checker
this.stopHeartbeatTimeoutChecker();
}

resetHeartbeatTimer

The resetHeartbeatTimer method will reset the heartbeat timer.

View resetHeartbeatTimer Source Code
private resetHeartbeatTimer(): void {
// Reset the timer when we receive a heartbeat from server
this.stopHeartbeat();
this.startHeartbeat();
}

sendHeartbeat

The sendHeartbeat method will send the heartbeat message.

View sendHeartbeat Source Code
private sendHeartbeat(): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send('[]');
this.heartbeatsSent++;
console.log(`๐Ÿ’“ Empty frame heartbeat #${this.heartbeatsSent} sent: []`);
}
}

startHeartbeatTimeoutChecker

The startHeartbeatTimeoutChecker method checks for server timeouts every 5 seconds.

View startHeartbeatTimeoutChecker Source Code
private startHeartbeatTimeoutChecker(): void {
if (this.heartbeatTimeoutTimer) {
clearInterval(this.heartbeatTimeoutTimer);
}
// Check every 5 seconds if we've heard from the server recently
this.heartbeatTimeoutTimer = setInterval(() => {
this.checkServerTimeout();
}, 5000);
}

stopHeartbeatTimeoutChecker

The stopHeartbeatTimeoutChecker method stops the timeout checker.

View stopHeartbeatTimeoutChecker Source Code
private stopHeartbeatTimeoutChecker(): void {
if (this.heartbeatTimeoutTimer) {
clearInterval(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}

checkServerTimeout

The checkServerTimeout method checks if the server has stopped responding and forces reconnection if needed.

View checkServerTimeout Source Code
private checkServerTimeout(): void {
const timeSinceLastMessage = Date.now() - this.lastServerMessageTime;
if (timeSinceLastMessage > this.serverTimeoutMs) {
console.error(`โš ๏ธ No server response for ${timeSinceLastMessage}ms (threshold: ${this.serverTimeoutMs}ms)`);
console.error('๐Ÿ’€ Connection appears dead, forcing reconnection...');
// Force close the connection to trigger reconnection logic
// Use 1000 (normal closure) or 4000-4999 (custom application codes)
if (this.ws) {
this.ws.close(4000, 'Server timeout - no heartbeat response');
}
}
}

resetConnectionState

The resetConnectionState method resets connection-related state when connection is lost.

View resetConnectionState Source Code
private resetConnectionState(): void {
this.isAuthenticated = false;
this.authenticationSent = false;
this.stopHeartbeat();
this.lastServerMessageTime = Date.now(); // Reset to avoid false timeout on reconnect
}

handleReconnect

The handleReconnect method handles reconnection, called by ReconnectionManager.

View handleReconnect Source Code
private async handleReconnect(): Promise<void> {
// Ensure old connection is cleaned up
if (this.ws) {
this.ws.removeAllListeners();
// Only close if the connection is fully established (OPEN state)
// Don't call close() on CONNECTING, CLOSING, or CLOSED states
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.close(1000, 'Reconnecting');
}
this.ws = null;
}
// Get fresh access token if needed
await this.tokenManager.getAccessToken();
// Attempt to reconnect
await this.connectWebSocket();
}

getConnectionStatus

The getConnectionStatus public method returns the current connection status.

View getConnectionStatus Source Code
public getConnectionStatus(): {
connected: boolean;
authenticated: boolean;
reconnecting: boolean;
reconnectAttempts: number;
} {
const reconnectState = this.reconnectionManager.getState();
return {
connected: this.ws?.readyState === WebSocket.OPEN,
authenticated: this.isAuthenticated,
reconnecting: reconnectState.isReconnecting,
reconnectAttempts: reconnectState.reconnectAttempts
};
}

disconnect

The disconnect public method manually disconnects from the WebSocket (will not auto-reconnect).

View disconnect Source Code
public disconnect(): void {
console.log('๐Ÿ”Œ Manual disconnect requested');
this.reconnectionManager.disable();
this.stopHeartbeat();
if (this.ws) {
this.ws.close(1000, 'Manual disconnect');
}
}

reconnect

The reconnect public method manually reconnects to the WebSocket.

View reconnect Source Code
public async reconnect(): Promise<void> {
console.log('๐Ÿ”„ Manual reconnect requested');
this.reconnectionManager.enable();
this.reconnectionManager.reset();
await this.reconnectionManager.attempt();
}

cleanup

The cleanup method will cleanup the client.

View cleanup Source Code
private cleanup(): void {
// Disable reconnection before cleanup
this.reconnectionManager.disable();
this.reconnectionManager.cleanup();
this.stopHeartbeat();
this.tokenManager.cleanup();
if (this.ws) {
this.ws.close();
}
console.log('๐Ÿงน Cleanup completed');
process.exit(0);
}

Sending Messages

We can send messages to the WebSocket server to call various endpoints. Any endpoint that is available in the REST API is also available in the WebSocket API.

One important endpoint that is only available in the WebSocket API and not in the REST API is user/syncrequest. For a detailed explanation of how user/syncrequest works, see the user/syncrequest guide. We will implement user/syncrequest in this page.

send

Add the send method to the TradovateWebSocketClient class.

This will allow us to call API endpoints through our websocket connection.

View send Source Code
/**
* Send a message to the WebSocket server
* @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')
* @param body - Optional request body (will be JSON stringified if object)
* @param requestId - Optional request ID (will auto-increment if not provided)
* @returns The request ID used for this message
*/
public send(endpoint: string, body?: any, requestId?: number): number {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.error('โŒ WebSocket is not connected');
throw new Error('WebSocket is not connected');
}
if (!this.isAuthenticated) {
console.error('โŒ WebSocket is not authenticated yet');
throw new Error('WebSocket is not authenticated yet');
}
// Use provided requestId or auto-increment
const reqId = requestId !== undefined ? requestId : this.requestIdCounter++;
// Format message according to Tradovate protocol: endpoint\nrequestId\n\nbody
let message = `${endpoint}\n${reqId}\n\n`;
if (body !== undefined) {
// If body is an object, stringify it; otherwise use as-is
message += typeof body === 'object' ? JSON.stringify(body) : body;
}
console.log(`๐Ÿ“ค Sending message to ${endpoint} (Request ID: ${reqId})`);
this.ws.send(message);
return reqId;
}

For example, to call the account/list endpoint, we can do the following:

this.send("account/list");

The Full Code

Here is the complete index.ts file with all methods implemented:

View TokenManager.ts Source Code
// TokenManager.ts
import fs from "fs/promises";
import fetch from "node-fetch";
import {
TRADOVATE_CONFIG,
ENDPOINTS,
TOKEN_REFRESH_INTERVAL,
TOKEN_STORAGE_PATH,
} from "./config.js";
import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";
export class TokenManager {
private tokenInfo: StoredTokenInfo | null = null;
private refreshTimer: NodeJS.Timeout | null = null;
constructor() {
this.loadTokenFromFile();
}
/**
* Get a valid access token, refreshing if necessary
*/
async getAccessToken(): Promise<string> {
// Check if we have a valid token
if (this.tokenInfo && this.isTokenValid()) {
return this.tokenInfo.accessToken;
}
// Get a new token
console.log("๐Ÿ”„ Getting new access token...");
await this.fetchNewToken();
if (!this.tokenInfo) {
throw new Error("Failed to get access token");
}
return this.tokenInfo.accessToken;
}
/**
* Get user ID from stored token info
*/
getUserId(): number {
if (!this.tokenInfo) {
throw new Error("No token info available. Please get access token first.");
}
return this.tokenInfo.userId;
}
/**
* Check if current token is valid (not expired)
*/
private isTokenValid(): boolean {
if (!this.tokenInfo) {
return false;
}
const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();
const currentTime = Date.now();
const bufferTime = 5 * 60 * 1000; // 5 minute buffer
return currentTime < expirationTime - bufferTime;
}
/**
* Fetch a new access token from the API
*/
private async fetchNewToken(): Promise<void> {
const authRequest: AuthRequest = {
name: TRADOVATE_CONFIG.name,
password: TRADOVATE_CONFIG.password,
appId: TRADOVATE_CONFIG.appId,
appVersion: TRADOVATE_CONFIG.appVersion,
sec: TRADOVATE_CONFIG.sec,
cid: TRADOVATE_CONFIG.cid,
};
try {
console.log(`๐Ÿ“ก Making auth request to: ${ENDPOINTS.AUTH_URL}`);
const response = await fetch(ENDPOINTS.AUTH_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(authRequest),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Auth request failed: ${response.status} ${response.statusText} - ${errorText}`
);
}
const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;
// Store with retrieval timestamp
this.tokenInfo = {
...tokenResponse,
retrievedAt: Date.now(),
};
// Save to file
await this.saveTokenToFile();
// Setup auto-refresh
this.setupTokenRefresh();
console.log("โœ… Access token retrieved successfully");
console.log(`๐Ÿ‘ค User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);
console.log(`๐Ÿข Organization: ${this.tokenInfo.orgName}`);
console.log(`โฐ Expires: ${this.tokenInfo.expirationTime}`);
} catch (error) {
console.error("โŒ Failed to fetch access token:", error);
throw error;
}
}
/**
* Load token from storage file
*/
private async loadTokenFromFile(): Promise<void> {
try {
const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");
this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;
console.log("โœ… Loaded access token from storage");
// Setup refresh timer if token is still valid
if (this.isTokenValid()) {
this.setupTokenRefresh();
}
} catch (error) {
console.log("๐Ÿ“ No existing token file found or invalid token");
this.tokenInfo = null;
}
}
/**
* Save token to storage file
*/
private async saveTokenToFile(): Promise<void> {
if (!this.tokenInfo) {
return;
}
try {
await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));
console.log("๐Ÿ’พ Access token saved to storage");
} catch (error) {
console.error("โŒ Failed to save token to file:", error);
}
}
/**
* Setup automatic token refresh
*/
private setupTokenRefresh(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
console.log(`โฐ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);
this.refreshTimer = setTimeout(async () => {
console.log("๐Ÿ”„ Refreshing access token...");
try {
await this.fetchNewToken();
console.log("โœ… Token refreshed successfully");
} catch (error) {
console.error("โŒ Failed to refresh token:", error);
}
}, TOKEN_REFRESH_INTERVAL);
}
/**
* Clear the refresh timer
*/
public cleanup(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
console.log("๐Ÿงน Token refresh timer cleaned up");
}
}
}
View config.ts Source Code
// config.ts
import dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files
//The .env file for this sample is located in the root of the project, and looks like this:
// TRADOVATE_NAME="your_username"
// TRADOVATE_PASSWORD="your_password"
// TRADOVATE_APP_ID="your_app_id"
// TRADOVATE_APP_VERSION=1.0
// TRADOVATE_SEC="your_secret_key"
// TRADOVATE_CID=0
// Load environment variables from .env file
dotenv.config();
export interface TradovateCredentials {
name: string;
password: string;
appId: string;
appVersion: string;
sec: string;
cid: number;
}
// Load credentials from environment variables
export const TRADOVATE_CONFIG: TradovateCredentials = {
name: process.env.TRADOVATE_NAME || "",
password: process.env.TRADOVATE_PASSWORD || "",
appId: process.env.TRADOVATE_APP_ID || "",
appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",
sec: process.env.TRADOVATE_SEC || "",
cid: parseInt(process.env.TRADOVATE_CID || "0", 10),
};
export const ENDPOINTS = {
AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for development
WS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",
} as const;
export const TOKEN_REFRESH_INTERVAL = 75 * 60 * 1000; // 75 minutes in milliseconds (keep below the token's expirationTime)
export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
// types.ts
export interface AccessTokenResponse {
accessToken: string;
mdAccessToken: string;
expirationTime: string;
userStatus: string;
userId: number;
name: string;
hasLive: boolean;
hasSimPlus: boolean;
hasFunded: boolean;
hasMarketData: boolean;
outdatedLiquidationPolicy: boolean;
outdatedSentimentPolicy: boolean;
experience: string;
orgName: string;
}
export interface StoredTokenInfo extends AccessTokenResponse {
retrievedAt: number; // timestamp when token was retrieved
}
export interface AuthRequest {
name: string;
password: string;
appId: string;
appVersion: string;
sec: string;
cid: number;
}
export interface SocketMessage {
i?: number; // request ID
s?: number; // status code
e?: SocketEventName;
d?: any; // data payload
}
export type SocketEventName = "props" | "md" | "clock" | "shutdown";
export type PluralEntityName = string; // This would be dynamically generated from EntityName
export interface TdvCache {
[key: string]: any[];
}
export type MessageEventHandler = (data: SocketMessage[]) => void;
// index.ts
import WebSocket from "ws";
import readline from "readline";
import { TokenManager } from "./token-manager.js";
import { ReconnectionManager } from "./reconnection-manager.js";
import { ENDPOINTS } from "./config.js";
import { SocketMessage } from "./types.js";
export class TradovateWebSocketClient {
private ws: WebSocket | null = null;
private tokenManager: TokenManager;
private reconnectionManager: ReconnectionManager;
private isAuthenticated: boolean = false;
private authenticationSent: boolean = false;
private heartbeatTimer: NodeJS.Timeout | null = null;
private readonly heartbeatInterval: number = 2500; // 2.5 seconds
private heartbeatsSent: number = 0;
private lastServerMessageTime: number = Date.now();
private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connection
private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;
private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)
private syncCompleted: boolean = false;
constructor() {
this.tokenManager = new TokenManager();
this.reconnectionManager = new ReconnectionManager({
maxReconnectAttempts: 10,
initialReconnectDelay: 1000, // 1 second
maxReconnectDelay: 60000, // 60 seconds
});
// Setup reconnection callbacks
this.reconnectionManager.setOnReconnect(async () => {
await this.handleReconnect();
});
this.reconnectionManager.setOnMaxAttemptsReached(() => {
this.cleanup();
});
}
async start(): Promise<void> {
console.log("๐Ÿš€ Tradovate WebSocket Client - TypeScript Version");
console.log("=".repeat(50));
try {
// Get access token (will fetch new one if needed)
await this.tokenManager.getAccessToken();
// Connect to WebSocket
await this.connectWebSocket();
} catch (error) {
console.error("โŒ Error starting client:", (error as Error).message);
this.cleanup();
}
}
private async connectWebSocket(): Promise<void> {
return new Promise((resolve, reject) => {
console.log("๐Ÿ”Œ Connecting to WebSocket...");
console.log(`๐Ÿ“ก URL: ${ENDPOINTS.WS_URL}`);
this.ws = new WebSocket(ENDPOINTS.WS_URL);
let settled = false; // Track if promise has been settled
const handleError = (error: Error) => {
console.error("โŒ WebSocket error:", error.message);
if (!settled) {
settled = true;
reject(error);
}
};
// Attach error handler
this.ws.on("error", handleError);
this.ws.on("open", () => {
console.log("โœ… WebSocket connection established");
this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connection
if (!settled) {
settled = true;
resolve();
}
});
this.ws.on("message", (data) => {
this.handleMessage(data);
});
this.ws.on("close", (code, reason) => {
console.log(`๐Ÿ”Œ WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);
this.resetConnectionState();
// Trigger reconnection if enabled
const reconnectState = this.reconnectionManager.getState();
if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {
this.reconnectionManager.schedule();
}
});
// Set connection timeout
setTimeout(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
if (!settled) {
settled = true;
reject(new Error("WebSocket connection timeout"));
}
}
}, 10000);
});
}
private async authenticate(): Promise<void> {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.error("โŒ WebSocket is not connected");
return;
}
if (this.authenticationSent) {
console.log("โญ๏ธ Authentication already sent, skipping");
return;
}
console.log("๐Ÿ” Preparing authentication message...");
try {
const accessToken = await this.tokenManager.getAccessToken();
// Correct Tradovate format: authorize\n[request ID]\n\n[access token]
const requestId = 0;
const authMessage = `authorize\n${requestId}\n\n${accessToken}`;
console.log("๐Ÿ“ค Sending authentication message...");
this.ws.send(authMessage);
this.authenticationSent = true;
console.log("โœ… Authentication message sent to WebSocket");
} catch (error) {
console.error("โŒ Failed to get access token for authentication:", error);
}
}
private handleMessage(data: WebSocket.Data): void {
const rawMessage = data.toString();
// Update last server message timestamp
this.lastServerMessageTime = Date.now();
// Handle heartbeat messages (single character messages like "o")
if (rawMessage.length === 1) {
console.log("๐Ÿ’“ Received frame:", rawMessage);
// If this is the first 'o' and we haven't authenticated yet, authenticate immediately
if (rawMessage === "o" && !this.authenticationSent) {
console.log("๐Ÿš€ Received server open frame - authenticating immediately");
this.authenticate();
return;
}
// Handle subsequent heartbeats only if authenticated
if (rawMessage === "o" && this.isAuthenticated) {
this.sendHeartbeat();
this.resetHeartbeatTimer();
} else if (rawMessage === "o" && !this.isAuthenticated) {
console.log("๐Ÿ’“ Heartbeat received but authentication still pending");
}
return;
}
// Handle messages with 'a' prefix (array responses)
if (rawMessage.startsWith("a[")) {
try {
// Extract JSON array from 'a[...]' format
const jsonPart = rawMessage.substring(1); // Remove 'a' prefix
const messageArray: SocketMessage[] = JSON.parse(jsonPart);
console.log("๐Ÿ“จ Received array response:", JSON.stringify(messageArray, null, 2));
// Check if this is specifically an authentication response (request ID 0)
if (Array.isArray(messageArray) && messageArray.length > 0) {
const response = messageArray[0];
// Authentication response has request ID 0
if (response.i === 0 && !this.isAuthenticated) {
if (response.s === 200) {
this.isAuthenticated = true;
console.log("โœ… Authentication successful!");
// Start heartbeat timer after successful authentication
this.startHeartbeat();
} else {
console.error("โŒ Authentication failed:", response);
this.cleanup();
}
}
// Other responses (auth/me, etc.) with different request IDs
else if (response.i && response.i > 1) {
console.log(`๐Ÿ“ฌ Response for request ID ${response.i}:`, response);
}
}
} catch (error) {
console.error("โŒ Error parsing array response:", (error as Error).message);
console.log("Raw message:", rawMessage);
}
return;
}
// Handle regular JSON messages
try {
const message: SocketMessage = JSON.parse(rawMessage);
console.log("๐Ÿ“จ Received JSON message:", JSON.stringify(message, null, 2));
} catch (error) {
console.log("๐Ÿ“ฆ Non-JSON message received:", rawMessage);
// Don't treat non-JSON messages as errors, just log them
}
}
/**
* Send a message to the WebSocket server
* @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')
* @param body - Optional request body (will be JSON stringified if object)
* @param requestId - Optional request ID (will auto-increment if not provided)
* @returns The request ID used for this message
*/
public send(endpoint: string, body?: any, requestId?: number): number {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.error("โŒ WebSocket is not connected");
throw new Error("WebSocket is not connected");
}
if (!this.isAuthenticated) {
console.error("โŒ WebSocket is not authenticated yet");
throw new Error("WebSocket is not authenticated yet");
}
// Use provided requestId or auto-increment
const reqId = requestId !== undefined ? requestId : this.requestIdCounter++;
// Format message according to Tradovate protocol: endpoint\nrequestId\n\nbody
let message = `${endpoint}\n${reqId}\n\n`;
if (body !== undefined) {
// If body is an object, stringify it; otherwise use as-is
message += typeof body === "object" ? JSON.stringify(body) : body;
}
console.log(`๐Ÿ“ค Sending message to ${endpoint} (Request ID: ${reqId})`);
this.ws.send(message);
return reqId;
}
/**
* Get the current connection status
*/
public getConnectionStatus(): {
connected: boolean;
authenticated: boolean;
reconnecting: boolean;
reconnectAttempts: number;
} {
const reconnectState = this.reconnectionManager.getState();
return {
connected: this.ws?.readyState === WebSocket.OPEN,
authenticated: this.isAuthenticated,
reconnecting: reconnectState.isReconnecting,
reconnectAttempts: reconnectState.reconnectAttempts,
};
}
/**
* Manually disconnect from the WebSocket (will not auto-reconnect)
*/
public disconnect(): void {
console.log("๐Ÿ”Œ Manual disconnect requested");
this.reconnectionManager.disable();
this.stopHeartbeat();
if (this.ws) {
this.ws.close(1000, "Manual disconnect");
}
}
/**
* Manually reconnect to the WebSocket (useful after calling disconnect)
*/
public async reconnect(): Promise<void> {
console.log("๐Ÿ”„ Manual reconnect requested");
this.reconnectionManager.enable();
this.reconnectionManager.reset();
await this.reconnectionManager.attempt();
}
private startHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
console.log("๐Ÿ’“ Starting heartbeat timer (2.5s interval) after authentication");
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatInterval);
// Start the timeout checker
this.startHeartbeatTimeoutChecker();
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
console.log("๐Ÿ’“ Heartbeat timer stopped");
}
// Stop the timeout checker
this.stopHeartbeatTimeoutChecker();
}
private startHeartbeatTimeoutChecker(): void {
if (this.heartbeatTimeoutTimer) {
clearInterval(this.heartbeatTimeoutTimer);
}
// Check every 5 seconds if we've heard from the server recently
this.heartbeatTimeoutTimer = setInterval(() => {
this.checkServerTimeout();
}, 5000);
}
private stopHeartbeatTimeoutChecker(): void {
if (this.heartbeatTimeoutTimer) {
clearInterval(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
private checkServerTimeout(): void {
const timeSinceLastMessage = Date.now() - this.lastServerMessageTime;
if (timeSinceLastMessage > this.serverTimeoutMs) {
console.error(
`โš ๏ธ No server response for ${timeSinceLastMessage}ms (threshold: ${this.serverTimeoutMs}ms)`
);
console.error("๐Ÿ’€ Connection appears dead, forcing reconnection...");
// Force close the connection to trigger reconnection logic
// Use 1000 (normal closure) or 4000-4999 (custom application codes)
if (this.ws) {
this.ws.close(4000, "Server timeout - no heartbeat response");
}
}
}
private resetHeartbeatTimer(): void {
// Reset the timer when we receive a heartbeat from server
this.stopHeartbeat();
this.startHeartbeat();
}
private sendHeartbeat(): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send("[]");
this.heartbeatsSent++;
console.log(`๐Ÿ’“ Empty frame heartbeat #${this.heartbeatsSent} sent: []`);
}
}
/**
* Reset connection-related state (called when connection is lost)
*/
private resetConnectionState(): void {
this.isAuthenticated = false;
this.authenticationSent = false;
this.stopHeartbeat();
this.lastServerMessageTime = Date.now(); // Reset to avoid false timeout on reconnect
}
/**
* Handle reconnection - called by ReconnectionManager
*/
private async handleReconnect(): Promise<void> {
// Ensure old connection is cleaned up
if (this.ws) {
this.ws.removeAllListeners();
// Only close if the connection is fully established (OPEN state)
// Don't call close() on CONNECTING, CLOSING, or CLOSED states
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.close(1000, "Reconnecting");
}
this.ws = null;
}
// Get fresh access token if needed
await this.tokenManager.getAccessToken();
// Attempt to reconnect
await this.connectWebSocket();
}
private cleanup(): void {
// Disable reconnection before cleanup
this.reconnectionManager.disable();
this.reconnectionManager.cleanup();
this.stopHeartbeat();
this.tokenManager.cleanup();
if (this.ws) {
this.ws.close();
}
console.log("๐Ÿงน Cleanup completed");
process.exit(0);
}
}
// Handle unhandled rejections and errors
process.on("unhandledRejection", (reason, promise) => {
console.error("โš ๏ธ Unhandled Rejection:", reason);
// Don't exit, let reconnection logic handle it
});
process.on("uncaughtException", (error) => {
console.error("โš ๏ธ Uncaught Exception:", error.message);
// Don't exit, let reconnection logic handle it
});
// Handle process termination
process.on("SIGINT", () => {
console.log("\n๐Ÿ›‘ Received SIGINT, shutting down gracefully...");
process.exit(0);
});
process.on("SIGTERM", () => {
console.log("\n๐Ÿ›‘ Received SIGTERM, shutting down gracefully...");
process.exit(0);
});
// Start the application
const client = new TradovateWebSocketClient();
client.start().catch((error) => {
console.error("๐Ÿ’ฅ Fatal error:", error.message);
process.exit(1);
});