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 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
1// TokenManager.ts
2
3import fs from "fs/promises";
4import fetch from "node-fetch";
5import {
6 TRADOVATE_CONFIG,
7 ENDPOINTS,
8 TOKEN_REFRESH_INTERVAL,
9 TOKEN_STORAGE_PATH,
10} from "./config.js";
11import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";
12
13export class TokenManager {
14 private tokenInfo: StoredTokenInfo | null = null;
15 private refreshTimer: NodeJS.Timeout | null = null;
16
17 constructor() {
18 this.loadTokenFromFile();
19 }
20
21 /**
22 * Get a valid access token, refreshing if necessary
23 */
24 async getAccessToken(): Promise<string> {
25 // Check if we have a valid token
26 if (this.tokenInfo && this.isTokenValid()) {
27 return this.tokenInfo.accessToken;
28 }
29
30 // Get a new token
31 console.log("๐Ÿ”„ Getting new access token...");
32 await this.fetchNewToken();
33
34 if (!this.tokenInfo) {
35 throw new Error("Failed to get access token");
36 }
37
38 return this.tokenInfo.accessToken;
39 }
40
41 /**
42 * Get user ID from stored token info
43 */
44 getUserId(): number {
45 if (!this.tokenInfo) {
46 throw new Error("No token info available. Please get access token first.");
47 }
48 return this.tokenInfo.userId;
49 }
50
51 /**
52 * Check if current token is valid (not expired)
53 */
54 private isTokenValid(): boolean {
55 if (!this.tokenInfo) {
56 return false;
57 }
58
59 const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();
60 const currentTime = Date.now();
61 const bufferTime = 5 * 60 * 1000; // 5 minute buffer
62
63 return currentTime < expirationTime - bufferTime;
64 }
65
66 /**
67 * Fetch a new access token from the API
68 */
69 private async fetchNewToken(): Promise<void> {
70 const authRequest: AuthRequest = {
71 name: TRADOVATE_CONFIG.name,
72 password: TRADOVATE_CONFIG.password,
73 appId: TRADOVATE_CONFIG.appId,
74 appVersion: TRADOVATE_CONFIG.appVersion,
75 sec: TRADOVATE_CONFIG.sec,
76 cid: TRADOVATE_CONFIG.cid,
77 };
78
79 try {
80 console.log(`๐Ÿ“ก Making auth request to: ${ENDPOINTS.AUTH_URL}`);
81 const response = await fetch(ENDPOINTS.AUTH_URL, {
82 method: "POST",
83 headers: {
84 "Content-Type": "application/json",
85 },
86 body: JSON.stringify(authRequest),
87 });
88
89 if (!response.ok) {
90 const errorText = await response.text();
91 throw new Error(
92 `Auth request failed: ${response.status} ${response.statusText} - ${errorText}`
93 );
94 }
95
96 const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;
97
98 // Store with retrieval timestamp
99 this.tokenInfo = {
100 ...tokenResponse,
101 retrievedAt: Date.now(),
102 };
103
104 // Save to file
105 await this.saveTokenToFile();
106
107 // Setup auto-refresh
108 this.setupTokenRefresh();
109
110 console.log("โœ… Access token retrieved successfully");
111 console.log(`๐Ÿ‘ค User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);
112 console.log(`๐Ÿข Organization: ${this.tokenInfo.orgName}`);
113 console.log(`โฐ Expires: ${this.tokenInfo.expirationTime}`);
114 } catch (error) {
115 console.error("โŒ Failed to fetch access token:", error);
116 throw error;
117 }
118 }
119
120 /**
121 * Load token from storage file
122 */
123 private async loadTokenFromFile(): Promise<void> {
124 try {
125 const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");
126 this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;
127 console.log("โœ… Loaded access token from storage");
128
129 // Setup refresh timer if token is still valid
130 if (this.isTokenValid()) {
131 this.setupTokenRefresh();
132 }
133 } catch (error) {
134 console.log("๐Ÿ“ No existing token file found or invalid token");
135 this.tokenInfo = null;
136 }
137 }
138
139 /**
140 * Save token to storage file
141 */
142 private async saveTokenToFile(): Promise<void> {
143 if (!this.tokenInfo) {
144 return;
145 }
146
147 try {
148 await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));
149 console.log("๐Ÿ’พ Access token saved to storage");
150 } catch (error) {
151 console.error("โŒ Failed to save token to file:", error);
152 }
153 }
154
155 /**
156 * Setup automatic token refresh
157 */
158 private setupTokenRefresh(): void {
159 if (this.refreshTimer) {
160 clearTimeout(this.refreshTimer);
161 }
162
163 console.log(`โฐ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);
164
165 this.refreshTimer = setTimeout(async () => {
166 console.log("๐Ÿ”„ Refreshing access token...");
167 try {
168 await this.fetchNewToken();
169 console.log("โœ… Token refreshed successfully");
170 } catch (error) {
171 console.error("โŒ Failed to refresh token:", error);
172 }
173 }, TOKEN_REFRESH_INTERVAL);
174 }
175
176 /**
177 * Clear the refresh timer
178 */
179 public cleanup(): void {
180 if (this.refreshTimer) {
181 clearTimeout(this.refreshTimer);
182 this.refreshTimer = null;
183 console.log("๐Ÿงน Token refresh timer cleaned up");
184 }
185 }
186}
View config.ts Source Code
1// config.ts
2
3import dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files
4
5//The .env file for this sample is located in the root of the project, and looks like this:
6// TRADOVATE_NAME="your_username"
7// TRADOVATE_PASSWORD="your_password"
8// TRADOVATE_APP_ID="your_app_id"
9// TRADOVATE_APP_VERSION=1.0
10// TRADOVATE_SEC="your_secret_key"
11// TRADOVATE_CID=0
12
13// Load environment variables from .env file
14dotenv.config();
15
16export interface TradovateCredentials {
17 name: string;
18 password: string;
19 appId: string;
20 appVersion: string;
21 sec: string;
22 cid: number;
23}
24
25// Load credentials from environment variables
26export const TRADOVATE_CONFIG: TradovateCredentials = {
27 name: process.env.TRADOVATE_NAME || "",
28 password: process.env.TRADOVATE_PASSWORD || "",
29 appId: process.env.TRADOVATE_APP_ID || "",
30 appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",
31 sec: process.env.TRADOVATE_SEC || "",
32 cid: parseInt(process.env.TRADOVATE_CID || "0", 10),
33};
34
35export const ENDPOINTS = {
36 AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for development
37 WS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",
38} as const;
39
40export const TOKEN_REFRESH_INTERVAL = 85 * 60 * 1000; // 85 minutes in milliseconds
41export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
1// types.ts
2
3export interface AccessTokenResponse {
4 accessToken: string;
5 mdAccessToken: string;
6 expirationTime: string;
7 userStatus: string;
8 userId: number;
9 name: string;
10 hasLive: boolean;
11 hasSimPlus: boolean;
12 hasFunded: boolean;
13 hasMarketData: boolean;
14 outdatedLiquidationPolicy: boolean;
15 outdatedSentimentPolicy: boolean;
16 experience: string;
17 orgName: string;
18}
19
20export interface StoredTokenInfo extends AccessTokenResponse {
21 retrievedAt: number; // timestamp when token was retrieved
22}
23
24export interface AuthRequest {
25 name: string;
26 password: string;
27 appId: string;
28 appVersion: string;
29 sec: string;
30 cid: number;
31}
32
33export interface SocketMessage {
34 i?: number; // request ID
35 s?: number; // status code
36 e?: SocketEventName;
37 d?: any; // data payload
38}
39
40export type SocketEventName = "props" | "md" | "clock" | "shutdown";
41
42export type PluralEntityName = string; // This would be dynamically generated from EntityName
43
44export interface TdvCache {
45 [key: string]: any[];
46}
47
48export type MessageEventHandler = (data: SocketMessage[]) => void;
View reconnection-manager.ts Source Code
1// reconnection-manager.ts
2
3/**
4 * ReconnectionManager
5 *
6 * Handles automatic reconnection logic with exponential backoff for WebSocket connections.
7 */
8export class ReconnectionManager {
9 private reconnectAttempts: number = 0;
10 private readonly maxReconnectAttempts: number;
11 private readonly initialReconnectDelay: number;
12 private readonly maxReconnectDelay: number;
13 private reconnectTimer: NodeJS.Timeout | null = null;
14 private shouldReconnect: boolean = true;
15 private isReconnecting: boolean = false;
16 private onReconnectCallback: (() => Promise<void>) | null = null;
17 private onMaxAttemptsReached: (() => void) | null = null;
18
19 constructor(
20 options: {
21 maxReconnectAttempts?: number;
22 initialReconnectDelay?: number;
23 maxReconnectDelay?: number;
24 } = {}
25 ) {
26 this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
27 this.initialReconnectDelay = options.initialReconnectDelay ?? 1000; // 1 second
28 this.maxReconnectDelay = options.maxReconnectDelay ?? 60000; // 60 seconds
29 }
30
31 /**
32 * Set the callback to execute when attempting to reconnect
33 */
34 public setOnReconnect(callback: () => Promise<void>): void {
35 this.onReconnectCallback = callback;
36 }
37
38 /**
39 * Set the callback to execute when max reconnection attempts are reached
40 */
41 public setOnMaxAttemptsReached(callback: () => void): void {
42 this.onMaxAttemptsReached = callback;
43 }
44
45 /**
46 * Get the current reconnection state
47 */
48 public getState(): {
49 isReconnecting: boolean;
50 reconnectAttempts: number;
51 shouldReconnect: boolean;
52 } {
53 return {
54 isReconnecting: this.isReconnecting,
55 reconnectAttempts: this.reconnectAttempts,
56 shouldReconnect: this.shouldReconnect,
57 };
58 }
59
60 /**
61 * Enable automatic reconnection
62 */
63 public enable(): void {
64 this.shouldReconnect = true;
65 }
66
67 /**
68 * Disable automatic reconnection and cancel any pending attempts
69 */
70 public disable(): void {
71 this.shouldReconnect = false;
72 this.cancel();
73 }
74
75 /**
76 * Reset the reconnection counter (useful after successful connection)
77 */
78 public reset(): void {
79 this.reconnectAttempts = 0;
80 this.isReconnecting = false;
81 }
82
83 /**
84 * Calculate exponential backoff delay
85 * Formula: min(initialDelay * 2^attempt, maxDelay) + jitter
86 */
87 private calculateBackoffDelay(): number {
88 const exponentialDelay = this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts);
89 const cappedDelay = Math.min(exponentialDelay, this.maxReconnectDelay);
90
91 // Add random jitter (0-10% of delay) to avoid thundering herd
92 const jitter = Math.random() * cappedDelay * 0.1;
93
94 return Math.floor(cappedDelay + jitter);
95 }
96
97 /**
98 * Schedule a reconnection attempt with exponential backoff
99 */
100 public schedule(): void {
101 if (this.reconnectAttempts >= this.maxReconnectAttempts) {
102 console.error(
103 `โŒ Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`
104 );
105 if (this.onMaxAttemptsReached) {
106 this.onMaxAttemptsReached();
107 }
108 return;
109 }
110
111 const delay = this.calculateBackoffDelay();
112 console.log(
113 `๐Ÿ”„ Scheduling reconnection attempt ${this.reconnectAttempts + 1}/${
114 this.maxReconnectAttempts
115 } in ${delay}ms...`
116 );
117
118 this.reconnectTimer = setTimeout(() => {
119 this.attempt();
120 }, delay);
121 }
122
123 /**
124 * Attempt to reconnect immediately
125 */
126 public async attempt(): Promise<void> {
127 if (!this.shouldReconnect) {
128 console.log("โน๏ธ Reconnection cancelled - reconnection disabled");
129 return;
130 }
131
132 if (!this.onReconnectCallback) {
133 console.error("โŒ No reconnection callback set");
134 return;
135 }
136
137 this.reconnectAttempts++;
138 this.isReconnecting = true;
139
140 console.log(
141 `๐Ÿ”„ Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`
142 );
143
144 try {
145 await this.onReconnectCallback();
146
147 // If successful, reset reconnection counter
148 this.reconnectAttempts = 0;
149 this.isReconnecting = false;
150 console.log("โœ… Reconnection successful!");
151 } catch (error) {
152 console.error(
153 `โŒ Reconnection attempt ${this.reconnectAttempts} failed:`,
154 (error as Error).message
155 );
156 this.isReconnecting = false;
157
158 // Schedule next attempt
159 this.schedule();
160 }
161 }
162
163 /**
164 * Cancel any pending reconnection attempts
165 */
166 public cancel(): void {
167 if (this.reconnectTimer) {
168 clearTimeout(this.reconnectTimer);
169 this.reconnectTimer = null;
170 }
171 }
172
173 /**
174 * Cleanup resources
175 */
176 public cleanup(): void {
177 this.cancel();
178 this.onReconnectCallback = null;
179 this.onMaxAttemptsReached = null;
180 }
181}

