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 3 import fs from "fs/promises"; 4 import fetch from "node-fetch"; 5 import { 6 TRADOVATE_CONFIG, 7 ENDPOINTS, 8 TOKEN_REFRESH_INTERVAL, 9 TOKEN_STORAGE_PATH, 10 } from "./config.js"; 11 import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js"; 12 13 export 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 3 import 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 14 dotenv.config(); 15 16 export 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 26 export 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 35 export 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 40 export const TOKEN_REFRESH_INTERVAL = 85 * 60 * 1000; // 85 minutes in milliseconds 41 export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
1 // types.ts 2 3 export 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 20 export interface StoredTokenInfo extends AccessTokenResponse { 21 retrievedAt: number; // timestamp when token was retrieved 22 } 23 24 export interface AuthRequest { 25 name: string; 26 password: string; 27 appId: string; 28 appVersion: string; 29 sec: string; 30 cid: number; 31 } 32 33 export interface SocketMessage { 34 i?: number; // request ID 35 s?: number; // status code 36 e?: SocketEventName; 37 d?: any; // data payload 38 } 39 40 export type SocketEventName = "props" | "md" | "clock" | "shutdown"; 41 42 export type PluralEntityName = string; // This would be dynamically generated from EntityName 43 44 export interface TdvCache { 45 [key: string]: any[]; 46 } 47 48 export 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 */ 8 export 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 3 import WebSocket from "ws"; 4 import readline from "readline"; 5 import { TokenManager } from "./token-manager.js"; 6 import { ReconnectionManager } from "./reconnection-manager.js"; 7 import { ENDPOINTS } from "./config.js"; 8 import { SocketMessage } from "./types.js"; 9 10 export 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 69 process.on("unhandledRejection", (reason, promise) => { 70 console.error("โ ๏ธ Unhandled Rejection:", reason); 71 // Don't exit, let reconnection logic handle it 72 }); 73 74 process.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 80 process.on("SIGINT", () => { 81 console.log("\n๐ Received SIGINT, shutting down gracefully..."); 82 process.exit(0); 83 }); 84 85 process.on("SIGTERM", () => { 86 console.log("\n๐ Received SIGTERM, shutting down gracefully..."); 87 process.exit(0); 88 }); 89 90 // Start the application 91 const client = new TradovateWebSocketClient(); 92 client.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
1 async 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 private 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
1 public 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
1 public 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
1 public 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
1 private 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 */ 8 public 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:
1 this.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 3 import fs from "fs/promises"; 4 import fetch from "node-fetch"; 5 import { 6 TRADOVATE_CONFIG, 7 ENDPOINTS, 8 TOKEN_REFRESH_INTERVAL, 9 TOKEN_STORAGE_PATH, 10 } from "./config.js"; 11 import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js"; 12 13 export 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 3 import 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 14 dotenv.config(); 15 16 export 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 26 export 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 35 export 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 40 export const TOKEN_REFRESH_INTERVAL = 85 * 60 * 1000; // 85 minutes in milliseconds 41 export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
1 // types.ts 2 3 export 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 20 export interface StoredTokenInfo extends AccessTokenResponse { 21 retrievedAt: number; // timestamp when token was retrieved 22 } 23 24 export interface AuthRequest { 25 name: string; 26 password: string; 27 appId: string; 28 appVersion: string; 29 sec: string; 30 cid: number; 31 } 32 33 export interface SocketMessage { 34 i?: number; // request ID 35 s?: number; // status code 36 e?: SocketEventName; 37 d?: any; // data payload 38 } 39 40 export type SocketEventName = "props" | "md" | "clock" | "shutdown"; 41 42 export type PluralEntityName = string; // This would be dynamically generated from EntityName 43 44 export interface TdvCache { 45 [key: string]: any[]; 46 } 47 48 export type MessageEventHandler = (data: SocketMessage[]) => void;
1 // index.ts 2 3 import WebSocket from "ws"; 4 import readline from "readline"; 5 import { TokenManager } from "./token-manager.js"; 6 import { ReconnectionManager } from "./reconnection-manager.js"; 7 import { ENDPOINTS } from "./config.js"; 8 import { SocketMessage } from "./types.js"; 9 10 export 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 415 process.on("unhandledRejection", (reason, promise) => { 416 console.error("โ ๏ธ Unhandled Rejection:", reason); 417 // Don't exit, let reconnection logic handle it 418 }); 419 420 process.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 426 process.on("SIGINT", () => { 427 console.log("\n๐ Received SIGINT, shutting down gracefully..."); 428 process.exit(0); 429 }); 430 431 process.on("SIGTERM", () => { 432 console.log("\n๐ Received SIGTERM, shutting down gracefully..."); 433 process.exit(0); 434 }); 435 436 // Start the application 437 const client = new TradovateWebSocketClient(); 438 client.start().catch((error) => { 439 console.error("๐ฅ Fatal error:", error.message); 440 process.exit(1); 441 });