Initial boilerplate code for our WebSocket client:

1// index.ts
2
3import WebSocket from "ws";
4import readline from "readline";
5import { TokenManager } from "./token-manager.js";
6import { ReconnectionManager } from "./reconnection-manager.js";
7import { ENDPOINTS } from "./config.js";
8import { SocketMessage } from "./types.js";
9
10export class TradovateWebSocketClient {
11 private ws: WebSocket | null = null;
12 private tokenManager: TokenManager;
13 private reconnectionManager: ReconnectionManager;
14 private isAuthenticated: boolean = false;
15 private authenticationSent: boolean = false;
16 private heartbeatTimer: NodeJS.Timeout | null = null;
17 private readonly heartbeatInterval: number = 2500; // 2.5 seconds
18 private heartbeatsSent: number = 0;
19 private lastServerMessageTime: number = Date.now();
20 private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connection
21 private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;
22 private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)
23 private syncCompleted: boolean = false;
24
25 constructor() {
26 this.tokenManager = new TokenManager();
27 this.reconnectionManager = new ReconnectionManager({
28 maxReconnectAttempts: 10,
29 initialReconnectDelay: 1000, // 1 second
30 maxReconnectDelay: 60000, // 60 seconds
31 });
32
33 // Setup reconnection callbacks
34 this.reconnectionManager.setOnReconnect(async () => {
35 await this.handleReconnect();
36 });
37
38 this.reconnectionManager.setOnMaxAttemptsReached(() => {
39 this.cleanup();
40 });
41 }
42
43 async start(): Promise<void> {}
44
45 private async connectWebSocket(): Promise<void> {
46 return new Promise((resolve, reject) => {});
47 }
48
49 private async authenticate(): Promise<void> {}
50
51 private handleMessage(data: WebSocket.Data): void {}
52
53 private startHeartbeat(): void {}
54
55 private stopHeartbeat(): void {}
56
57 private resetHeartbeatTimer(): void {}
58
59 private sendHeartbeat(): void {}
60
61 private resetConnectionState(): void {}
62
63 private async handleReconnect(): Promise<void> {}
64
65 private cleanup(): void {}
66}
67
68// Handle unhandled rejections and errors
69process.on("unhandledRejection", (reason, promise) => {
70 console.error("โš ๏ธ Unhandled Rejection:", reason);
71 // Don't exit, let reconnection logic handle it
72});
73
74process.on("uncaughtException", (error) => {
75 console.error("โš ๏ธ Uncaught Exception:", error.message);
76 // Don't exit, let reconnection logic handle it
77});
78
79// Handle process termination
80process.on("SIGINT", () => {
81 console.log("\n๐Ÿ›‘ Received SIGINT, shutting down gracefully...");
82 process.exit(0);
83});
84
85process.on("SIGTERM", () => {
86 console.log("\n๐Ÿ›‘ Received SIGTERM, shutting down gracefully...");
87 process.exit(0);
88});
89
90// Start the application
91const client = new TradovateWebSocketClient();
92client.start().catch((error) => {
93 console.error("๐Ÿ’ฅ Fatal error:", error.message);
94 process.exit(1);
95});

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
1async start(): Promise<void> {
2 console.log('๐Ÿš€ Tradovate WebSocket Client - TypeScript Version');
3 console.log('='.repeat(50));
4
5 try {
6 // Get access token (will fetch new one if needed)
7 await this.tokenManager.getAccessToken();
8
9 // Connect to WebSocket
10 await this.connectWebSocket();
11
12 } catch (error) {
13 console.error('โŒ Error starting client:', (error as Error).message);
14 this.cleanup();
15 }
16 }

connectWebSocket

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

View connectWebSocket Source Code
1private async connectWebSocket(): Promise<void> {
2 return new Promise((resolve, reject) => {
3 console.log('๐Ÿ”Œ Connecting to WebSocket...');
4 console.log(`๐Ÿ“ก URL: ${ENDPOINTS.WS_URL}`);
5
6 this.ws = new WebSocket(ENDPOINTS.WS_URL);
7 let settled = false; // Track if promise has been settled
8
9 const handleError = (error: Error) => {
10 console.error('โŒ WebSocket error:', error.message);
11 if (!settled) {
12 settled = true;
13 reject(error);
14 }
15 };
16
17 // Attach error handler
18 this.ws.on('error', handleError);
19
20 this.ws.on('open', () => {
21 console.log('โœ… WebSocket connection established');
22 this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connection
23 if (!settled) {
24 settled = true;
25 resolve();
26 }
27 });
28
29 this.ws.on('message', (data) => {
30 this.handleMessage(data);
31 });
32
33 this.ws.on('close', (code, reason) => {
34 console.log(`๐Ÿ”Œ WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);
35 this.resetConnectionState();
36
37 // Trigger reconnection if enabled
38 const reconnectState = this.reconnectionManager.getState();
39 if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {
40 this.reconnectionManager.schedule();
41 }
42 });
43
44 // Set connection timeout
45 setTimeout(() => {
46 if (this.ws?.readyState !== WebSocket.OPEN) {
47 if (!settled) {
48 settled = true;
49 reject(new Error('WebSocket connection timeout'));
50 }
51 }
52 }, 10000);
53 });
54 }

authenticate

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

View authenticate Source Code
1private async authenticate(): Promise<void> {
2 if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
3 console.error('โŒ WebSocket is not connected');
4 return;
5 }
6
7 if (this.authenticationSent) {
8 console.log('โญ๏ธ Authentication already sent, skipping');
9 return;
10 }
11
12 console.log('๐Ÿ” Preparing authentication message...');
13
14 try {
15 const accessToken = await this.tokenManager.getAccessToken();
16
17 // Correct Tradovate format: authorize\n[request ID]\n\n[access token]
18 const requestId = 0;
19 const authMessage = `authorize\n${requestId}\n\n${accessToken}`;
20
21 console.log('๐Ÿ“ค Sending authentication message...');
22 this.ws.send(authMessage);
23 this.authenticationSent = true;
24 console.log('โœ… Authentication message sent to WebSocket');
25
26 } catch (error) {
27 console.error('โŒ Failed to get access token for authentication:', error);
28 }
29 }

handleMessage

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

View handleMessage Source Code
1private handleMessage(data: WebSocket.Data): void {
2 const rawMessage = data.toString();
3
4 // Update last server message timestamp
5 this.lastServerMessageTime = Date.now();
6
7 // Handle heartbeat messages (single character messages like "o")
8 if (rawMessage.length === 1) {
9 console.log('๐Ÿ’“ Received frame:', rawMessage);
10
11 // If this is the first 'o' and we haven't authenticated yet, authenticate immediately
12 if (rawMessage === 'o' && !this.authenticationSent) {
13 console.log('๐Ÿš€ Received server open frame - authenticating immediately');
14 this.authenticate();
15 return;
16 }
17
18 // Handle subsequent heartbeats only if authenticated
19 if (rawMessage === 'o' && this.isAuthenticated) {
20 this.sendHeartbeat();
21 this.resetHeartbeatTimer();
22 } else if (rawMessage === 'o' && !this.isAuthenticated) {
23 console.log('๐Ÿ’“ Heartbeat received but authentication still pending');
24 }
25 return;
26 }
27
28 // Handle messages with 'a' prefix (array responses)
29 if (rawMessage.startsWith('a[')) {
30 try {
31 // Extract JSON array from 'a[...]' format
32 const jsonPart = rawMessage.substring(1); // Remove 'a' prefix
33 const messageArray: SocketMessage[] = JSON.parse(jsonPart);
34 console.log('๐Ÿ“จ Received array response:', JSON.stringify(messageArray, null, 2));
35
36 // Check if this is specifically an authentication response (request ID 0)
37 if (Array.isArray(messageArray) && messageArray.length > 0) {
38 const response = messageArray[0];
39
40 // Authentication response has request ID 0
41 if (response.i === 0 && !this.isAuthenticated) {
42 if (response.s === 200) {
43 this.isAuthenticated = true;
44 console.log('โœ… Authentication successful!');
45 // Start heartbeat timer after successful authentication
46 this.startHeartbeat();
47 } else {
48 console.error('โŒ Authentication failed:', response);
49 this.cleanup();
50 }
51 }
52 // Other responses (auth/me, etc.) with different request IDs
53 else if (response.i && response.i > 1) {
54 console.log(`๐Ÿ“ฌ Response for request ID ${response.i}:`, response);
55 }
56 }
57 } catch (error) {
58 console.error('โŒ Error parsing array response:', (error as Error).message);
59 console.log('Raw message:', rawMessage);
60 }
61 return;
62 }
63
64 // Handle regular JSON messages
65 try {
66 const message: SocketMessage = JSON.parse(rawMessage);
67 console.log('๐Ÿ“จ Received JSON message:', JSON.stringify(message, null, 2));
68
69 } catch (error) {
70 console.log('๐Ÿ“ฆ Non-JSON message received:', rawMessage);
71 // Don't treat non-JSON messages as errors, just log them
72 }
73 }

startHeartbeat

The startHeartbeat method starts the heartbeat timer and timeout checker.

View startHeartbeat Source Code
1private startHeartbeat(): void {
2 if (this.heartbeatTimer) {
3 clearInterval(this.heartbeatTimer);
4 }
5 console.log('๐Ÿ’“ Starting heartbeat timer (2.5s interval) after authentication');
6 this.heartbeatTimer = setInterval(() => {
7 this.sendHeartbeat();
8 }, this.heartbeatInterval);
9
10 // Start the timeout checker
11 this.startHeartbeatTimeoutChecker();
12 }

stopHeartbeat

The stopHeartbeat method will stop the heartbeats and timeout checker.

View stopHeartbeat Source Code
1private stopHeartbeat(): void {
2 if (this.heartbeatTimer) {
3 clearInterval(this.heartbeatTimer);
4 this.heartbeatTimer = null;
5 console.log('๐Ÿ’“ Heartbeat timer stopped');
6 }
7
8 // Stop the timeout checker
9 this.stopHeartbeatTimeoutChecker();
10 }

resetHeartbeatTimer

The resetHeartbeatTimer method will reset the heartbeat timer.

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

sendHeartbeat

The sendHeartbeat method will send the heartbeat message.

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

startHeartbeatTimeoutChecker

The startHeartbeatTimeoutChecker method checks for server timeouts every 5 seconds.

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

stopHeartbeatTimeoutChecker

The stopHeartbeatTimeoutChecker method stops the timeout checker.

View stopHeartbeatTimeoutChecker Source Code
1private stopHeartbeatTimeoutChecker(): void {
2 if (this.heartbeatTimeoutTimer) {
3 clearInterval(this.heartbeatTimeoutTimer);
4 this.heartbeatTimeoutTimer = null;
5 }
6 }

checkServerTimeout

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

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

resetConnectionState

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

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

handleReconnect

The handleReconnect method handles reconnection, called by ReconnectionManager.

View handleReconnect Source Code
1private async handleReconnect(): Promise<void> {
2 // Ensure old connection is cleaned up
3 if (this.ws) {
4 this.ws.removeAllListeners();
5 // Only close if the connection is fully established (OPEN state)
6 // Don't call close() on CONNECTING, CLOSING, or CLOSED states
7 if (this.ws.readyState === WebSocket.OPEN) {
8 this.ws.close(1000, 'Reconnecting');
9 }
10 this.ws = null;
11 }
12
13 // Get fresh access token if needed
14 await this.tokenManager.getAccessToken();
15
16 // Attempt to reconnect
17 await this.connectWebSocket();
18 }

getConnectionStatus

The getConnectionStatus public method returns the current connection status.

View getConnectionStatus Source Code
1public getConnectionStatus(): {
2 connected: boolean;
3 authenticated: boolean;
4 reconnecting: boolean;
5 reconnectAttempts: number;
6 } {
7 const reconnectState = this.reconnectionManager.getState();
8 return {
9 connected: this.ws?.readyState === WebSocket.OPEN,
10 authenticated: this.isAuthenticated,
11 reconnecting: reconnectState.isReconnecting,
12 reconnectAttempts: reconnectState.reconnectAttempts
13 };
14 }

disconnect

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

View disconnect Source Code
1public disconnect(): void {
2 console.log('๐Ÿ”Œ Manual disconnect requested');
3 this.reconnectionManager.disable();
4 this.stopHeartbeat();
5
6 if (this.ws) {
7 this.ws.close(1000, 'Manual disconnect');
8 }
9 }

reconnect

The reconnect public method manually reconnects to the WebSocket.

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

cleanup

The cleanup method will cleanup the client.

View cleanup Source Code
1private cleanup(): void {
2 // Disable reconnection before cleanup
3 this.reconnectionManager.disable();
4 this.reconnectionManager.cleanup();
5 this.stopHeartbeat();
6 this.tokenManager.cleanup();
7
8 if (this.ws) {
9 this.ws.close();
10 }
11
12 console.log('๐Ÿงน Cleanup completed');
13 process.exit(0);
14 }

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
1/**
2 * Send a message to the WebSocket server
3 * @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')
4 * @param body - Optional request body (will be JSON stringified if object)
5 * @param requestId - Optional request ID (will auto-increment if not provided)
6 * @returns The request ID used for this message
7 */
8public send(endpoint: string, body?: any, requestId?: number): number {
9 if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
10 console.error('โŒ WebSocket is not connected');
11 throw new Error('WebSocket is not connected');
12 }
13
14 if (!this.isAuthenticated) {
15 console.error('โŒ WebSocket is not authenticated yet');
16 throw new Error('WebSocket is not authenticated yet');
17 }
18
19 // Use provided requestId or auto-increment
20 const reqId = requestId !== undefined ? requestId : this.requestIdCounter++;
21
22 // Format message according to Tradovate protocol: endpoint\nrequestId\n\nbody
23 let message = `${endpoint}\n${reqId}\n\n`;
24
25 if (body !== undefined) {
26 // If body is an object, stringify it; otherwise use as-is
27 message += typeof body === 'object' ? JSON.stringify(body) : body;
28 }
29
30 console.log(`๐Ÿ“ค Sending message to ${endpoint} (Request ID: ${reqId})`);
31 this.ws.send(message);
32
33 return reqId;
34 }

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

1this.send("account/list");

The Full Code

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

View TokenManager.ts Source Code
1// TokenManager.ts
2
3import fs from "fs/promises";
4import fetch from "node-fetch";
5import {
6 TRADOVATE_CONFIG,
7 ENDPOINTS,
8 TOKEN_REFRESH_INTERVAL,
9 TOKEN_STORAGE_PATH,
10} from "./config.js";
11import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";
12
13export class TokenManager {
14 private tokenInfo: StoredTokenInfo | null = null;
15 private refreshTimer: NodeJS.Timeout | null = null;
16
17 constructor() {
18 this.loadTokenFromFile();
19 }
20
21 /**
22 * Get a valid access token, refreshing if necessary
23 */
24 async getAccessToken(): Promise<string> {
25 // Check if we have a valid token
26 if (this.tokenInfo && this.isTokenValid()) {
27 return this.tokenInfo.accessToken;
28 }
29
30 // Get a new token
31 console.log("๐Ÿ”„ Getting new access token...");
32 await this.fetchNewToken();
33
34 if (!this.tokenInfo) {
35 throw new Error("Failed to get access token");
36 }
37
38 return this.tokenInfo.accessToken;
39 }
40
41 /**
42 * Get user ID from stored token info
43 */
44 getUserId(): number {
45 if (!this.tokenInfo) {
46 throw new Error("No token info available. Please get access token first.");
47 }
48 return this.tokenInfo.userId;
49 }
50
51 /**
52 * Check if current token is valid (not expired)
53 */
54 private isTokenValid(): boolean {
55 if (!this.tokenInfo) {
56 return false;
57 }
58
59 const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();
60 const currentTime = Date.now();
61 const bufferTime = 5 * 60 * 1000; // 5 minute buffer
62
63 return currentTime < expirationTime - bufferTime;
64 }
65
66 /**
67 * Fetch a new access token from the API
68 */
69 private async fetchNewToken(): Promise<void> {
70 const authRequest: AuthRequest = {
71 name: TRADOVATE_CONFIG.name,
72 password: TRADOVATE_CONFIG.password,
73 appId: TRADOVATE_CONFIG.appId,
74 appVersion: TRADOVATE_CONFIG.appVersion,
75 sec: TRADOVATE_CONFIG.sec,
76 cid: TRADOVATE_CONFIG.cid,
77 };
78
79 try {
80 console.log(`๐Ÿ“ก Making auth request to: ${ENDPOINTS.AUTH_URL}`);
81 const response = await fetch(ENDPOINTS.AUTH_URL, {
82 method: "POST",
83 headers: {
84 "Content-Type": "application/json",
85 },
86 body: JSON.stringify(authRequest),
87 });
88
89 if (!response.ok) {
90 const errorText = await response.text();
91 throw new Error(
92 `Auth request failed: ${response.status} ${response.statusText} - ${errorText}`
93 );
94 }
95
96 const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;
97
98 // Store with retrieval timestamp
99 this.tokenInfo = {
100 ...tokenResponse,
101 retrievedAt: Date.now(),
102 };
103
104 // Save to file
105 await this.saveTokenToFile();
106
107 // Setup auto-refresh
108 this.setupTokenRefresh();
109
110 console.log("โœ… Access token retrieved successfully");
111 console.log(`๐Ÿ‘ค User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);
112 console.log(`๐Ÿข Organization: ${this.tokenInfo.orgName}`);
113 console.log(`โฐ Expires: ${this.tokenInfo.expirationTime}`);
114 } catch (error) {
115 console.error("โŒ Failed to fetch access token:", error);
116 throw error;
117 }
118 }
119
120 /**
121 * Load token from storage file
122 */
123 private async loadTokenFromFile(): Promise<void> {
124 try {
125 const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");
126 this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;
127 console.log("โœ… Loaded access token from storage");
128
129 // Setup refresh timer if token is still valid
130 if (this.isTokenValid()) {
131 this.setupTokenRefresh();
132 }
133 } catch (error) {
134 console.log("๐Ÿ“ No existing token file found or invalid token");
135 this.tokenInfo = null;
136 }
137 }
138
139 /**
140 * Save token to storage file
141 */
142 private async saveTokenToFile(): Promise<void> {
143 if (!this.tokenInfo) {
144 return;
145 }
146
147 try {
148 await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));
149 console.log("๐Ÿ’พ Access token saved to storage");
150 } catch (error) {
151 console.error("โŒ Failed to save token to file:", error);
152 }
153 }
154
155 /**
156 * Setup automatic token refresh
157 */
158 private setupTokenRefresh(): void {
159 if (this.refreshTimer) {
160 clearTimeout(this.refreshTimer);
161 }
162
163 console.log(`โฐ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);
164
165 this.refreshTimer = setTimeout(async () => {
166 console.log("๐Ÿ”„ Refreshing access token...");
167 try {
168 await this.fetchNewToken();
169 console.log("โœ… Token refreshed successfully");
170 } catch (error) {
171 console.error("โŒ Failed to refresh token:", error);
172 }
173 }, TOKEN_REFRESH_INTERVAL);
174 }
175
176 /**
177 * Clear the refresh timer
178 */
179 public cleanup(): void {
180 if (this.refreshTimer) {
181 clearTimeout(this.refreshTimer);
182 this.refreshTimer = null;
183 console.log("๐Ÿงน Token refresh timer cleaned up");
184 }
185 }
186}
View config.ts Source Code
1// config.ts
2
3import dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files
4
5//The .env file for this sample is located in the root of the project, and looks like this:
6// TRADOVATE_NAME="your_username"
7// TRADOVATE_PASSWORD="your_password"
8// TRADOVATE_APP_ID="your_app_id"
9// TRADOVATE_APP_VERSION=1.0
10// TRADOVATE_SEC="your_secret_key"
11// TRADOVATE_CID=0
12
13// Load environment variables from .env file
14dotenv.config();
15
16export interface TradovateCredentials {
17 name: string;
18 password: string;
19 appId: string;
20 appVersion: string;
21 sec: string;
22 cid: number;
23}
24
25// Load credentials from environment variables
26export const TRADOVATE_CONFIG: TradovateCredentials = {
27 name: process.env.TRADOVATE_NAME || "",
28 password: process.env.TRADOVATE_PASSWORD || "",
29 appId: process.env.TRADOVATE_APP_ID || "",
30 appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",
31 sec: process.env.TRADOVATE_SEC || "",
32 cid: parseInt(process.env.TRADOVATE_CID || "0", 10),
33};
34
35export const ENDPOINTS = {
36 AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for development
37 WS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",
38} as const;
39
40export const TOKEN_REFRESH_INTERVAL = 85 * 60 * 1000; // 85 minutes in milliseconds
41export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
1// types.ts
2
3export interface AccessTokenResponse {
4 accessToken: string;
5 mdAccessToken: string;
6 expirationTime: string;
7 userStatus: string;
8 userId: number;
9 name: string;
10 hasLive: boolean;
11 hasSimPlus: boolean;
12 hasFunded: boolean;
13 hasMarketData: boolean;
14 outdatedLiquidationPolicy: boolean;
15 outdatedSentimentPolicy: boolean;
16 experience: string;
17 orgName: string;
18}
19
20export interface StoredTokenInfo extends AccessTokenResponse {
21 retrievedAt: number; // timestamp when token was retrieved
22}
23
24export interface AuthRequest {
25 name: string;
26 password: string;
27 appId: string;
28 appVersion: string;
29 sec: string;
30 cid: number;
31}
32
33export interface SocketMessage {
34 i?: number; // request ID
35 s?: number; // status code
36 e?: SocketEventName;
37 d?: any; // data payload
38}
39
40export type SocketEventName = "props" | "md" | "clock" | "shutdown";
41
42export type PluralEntityName = string; // This would be dynamically generated from EntityName
43
44export interface TdvCache {
45 [key: string]: any[];
46}
47
48export type MessageEventHandler = (data: SocketMessage[]) => void;
1// index.ts
2
3import WebSocket from "ws";
4import readline from "readline";
5import { TokenManager } from "./token-manager.js";
6import { ReconnectionManager } from "./reconnection-manager.js";
7import { ENDPOINTS } from "./config.js";
8import { SocketMessage } from "./types.js";
9
10export class TradovateWebSocketClient {
11 private ws: WebSocket | null = null;
12 private tokenManager: TokenManager;
13 private reconnectionManager: ReconnectionManager;
14 private isAuthenticated: boolean = false;
15 private authenticationSent: boolean = false;
16 private heartbeatTimer: NodeJS.Timeout | null = null;
17 private readonly heartbeatInterval: number = 2500; // 2.5 seconds
18 private heartbeatsSent: number = 0;
19 private lastServerMessageTime: number = Date.now();
20 private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connection
21 private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;
22 private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)
23 private syncCompleted: boolean = false;
24
25 constructor() {
26 this.tokenManager = new TokenManager();
27 this.reconnectionManager = new ReconnectionManager({
28 maxReconnectAttempts: 10,
29 initialReconnectDelay: 1000, // 1 second
30 maxReconnectDelay: 60000, // 60 seconds
31 });
32
33 // Setup reconnection callbacks
34 this.reconnectionManager.setOnReconnect(async () => {
35 await this.handleReconnect();
36 });
37
38 this.reconnectionManager.setOnMaxAttemptsReached(() => {
39 this.cleanup();
40 });
41 }
42
43 async start(): Promise<void> {
44 console.log("๐Ÿš€ Tradovate WebSocket Client - TypeScript Version");
45 console.log("=".repeat(50));
46
47 try {
48 // Get access token (will fetch new one if needed)
49 await this.tokenManager.getAccessToken();
50
51 // Connect to WebSocket
52 await this.connectWebSocket();
53 } catch (error) {
54 console.error("โŒ Error starting client:", (error as Error).message);
55 this.cleanup();
56 }
57 }
58
59 private async connectWebSocket(): Promise<void> {
60 return new Promise((resolve, reject) => {
61 console.log("๐Ÿ”Œ Connecting to WebSocket...");
62 console.log(`๐Ÿ“ก URL: ${ENDPOINTS.WS_URL}`);
63
64 this.ws = new WebSocket(ENDPOINTS.WS_URL);
65 let settled = false; // Track if promise has been settled
66
67 const handleError = (error: Error) => {
68 console.error("โŒ WebSocket error:", error.message);
69 if (!settled) {
70 settled = true;
71 reject(error);
72 }
73 };
74
75 // Attach error handler
76 this.ws.on("error", handleError);
77
78 this.ws.on("open", () => {
79 console.log("โœ… WebSocket connection established");
80 this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connection
81 if (!settled) {
82 settled = true;
83 resolve();
84 }
85 });
86
87 this.ws.on("message", (data) => {
88 this.handleMessage(data);
89 });
90
91 this.ws.on("close", (code, reason) => {
92 console.log(`๐Ÿ”Œ WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);
93 this.resetConnectionState();
94
95 // Trigger reconnection if enabled
96 const reconnectState = this.reconnectionManager.getState();
97 if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {
98 this.reconnectionManager.schedule();
99 }
100 });
101
102 // Set connection timeout
103 setTimeout(() => {
104 if (this.ws?.readyState !== WebSocket.OPEN) {
105 if (!settled) {
106 settled = true;
107 reject(new Error("WebSocket connection timeout"));
108 }
109 }
110 }, 10000);
111 });
112 }
113
114 private async authenticate(): Promise<void> {
115 if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
116 console.error("โŒ WebSocket is not connected");
117 return;
118 }
119
120 if (this.authenticationSent) {
121 console.log("โญ๏ธ Authentication already sent, skipping");
122 return;
123 }
124
125 console.log("๐Ÿ” Preparing authentication message...");
126
127 try {
128 const accessToken = await this.tokenManager.getAccessToken();
129
130 // Correct Tradovate format: authorize\n[request ID]\n\n[access token]
131 const requestId = 0;
132 const authMessage = `authorize\n${requestId}\n\n${accessToken}`;
133
134 console.log("๐Ÿ“ค Sending authentication message...");
135 this.ws.send(authMessage);
136 this.authenticationSent = true;
137 console.log("โœ… Authentication message sent to WebSocket");
138 } catch (error) {
139 console.error("โŒ Failed to get access token for authentication:", error);
140 }
141 }
142
143 private handleMessage(data: WebSocket.Data): void {
144 const rawMessage = data.toString();
145
146 // Update last server message timestamp
147 this.lastServerMessageTime = Date.now();
148
149 // Handle heartbeat messages (single character messages like "o")
150 if (rawMessage.length === 1) {
151 console.log("๐Ÿ’“ Received frame:", rawMessage);
152
153 // If this is the first 'o' and we haven't authenticated yet, authenticate immediately
154 if (rawMessage === "o" && !this.authenticationSent) {
155 console.log("๐Ÿš€ Received server open frame - authenticating immediately");
156 this.authenticate();
157 return;
158 }
159
160 // Handle subsequent heartbeats only if authenticated
161 if (rawMessage === "o" && this.isAuthenticated) {
162 this.sendHeartbeat();
163 this.resetHeartbeatTimer();
164 } else if (rawMessage === "o" && !this.isAuthenticated) {
165 console.log("๐Ÿ’“ Heartbeat received but authentication still pending");
166 }
167 return;
168 }
169
170 // Handle messages with 'a' prefix (array responses)
171 if (rawMessage.startsWith("a[")) {
172 try {
173 // Extract JSON array from 'a[...]' format
174 const jsonPart = rawMessage.substring(1); // Remove 'a' prefix
175 const messageArray: SocketMessage[] = JSON.parse(jsonPart);
176 console.log("๐Ÿ“จ Received array response:", JSON.stringify(messageArray, null, 2));
177
178 // Check if this is specifically an authentication response (request ID 0)
179 if (Array.isArray(messageArray) && messageArray.length > 0) {
180 const response = messageArray[0];
181
182 // Authentication response has request ID 0
183 if (response.i === 0 && !this.isAuthenticated) {
184 if (response.s === 200) {
185 this.isAuthenticated = true;
186 console.log("โœ… Authentication successful!");
187 // Start heartbeat timer after successful authentication
188 this.startHeartbeat();
189 } else {
190 console.error("โŒ Authentication failed:", response);
191 this.cleanup();
192 }
193 }
194 // Other responses (auth/me, etc.) with different request IDs
195 else if (response.i && response.i > 1) {
196 console.log(`๐Ÿ“ฌ Response for request ID ${response.i}:`, response);
197 }
198 }
199 } catch (error) {
200 console.error("โŒ Error parsing array response:", (error as Error).message);
201 console.log("Raw message:", rawMessage);
202 }
203 return;
204 }
205
206 // Handle regular JSON messages
207 try {
208 const message: SocketMessage = JSON.parse(rawMessage);
209 console.log("๐Ÿ“จ Received JSON message:", JSON.stringify(message, null, 2));
210 } catch (error) {
211 console.log("๐Ÿ“ฆ Non-JSON message received:", rawMessage);
212 // Don't treat non-JSON messages as errors, just log them
213 }
214 }
215
216 /**
217 * Send a message to the WebSocket server
218 * @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')
219 * @param body - Optional request body (will be JSON stringified if object)
220 * @param requestId - Optional request ID (will auto-increment if not provided)
221 * @returns The request ID used for this message
222 */
223 public send(endpoint: string, body?: any, requestId?: number): number {
224 if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
225 console.error("โŒ WebSocket is not connected");
226 throw new Error("WebSocket is not connected");
227 }
228
229 if (!this.isAuthenticated) {
230 console.error("โŒ WebSocket is not authenticated yet");
231 throw new Error("WebSocket is not authenticated yet");
232 }
233
234 // Use provided requestId or auto-increment
235 const reqId = requestId !== undefined ? requestId : this.requestIdCounter++;
236
237 // Format message according to Tradovate protocol: endpoint\nrequestId\n\nbody
238 let message = `${endpoint}\n${reqId}\n\n`;
239
240 if (body !== undefined) {
241 // If body is an object, stringify it; otherwise use as-is
242 message += typeof body === "object" ? JSON.stringify(body) : body;
243 }
244
245 console.log(`๐Ÿ“ค Sending message to ${endpoint} (Request ID: ${reqId})`);
246 this.ws.send(message);
247
248 return reqId;
249 }
250
251 /**
252 * Get the current connection status
253 */
254 public getConnectionStatus(): {
255 connected: boolean;
256 authenticated: boolean;
257 reconnecting: boolean;
258 reconnectAttempts: number;
259 } {
260 const reconnectState = this.reconnectionManager.getState();
261 return {
262 connected: this.ws?.readyState === WebSocket.OPEN,
263 authenticated: this.isAuthenticated,
264 reconnecting: reconnectState.isReconnecting,
265 reconnectAttempts: reconnectState.reconnectAttempts,
266 };
267 }
268
269 /**
270 * Manually disconnect from the WebSocket (will not auto-reconnect)
271 */
272 public disconnect(): void {
273 console.log("๐Ÿ”Œ Manual disconnect requested");
274 this.reconnectionManager.disable();
275 this.stopHeartbeat();
276
277 if (this.ws) {
278 this.ws.close(1000, "Manual disconnect");
279 }
280 }
281
282 /**
283 * Manually reconnect to the WebSocket (useful after calling disconnect)
284 */
285 public async reconnect(): Promise<void> {
286 console.log("๐Ÿ”„ Manual reconnect requested");
287 this.reconnectionManager.enable();
288 this.reconnectionManager.reset();
289
290 await this.reconnectionManager.attempt();
291 }
292
293 private startHeartbeat(): void {
294 if (this.heartbeatTimer) {
295 clearInterval(this.heartbeatTimer);
296 }
297 console.log("๐Ÿ’“ Starting heartbeat timer (2.5s interval) after authentication");
298 this.heartbeatTimer = setInterval(() => {
299 this.sendHeartbeat();
300 }, this.heartbeatInterval);
301
302 // Start the timeout checker
303 this.startHeartbeatTimeoutChecker();
304 }
305
306 private stopHeartbeat(): void {
307 if (this.heartbeatTimer) {
308 clearInterval(this.heartbeatTimer);
309 this.heartbeatTimer = null;
310 console.log("๐Ÿ’“ Heartbeat timer stopped");
311 }
312
313 // Stop the timeout checker
314 this.stopHeartbeatTimeoutChecker();
315 }
316
317 private startHeartbeatTimeoutChecker(): void {
318 if (this.heartbeatTimeoutTimer) {
319 clearInterval(this.heartbeatTimeoutTimer);
320 }
321
322 // Check every 5 seconds if we've heard from the server recently
323 this.heartbeatTimeoutTimer = setInterval(() => {
324 this.checkServerTimeout();
325 }, 5000);
326 }
327
328 private stopHeartbeatTimeoutChecker(): void {
329 if (this.heartbeatTimeoutTimer) {
330 clearInterval(this.heartbeatTimeoutTimer);
331 this.heartbeatTimeoutTimer = null;
332 }
333 }
334
335 private checkServerTimeout(): void {
336 const timeSinceLastMessage = Date.now() - this.lastServerMessageTime;
337
338 if (timeSinceLastMessage > this.serverTimeoutMs) {
339 console.error(
340 `โš ๏ธ No server response for ${timeSinceLastMessage}ms (threshold: ${this.serverTimeoutMs}ms)`
341 );
342 console.error("๐Ÿ’€ Connection appears dead, forcing reconnection...");
343
344 // Force close the connection to trigger reconnection logic
345 // Use 1000 (normal closure) or 4000-4999 (custom application codes)
346 if (this.ws) {
347 this.ws.close(4000, "Server timeout - no heartbeat response");
348 }
349 }
350 }
351
352 private resetHeartbeatTimer(): void {
353 // Reset the timer when we receive a heartbeat from server
354 this.stopHeartbeat();
355 this.startHeartbeat();
356 }
357
358 private sendHeartbeat(): void {
359 if (this.ws && this.ws.readyState === WebSocket.OPEN) {
360 this.ws.send("[]");
361 this.heartbeatsSent++;
362 console.log(`๐Ÿ’“ Empty frame heartbeat #${this.heartbeatsSent} sent: []`);
363 }
364 }
365
366 /**
367 * Reset connection-related state (called when connection is lost)
368 */
369 private resetConnectionState(): void {
370 this.isAuthenticated = false;
371 this.authenticationSent = false;
372 this.stopHeartbeat();
373 this.lastServerMessageTime = Date.now(); // Reset to avoid false timeout on reconnect
374 }
375
376 /**
377 * Handle reconnection - called by ReconnectionManager
378 */
379 private async handleReconnect(): Promise<void> {
380 // Ensure old connection is cleaned up
381 if (this.ws) {
382 this.ws.removeAllListeners();
383 // Only close if the connection is fully established (OPEN state)
384 // Don't call close() on CONNECTING, CLOSING, or CLOSED states
385 if (this.ws.readyState === WebSocket.OPEN) {
386 this.ws.close(1000, "Reconnecting");
387 }
388 this.ws = null;
389 }
390
391 // Get fresh access token if needed
392 await this.tokenManager.getAccessToken();
393
394 // Attempt to reconnect
395 await this.connectWebSocket();
396 }
397
398 private cleanup(): void {
399 // Disable reconnection before cleanup
400 this.reconnectionManager.disable();
401 this.reconnectionManager.cleanup();
402 this.stopHeartbeat();
403 this.tokenManager.cleanup();
404
405 if (this.ws) {
406 this.ws.close();
407 }
408
409 console.log("๐Ÿงน Cleanup completed");
410 process.exit(0);
411 }
412}
413
414// Handle unhandled rejections and errors
415process.on("unhandledRejection", (reason, promise) => {
416 console.error("โš ๏ธ Unhandled Rejection:", reason);
417 // Don't exit, let reconnection logic handle it
418});
419
420process.on("uncaughtException", (error) => {
421 console.error("โš ๏ธ Uncaught Exception:", error.message);
422 // Don't exit, let reconnection logic handle it
423});
424
425// Handle process termination
426process.on("SIGINT", () => {
427 console.log("\n๐Ÿ›‘ Received SIGINT, shutting down gracefully...");
428 process.exit(0);
429});
430
431process.on("SIGTERM", () => {
432 console.log("\n๐Ÿ›‘ Received SIGTERM, shutting down gracefully...");
433 process.exit(0);
434});
435
436// Start the application
437const client = new TradovateWebSocketClient();
438client.start().catch((error) => {
439 console.error("๐Ÿ’ฅ Fatal error:", error.message);
440 process.exit(1);
441});