WebSocket Connections
Real-time data is critical for modern trading applications. The Tradovate Partner API provides robust WebSocket connections for streaming user data, position updates, order status changes, and risk alerts.
Overview
WebSocket connections offer several advantages over REST endpoints for real-time data:
- Low Latency - Direct TCP connection with minimal overhead
- Bidirectional - Server can push data without client polling
- Efficient - Single persistent connection for multiple data streams
- Real-time - Immediate delivery of critical trading events
Connection Limits
Tradovate limits your simultaneous WebSocket usage in a few ways. When you exceed a limit, the server closes your oldest connection and revokes its token to make room for the new one.
| Limit | Default | Applies to |
|---|---|---|
| Total connections per user | 50 | All WebSocket connections |
| Distinct devices (organization administrators) | 15 | Market-data connections |
| Distinct IP addresses | 5 | Market-data connections |
Before closing a connection to enforce a limit, the server sends a shutdown event with a reasonCode (ConnectionQuotaReached, DeviceQuotaReached, IPQuotaReached, or Maintenance). Log these events so you can distinguish a deliberate server close from a network drop.
Disconnect Behavior
The server never sends a WebSocket close status code, so any server-initiated disconnect reaches your client as either 1005 (a graceful close, such as a quota or maintenance disconnect, where the server sent an empty close frame) or 1006 (an abrupt close, such as an idle timeout or network drop, where no close frame was sent). The server drops any connection that sends nothing for 15 seconds.
The reliable way to tell a deliberate disconnect from a network issue is the shutdown event: a quota or policy disconnect is always preceded by one carrying a reasonCode. If a close was preceded by a shutdown event, it was deliberate. If not, treat it as a heartbeat, idle, or network issue and reconnect.
Connection Management
Step 1: Define the WebSocket Client
First, we will set up our WebSocket client class. This class will handle the connection to the WebSocket server, the authentication, reconnection logic, and the sending and receiving of messages from the Tradovate WebSocket server.
The client class will use helper classes called TokenManager and ReconnectionManager to handle access token management and automatic reconnection with exponential backoff, along with some other helper .ts files for configuration and types.
View TokenManager.ts Source Code
// TokenManager.tsimport fs from "fs/promises";import fetch from "node-fetch";import {TRADOVATE_CONFIG,ENDPOINTS,TOKEN_REFRESH_INTERVAL,TOKEN_STORAGE_PATH,} from "./config.js";import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";export class TokenManager {private tokenInfo: StoredTokenInfo | null = null;private refreshTimer: NodeJS.Timeout | null = null;constructor() {this.loadTokenFromFile();}/*** Get a valid access token, refreshing if necessary*/async getAccessToken(): Promise<string> {// Check if we have a valid tokenif (this.tokenInfo && this.isTokenValid()) {return this.tokenInfo.accessToken;}// Get a new tokenconsole.log("๐ Getting new access token...");await this.fetchNewToken();if (!this.tokenInfo) {throw new Error("Failed to get access token");}return this.tokenInfo.accessToken;}/*** Get user ID from stored token info*/getUserId(): number {if (!this.tokenInfo) {throw new Error("No token info available. Please get access token first.");}return this.tokenInfo.userId;}/*** Check if current token is valid (not expired)*/private isTokenValid(): boolean {if (!this.tokenInfo) {return false;}const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();const currentTime = Date.now();const bufferTime = 5 * 60 * 1000; // 5 minute bufferreturn currentTime < expirationTime - bufferTime;}/*** Fetch a new access token from the API*/private async fetchNewToken(): Promise<void> {const authRequest: AuthRequest = {name: TRADOVATE_CONFIG.name,password: TRADOVATE_CONFIG.password,appId: TRADOVATE_CONFIG.appId,appVersion: TRADOVATE_CONFIG.appVersion,sec: TRADOVATE_CONFIG.sec,cid: TRADOVATE_CONFIG.cid,};try {console.log(`๐ก Making auth request to: ${ENDPOINTS.AUTH_URL}`);const response = await fetch(ENDPOINTS.AUTH_URL, {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify(authRequest),});if (!response.ok) {const errorText = await response.text();throw new Error(`Auth request failed: ${response.status} ${response.statusText} - ${errorText}`);}const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;// Store with retrieval timestampthis.tokenInfo = {...tokenResponse,retrievedAt: Date.now(),};// Save to fileawait this.saveTokenToFile();// Setup auto-refreshthis.setupTokenRefresh();console.log("โ Access token retrieved successfully");console.log(`๐ค User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);console.log(`๐ข Organization: ${this.tokenInfo.orgName}`);console.log(`โฐ Expires: ${this.tokenInfo.expirationTime}`);} catch (error) {console.error("โ Failed to fetch access token:", error);throw error;}}/*** Load token from storage file*/private async loadTokenFromFile(): Promise<void> {try {const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;console.log("โ Loaded access token from storage");// Setup refresh timer if token is still validif (this.isTokenValid()) {this.setupTokenRefresh();}} catch (error) {console.log("๐ No existing token file found or invalid token");this.tokenInfo = null;}}/*** Save token to storage file*/private async saveTokenToFile(): Promise<void> {if (!this.tokenInfo) {return;}try {await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));console.log("๐พ Access token saved to storage");} catch (error) {console.error("โ Failed to save token to file:", error);}}/*** Setup automatic token refresh*/private setupTokenRefresh(): void {if (this.refreshTimer) {clearTimeout(this.refreshTimer);}console.log(`โฐ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);this.refreshTimer = setTimeout(async () => {console.log("๐ Refreshing access token...");try {await this.fetchNewToken();console.log("โ Token refreshed successfully");} catch (error) {console.error("โ Failed to refresh token:", error);}}, TOKEN_REFRESH_INTERVAL);}/*** Clear the refresh timer*/public cleanup(): void {if (this.refreshTimer) {clearTimeout(this.refreshTimer);this.refreshTimer = null;console.log("๐งน Token refresh timer cleaned up");}}}
View config.ts Source Code
// config.tsimport dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files//The .env file for this sample is located in the root of the project, and looks like this:// TRADOVATE_NAME="your_username"// TRADOVATE_PASSWORD="your_password"// TRADOVATE_APP_ID="your_app_id"// TRADOVATE_APP_VERSION=1.0// TRADOVATE_SEC="your_secret_key"// TRADOVATE_CID=0// Load environment variables from .env filedotenv.config();export interface TradovateCredentials {name: string;password: string;appId: string;appVersion: string;sec: string;cid: number;}// Load credentials from environment variablesexport const TRADOVATE_CONFIG: TradovateCredentials = {name: process.env.TRADOVATE_NAME || "",password: process.env.TRADOVATE_PASSWORD || "",appId: process.env.TRADOVATE_APP_ID || "",appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",sec: process.env.TRADOVATE_SEC || "",cid: parseInt(process.env.TRADOVATE_CID || "0", 10),};export const ENDPOINTS = {AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for developmentWS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",} as const;export const TOKEN_REFRESH_INTERVAL = 75 * 60 * 1000; // 75 minutes in milliseconds (keep below the token's expirationTime)export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
// types.tsexport interface AccessTokenResponse {accessToken: string;mdAccessToken: string;expirationTime: string;userStatus: string;userId: number;name: string;hasLive: boolean;hasSimPlus: boolean;hasFunded: boolean;hasMarketData: boolean;outdatedLiquidationPolicy: boolean;outdatedSentimentPolicy: boolean;experience: string;orgName: string;}export interface StoredTokenInfo extends AccessTokenResponse {retrievedAt: number; // timestamp when token was retrieved}export interface AuthRequest {name: string;password: string;appId: string;appVersion: string;sec: string;cid: number;}export interface SocketMessage {i?: number; // request IDs?: number; // status codee?: SocketEventName;d?: any; // data payload}export type SocketEventName = "props" | "md" | "clock" | "shutdown";export type PluralEntityName = string; // This would be dynamically generated from EntityNameexport interface TdvCache {[key: string]: any[];}export type MessageEventHandler = (data: SocketMessage[]) => void;
View reconnection-manager.ts Source Code
// reconnection-manager.ts/*** ReconnectionManager** Handles automatic reconnection logic with exponential backoff for WebSocket connections.*/export class ReconnectionManager {private reconnectAttempts: number = 0;private readonly maxReconnectAttempts: number;private readonly initialReconnectDelay: number;private readonly maxReconnectDelay: number;private reconnectTimer: NodeJS.Timeout | null = null;private shouldReconnect: boolean = true;private isReconnecting: boolean = false;private onReconnectCallback: (() => Promise<void>) | null = null;private onMaxAttemptsReached: (() => void) | null = null;constructor(options: {maxReconnectAttempts?: number;initialReconnectDelay?: number;maxReconnectDelay?: number;} = {}) {this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;this.initialReconnectDelay = options.initialReconnectDelay ?? 1000; // 1 secondthis.maxReconnectDelay = options.maxReconnectDelay ?? 60000; // 60 seconds}/*** Set the callback to execute when attempting to reconnect*/public setOnReconnect(callback: () => Promise<void>): void {this.onReconnectCallback = callback;}/*** Set the callback to execute when max reconnection attempts are reached*/public setOnMaxAttemptsReached(callback: () => void): void {this.onMaxAttemptsReached = callback;}/*** Get the current reconnection state*/public getState(): {isReconnecting: boolean;reconnectAttempts: number;shouldReconnect: boolean;} {return {isReconnecting: this.isReconnecting,reconnectAttempts: this.reconnectAttempts,shouldReconnect: this.shouldReconnect,};}/*** Enable automatic reconnection*/public enable(): void {this.shouldReconnect = true;}/*** Disable automatic reconnection and cancel any pending attempts*/public disable(): void {this.shouldReconnect = false;this.cancel();}/*** Reset the reconnection counter (useful after successful connection)*/public reset(): void {this.reconnectAttempts = 0;this.isReconnecting = false;}/*** Calculate exponential backoff delay* Formula: min(initialDelay * 2^attempt, maxDelay) + jitter*/private calculateBackoffDelay(): number {const exponentialDelay = this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts);const cappedDelay = Math.min(exponentialDelay, this.maxReconnectDelay);// Add random jitter (0-10% of delay) to avoid thundering herdconst jitter = Math.random() * cappedDelay * 0.1;return Math.floor(cappedDelay + jitter);}/*** Schedule a reconnection attempt with exponential backoff*/public schedule(): void {if (this.reconnectAttempts >= this.maxReconnectAttempts) {console.error(`โ Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`);if (this.onMaxAttemptsReached) {this.onMaxAttemptsReached();}return;}const delay = this.calculateBackoffDelay();console.log(`๐ Scheduling reconnection attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts} in ${delay}ms...`);this.reconnectTimer = setTimeout(() => {this.attempt();}, delay);}/*** Attempt to reconnect immediately*/public async attempt(): Promise<void> {if (!this.shouldReconnect) {console.log("โน๏ธ Reconnection cancelled - reconnection disabled");return;}if (!this.onReconnectCallback) {console.error("โ No reconnection callback set");return;}this.reconnectAttempts++;this.isReconnecting = true;console.log(`๐ Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);try {await this.onReconnectCallback();// If successful, reset reconnection counterthis.reconnectAttempts = 0;this.isReconnecting = false;console.log("โ Reconnection successful!");} catch (error) {console.error(`โ Reconnection attempt ${this.reconnectAttempts} failed:`,(error as Error).message);this.isReconnecting = false;// Schedule next attemptthis.schedule();}}/*** Cancel any pending reconnection attempts*/public cancel(): void {if (this.reconnectTimer) {clearTimeout(this.reconnectTimer);this.reconnectTimer = null;}}/*** Cleanup resources*/public cleanup(): void {this.cancel();this.onReconnectCallback = null;this.onMaxAttemptsReached = null;}}
Initial boilerplate code for our WebSocket client:
// index.tsimport WebSocket from "ws";import readline from "readline";import { TokenManager } from "./token-manager.js";import { ReconnectionManager } from "./reconnection-manager.js";import { ENDPOINTS } from "./config.js";import { SocketMessage } from "./types.js";export class TradovateWebSocketClient {private ws: WebSocket | null = null;private tokenManager: TokenManager;private reconnectionManager: ReconnectionManager;private isAuthenticated: boolean = false;private authenticationSent: boolean = false;private heartbeatTimer: NodeJS.Timeout | null = null;private readonly heartbeatInterval: number = 2500; // 2.5 secondsprivate heartbeatsSent: number = 0;private lastServerMessageTime: number = Date.now();private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connectionprivate heartbeatTimeoutTimer: NodeJS.Timeout | null = null;private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)private syncCompleted: boolean = false;constructor() {this.tokenManager = new TokenManager();this.reconnectionManager = new ReconnectionManager({maxReconnectAttempts: 10,initialReconnectDelay: 1000, // 1 secondmaxReconnectDelay: 60000, // 60 seconds});// Setup reconnection callbacksthis.reconnectionManager.setOnReconnect(async () => {await this.handleReconnect();});this.reconnectionManager.setOnMaxAttemptsReached(() => {this.cleanup();});}async start(): Promise<void> {}private async connectWebSocket(): Promise<void> {return new Promise((resolve, reject) => {});}private async authenticate(): Promise<void> {}private handleMessage(data: WebSocket.Data): void {}private startHeartbeat(): void {}private stopHeartbeat(): void {}private resetHeartbeatTimer(): void {}private sendHeartbeat(): void {}private resetConnectionState(): void {}private async handleReconnect(): Promise<void> {}private cleanup(): void {}}// Handle unhandled rejections and errorsprocess.on("unhandledRejection", (reason, promise) => {console.error("โ ๏ธ Unhandled Rejection:", reason);// Don't exit, let reconnection logic handle it});process.on("uncaughtException", (error) => {console.error("โ ๏ธ Uncaught Exception:", error.message);// Don't exit, let reconnection logic handle it});// Handle process terminationprocess.on("SIGINT", () => {console.log("\n๐ Received SIGINT, shutting down gracefully...");process.exit(0);});process.on("SIGTERM", () => {console.log("\n๐ Received SIGTERM, shutting down gracefully...");process.exit(0);});// Start the applicationconst client = new TradovateWebSocketClient();client.start().catch((error) => {console.error("๐ฅ Fatal error:", error.message);process.exit(1);});
Step 2: Define our WebSocket Client Methods for basic functionality
start
The start method initializes the WebSocket client, obtains an access token, and establishes the WebSocket connection.
View start Source Code
async start(): Promise<void> {console.log('๐ Tradovate WebSocket Client - TypeScript Version');console.log('='.repeat(50));try {// Get access token (will fetch new one if needed)await this.tokenManager.getAccessToken();// Connect to WebSocketawait this.connectWebSocket();} catch (error) {console.error('โ Error starting client:', (error as Error).message);this.cleanup();}}
connectWebSocket
The connectWebSocket method will connect to the WebSocket server and authenticate the client.
View connectWebSocket Source Code
private async connectWebSocket(): Promise<void> {return new Promise((resolve, reject) => {console.log('๐ Connecting to WebSocket...');console.log(`๐ก URL: ${ENDPOINTS.WS_URL}`);this.ws = new WebSocket(ENDPOINTS.WS_URL);let settled = false; // Track if promise has been settledconst handleError = (error: Error) => {console.error('โ WebSocket error:', error.message);if (!settled) {settled = true;reject(error);}};// Attach error handlerthis.ws.on('error', handleError);this.ws.on('open', () => {console.log('โ WebSocket connection established');this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connectionif (!settled) {settled = true;resolve();}});this.ws.on('message', (data) => {this.handleMessage(data);});this.ws.on('close', (code, reason) => {console.log(`๐ WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);this.resetConnectionState();// Trigger reconnection if enabledconst reconnectState = this.reconnectionManager.getState();if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {this.reconnectionManager.schedule();}});// Set connection timeoutsetTimeout(() => {if (this.ws?.readyState !== WebSocket.OPEN) {if (!settled) {settled = true;reject(new Error('WebSocket connection timeout'));}}}, 10000);});}
authenticate
The authenticate method will authenticate the client with the WebSocket server using our access token.
View authenticate Source Code
private async authenticate(): Promise<void> {if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {console.error('โ WebSocket is not connected');return;}if (this.authenticationSent) {console.log('โญ๏ธ Authentication already sent, skipping');return;}console.log('๐ Preparing authentication message...');try {const accessToken = await this.tokenManager.getAccessToken();// Correct Tradovate format: authorize\n[request ID]\n\n[access token]const requestId = 0;const authMessage = `authorize\n${requestId}\n\n${accessToken}`;console.log('๐ค Sending authentication message...');this.ws.send(authMessage);this.authenticationSent = true;console.log('โ Authentication message sent to WebSocket');} catch (error) {console.error('โ Failed to get access token for authentication:', error);}}
handleMessage
The handleMessage method will handle the incoming messages from the WebSocket server.
View handleMessage Source Code
private handleMessage(data: WebSocket.Data): void {const rawMessage = data.toString();// Update last server message timestampthis.lastServerMessageTime = Date.now();// Handle heartbeat messages (single character messages like "o")if (rawMessage.length === 1) {console.log('๐ Received frame:', rawMessage);// If this is the first 'o' and we haven't authenticated yet, authenticate immediatelyif (rawMessage === 'o' && !this.authenticationSent) {console.log('๐ Received server open frame - authenticating immediately');this.authenticate();return;}// Handle subsequent heartbeats only if authenticatedif (rawMessage === 'o' && this.isAuthenticated) {this.sendHeartbeat();this.resetHeartbeatTimer();} else if (rawMessage === 'o' && !this.isAuthenticated) {console.log('๐ Heartbeat received but authentication still pending');}return;}// Handle messages with 'a' prefix (array responses)if (rawMessage.startsWith('a[')) {try {// Extract JSON array from 'a[...]' formatconst jsonPart = rawMessage.substring(1); // Remove 'a' prefixconst messageArray: SocketMessage[] = JSON.parse(jsonPart);console.log('๐จ Received array response:', JSON.stringify(messageArray, null, 2));// Check if this is specifically an authentication response (request ID 0)if (Array.isArray(messageArray) && messageArray.length > 0) {const response = messageArray[0];// Authentication response has request ID 0if (response.i === 0 && !this.isAuthenticated) {if (response.s === 200) {this.isAuthenticated = true;console.log('โ Authentication successful!');// Start heartbeat timer after successful authenticationthis.startHeartbeat();} else {console.error('โ Authentication failed:', response);this.cleanup();}}// Other responses (auth/me, etc.) with different request IDselse if (response.i && response.i > 1) {console.log(`๐ฌ Response for request ID ${response.i}:`, response);}}} catch (error) {console.error('โ Error parsing array response:', (error as Error).message);console.log('Raw message:', rawMessage);}return;}// Handle regular JSON messagestry {const message: SocketMessage = JSON.parse(rawMessage);console.log('๐จ Received JSON message:', JSON.stringify(message, null, 2));} catch (error) {console.log('๐ฆ Non-JSON message received:', rawMessage);// Don't treat non-JSON messages as errors, just log them}}
startHeartbeat
The startHeartbeat method starts the heartbeat timer and timeout checker.
View startHeartbeat Source Code
private startHeartbeat(): void {if (this.heartbeatTimer) {clearInterval(this.heartbeatTimer);}console.log('๐ Starting heartbeat timer (2.5s interval) after authentication');this.heartbeatTimer = setInterval(() => {this.sendHeartbeat();}, this.heartbeatInterval);// Start the timeout checkerthis.startHeartbeatTimeoutChecker();}
stopHeartbeat
The stopHeartbeat method will stop the heartbeats and timeout checker.
View stopHeartbeat Source Code
private stopHeartbeat(): void {if (this.heartbeatTimer) {clearInterval(this.heartbeatTimer);this.heartbeatTimer = null;console.log('๐ Heartbeat timer stopped');}// Stop the timeout checkerthis.stopHeartbeatTimeoutChecker();}
resetHeartbeatTimer
The resetHeartbeatTimer method will reset the heartbeat timer.
View resetHeartbeatTimer Source Code
private resetHeartbeatTimer(): void {// Reset the timer when we receive a heartbeat from serverthis.stopHeartbeat();this.startHeartbeat();}
sendHeartbeat
The sendHeartbeat method will send the heartbeat message.
View sendHeartbeat Source Code
private sendHeartbeat(): void {if (this.ws && this.ws.readyState === WebSocket.OPEN) {this.ws.send('[]');this.heartbeatsSent++;console.log(`๐ Empty frame heartbeat #${this.heartbeatsSent} sent: []`);}}
startHeartbeatTimeoutChecker
The startHeartbeatTimeoutChecker method checks for server timeouts every 5 seconds.
View startHeartbeatTimeoutChecker Source Code
private startHeartbeatTimeoutChecker(): void {if (this.heartbeatTimeoutTimer) {clearInterval(this.heartbeatTimeoutTimer);}// Check every 5 seconds if we've heard from the server recentlythis.heartbeatTimeoutTimer = setInterval(() => {this.checkServerTimeout();}, 5000);}
stopHeartbeatTimeoutChecker
The stopHeartbeatTimeoutChecker method stops the timeout checker.
View stopHeartbeatTimeoutChecker Source Code
private stopHeartbeatTimeoutChecker(): void {if (this.heartbeatTimeoutTimer) {clearInterval(this.heartbeatTimeoutTimer);this.heartbeatTimeoutTimer = null;}}
checkServerTimeout
The checkServerTimeout method checks if the server has stopped responding and forces reconnection if needed.
View checkServerTimeout Source Code
private checkServerTimeout(): void {const timeSinceLastMessage = Date.now() - this.lastServerMessageTime;if (timeSinceLastMessage > this.serverTimeoutMs) {console.error(`โ ๏ธ No server response for ${timeSinceLastMessage}ms (threshold: ${this.serverTimeoutMs}ms)`);console.error('๐ Connection appears dead, forcing reconnection...');// Force close the connection to trigger reconnection logic// Use 1000 (normal closure) or 4000-4999 (custom application codes)if (this.ws) {this.ws.close(4000, 'Server timeout - no heartbeat response');}}}
resetConnectionState
The resetConnectionState method resets connection-related state when connection is lost.
View resetConnectionState Source Code
private resetConnectionState(): void {this.isAuthenticated = false;this.authenticationSent = false;this.stopHeartbeat();this.lastServerMessageTime = Date.now(); // Reset to avoid false timeout on reconnect}
handleReconnect
The handleReconnect method handles reconnection, called by ReconnectionManager.
View handleReconnect Source Code
private async handleReconnect(): Promise<void> {// Ensure old connection is cleaned upif (this.ws) {this.ws.removeAllListeners();// Only close if the connection is fully established (OPEN state)// Don't call close() on CONNECTING, CLOSING, or CLOSED statesif (this.ws.readyState === WebSocket.OPEN) {this.ws.close(1000, 'Reconnecting');}this.ws = null;}// Get fresh access token if neededawait this.tokenManager.getAccessToken();// Attempt to reconnectawait this.connectWebSocket();}
getConnectionStatus
The getConnectionStatus public method returns the current connection status.
View getConnectionStatus Source Code
public getConnectionStatus(): {connected: boolean;authenticated: boolean;reconnecting: boolean;reconnectAttempts: number;} {const reconnectState = this.reconnectionManager.getState();return {connected: this.ws?.readyState === WebSocket.OPEN,authenticated: this.isAuthenticated,reconnecting: reconnectState.isReconnecting,reconnectAttempts: reconnectState.reconnectAttempts};}
disconnect
The disconnect public method manually disconnects from the WebSocket (will not auto-reconnect).
View disconnect Source Code
public disconnect(): void {console.log('๐ Manual disconnect requested');this.reconnectionManager.disable();this.stopHeartbeat();if (this.ws) {this.ws.close(1000, 'Manual disconnect');}}
reconnect
The reconnect public method manually reconnects to the WebSocket.
View reconnect Source Code
public async reconnect(): Promise<void> {console.log('๐ Manual reconnect requested');this.reconnectionManager.enable();this.reconnectionManager.reset();await this.reconnectionManager.attempt();}
cleanup
The cleanup method will cleanup the client.
View cleanup Source Code
private cleanup(): void {// Disable reconnection before cleanupthis.reconnectionManager.disable();this.reconnectionManager.cleanup();this.stopHeartbeat();this.tokenManager.cleanup();if (this.ws) {this.ws.close();}console.log('๐งน Cleanup completed');process.exit(0);}
Sending Messages
We can send messages to the WebSocket server to call various endpoints. Any endpoint that is available in the REST API is also available in the WebSocket API.
One important endpoint that is only available in the WebSocket API and not in the REST API is user/syncrequest. For a detailed explanation of how user/syncrequest works, see the user/syncrequest guide. We will implement user/syncrequest in this page.
send
Add the send method to the TradovateWebSocketClient class.
This will allow us to call API endpoints through our websocket connection.
View send Source Code
/*** Send a message to the WebSocket server* @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')* @param body - Optional request body (will be JSON stringified if object)* @param requestId - Optional request ID (will auto-increment if not provided)* @returns The request ID used for this message*/public send(endpoint: string, body?: any, requestId?: number): number {if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {console.error('โ WebSocket is not connected');throw new Error('WebSocket is not connected');}if (!this.isAuthenticated) {console.error('โ WebSocket is not authenticated yet');throw new Error('WebSocket is not authenticated yet');}// Use provided requestId or auto-incrementconst reqId = requestId !== undefined ? requestId : this.requestIdCounter++;// Format message according to Tradovate protocol: endpoint\nrequestId\n\nbodylet message = `${endpoint}\n${reqId}\n\n`;if (body !== undefined) {// If body is an object, stringify it; otherwise use as-ismessage += typeof body === 'object' ? JSON.stringify(body) : body;}console.log(`๐ค Sending message to ${endpoint} (Request ID: ${reqId})`);this.ws.send(message);return reqId;}
For example, to call the account/list endpoint, we can do the following:
this.send("account/list");
The Full Code
Here is the complete index.ts file with all methods implemented:
View TokenManager.ts Source Code
// TokenManager.tsimport fs from "fs/promises";import fetch from "node-fetch";import {TRADOVATE_CONFIG,ENDPOINTS,TOKEN_REFRESH_INTERVAL,TOKEN_STORAGE_PATH,} from "./config.js";import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";export class TokenManager {private tokenInfo: StoredTokenInfo | null = null;private refreshTimer: NodeJS.Timeout | null = null;constructor() {this.loadTokenFromFile();}/*** Get a valid access token, refreshing if necessary*/async getAccessToken(): Promise<string> {// Check if we have a valid tokenif (this.tokenInfo && this.isTokenValid()) {return this.tokenInfo.accessToken;}// Get a new tokenconsole.log("๐ Getting new access token...");await this.fetchNewToken();if (!this.tokenInfo) {throw new Error("Failed to get access token");}return this.tokenInfo.accessToken;}/*** Get user ID from stored token info*/getUserId(): number {if (!this.tokenInfo) {throw new Error("No token info available. Please get access token first.");}return this.tokenInfo.userId;}/*** Check if current token is valid (not expired)*/private isTokenValid(): boolean {if (!this.tokenInfo) {return false;}const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();const currentTime = Date.now();const bufferTime = 5 * 60 * 1000; // 5 minute bufferreturn currentTime < expirationTime - bufferTime;}/*** Fetch a new access token from the API*/private async fetchNewToken(): Promise<void> {const authRequest: AuthRequest = {name: TRADOVATE_CONFIG.name,password: TRADOVATE_CONFIG.password,appId: TRADOVATE_CONFIG.appId,appVersion: TRADOVATE_CONFIG.appVersion,sec: TRADOVATE_CONFIG.sec,cid: TRADOVATE_CONFIG.cid,};try {console.log(`๐ก Making auth request to: ${ENDPOINTS.AUTH_URL}`);const response = await fetch(ENDPOINTS.AUTH_URL, {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify(authRequest),});if (!response.ok) {const errorText = await response.text();throw new Error(`Auth request failed: ${response.status} ${response.statusText} - ${errorText}`);}const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;// Store with retrieval timestampthis.tokenInfo = {...tokenResponse,retrievedAt: Date.now(),};// Save to fileawait this.saveTokenToFile();// Setup auto-refreshthis.setupTokenRefresh();console.log("โ Access token retrieved successfully");console.log(`๐ค User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);console.log(`๐ข Organization: ${this.tokenInfo.orgName}`);console.log(`โฐ Expires: ${this.tokenInfo.expirationTime}`);} catch (error) {console.error("โ Failed to fetch access token:", error);throw error;}}/*** Load token from storage file*/private async loadTokenFromFile(): Promise<void> {try {const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;console.log("โ Loaded access token from storage");// Setup refresh timer if token is still validif (this.isTokenValid()) {this.setupTokenRefresh();}} catch (error) {console.log("๐ No existing token file found or invalid token");this.tokenInfo = null;}}/*** Save token to storage file*/private async saveTokenToFile(): Promise<void> {if (!this.tokenInfo) {return;}try {await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));console.log("๐พ Access token saved to storage");} catch (error) {console.error("โ Failed to save token to file:", error);}}/*** Setup automatic token refresh*/private setupTokenRefresh(): void {if (this.refreshTimer) {clearTimeout(this.refreshTimer);}console.log(`โฐ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);this.refreshTimer = setTimeout(async () => {console.log("๐ Refreshing access token...");try {await this.fetchNewToken();console.log("โ Token refreshed successfully");} catch (error) {console.error("โ Failed to refresh token:", error);}}, TOKEN_REFRESH_INTERVAL);}/*** Clear the refresh timer*/public cleanup(): void {if (this.refreshTimer) {clearTimeout(this.refreshTimer);this.refreshTimer = null;console.log("๐งน Token refresh timer cleaned up");}}}
View config.ts Source Code
// config.tsimport dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files//The .env file for this sample is located in the root of the project, and looks like this:// TRADOVATE_NAME="your_username"// TRADOVATE_PASSWORD="your_password"// TRADOVATE_APP_ID="your_app_id"// TRADOVATE_APP_VERSION=1.0// TRADOVATE_SEC="your_secret_key"// TRADOVATE_CID=0// Load environment variables from .env filedotenv.config();export interface TradovateCredentials {name: string;password: string;appId: string;appVersion: string;sec: string;cid: number;}// Load credentials from environment variablesexport const TRADOVATE_CONFIG: TradovateCredentials = {name: process.env.TRADOVATE_NAME || "",password: process.env.TRADOVATE_PASSWORD || "",appId: process.env.TRADOVATE_APP_ID || "",appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",sec: process.env.TRADOVATE_SEC || "",cid: parseInt(process.env.TRADOVATE_CID || "0", 10),};export const ENDPOINTS = {AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for developmentWS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",} as const;export const TOKEN_REFRESH_INTERVAL = 75 * 60 * 1000; // 75 minutes in milliseconds (keep below the token's expirationTime)export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
View types.ts Source Code
// types.tsexport interface AccessTokenResponse {accessToken: string;mdAccessToken: string;expirationTime: string;userStatus: string;userId: number;name: string;hasLive: boolean;hasSimPlus: boolean;hasFunded: boolean;hasMarketData: boolean;outdatedLiquidationPolicy: boolean;outdatedSentimentPolicy: boolean;experience: string;orgName: string;}export interface StoredTokenInfo extends AccessTokenResponse {retrievedAt: number; // timestamp when token was retrieved}export interface AuthRequest {name: string;password: string;appId: string;appVersion: string;sec: string;cid: number;}export interface SocketMessage {i?: number; // request IDs?: number; // status codee?: SocketEventName;d?: any; // data payload}export type SocketEventName = "props" | "md" | "clock" | "shutdown";export type PluralEntityName = string; // This would be dynamically generated from EntityNameexport interface TdvCache {[key: string]: any[];}export type MessageEventHandler = (data: SocketMessage[]) => void;
// index.tsimport WebSocket from "ws";import readline from "readline";import { TokenManager } from "./token-manager.js";import { ReconnectionManager } from "./reconnection-manager.js";import { ENDPOINTS } from "./config.js";import { SocketMessage } from "./types.js";export class TradovateWebSocketClient {private ws: WebSocket | null = null;private tokenManager: TokenManager;private reconnectionManager: ReconnectionManager;private isAuthenticated: boolean = false;private authenticationSent: boolean = false;private heartbeatTimer: NodeJS.Timeout | null = null;private readonly heartbeatInterval: number = 2500; // 2.5 secondsprivate heartbeatsSent: number = 0;private lastServerMessageTime: number = Date.now();private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connectionprivate heartbeatTimeoutTimer: NodeJS.Timeout | null = null;private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)private syncCompleted: boolean = false;constructor() {this.tokenManager = new TokenManager();this.reconnectionManager = new ReconnectionManager({maxReconnectAttempts: 10,initialReconnectDelay: 1000, // 1 secondmaxReconnectDelay: 60000, // 60 seconds});// Setup reconnection callbacksthis.reconnectionManager.setOnReconnect(async () => {await this.handleReconnect();});this.reconnectionManager.setOnMaxAttemptsReached(() => {this.cleanup();});}async start(): Promise<void> {console.log("๐ Tradovate WebSocket Client - TypeScript Version");console.log("=".repeat(50));try {// Get access token (will fetch new one if needed)await this.tokenManager.getAccessToken();// Connect to WebSocketawait this.connectWebSocket();} catch (error) {console.error("โ Error starting client:", (error as Error).message);this.cleanup();}}private async connectWebSocket(): Promise<void> {return new Promise((resolve, reject) => {console.log("๐ Connecting to WebSocket...");console.log(`๐ก URL: ${ENDPOINTS.WS_URL}`);this.ws = new WebSocket(ENDPOINTS.WS_URL);let settled = false; // Track if promise has been settledconst handleError = (error: Error) => {console.error("โ WebSocket error:", error.message);if (!settled) {settled = true;reject(error);}};// Attach error handlerthis.ws.on("error", handleError);this.ws.on("open", () => {console.log("โ WebSocket connection established");this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connectionif (!settled) {settled = true;resolve();}});this.ws.on("message", (data) => {this.handleMessage(data);});this.ws.on("close", (code, reason) => {console.log(`๐ WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);this.resetConnectionState();// Trigger reconnection if enabledconst reconnectState = this.reconnectionManager.getState();if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {this.reconnectionManager.schedule();}});// Set connection timeoutsetTimeout(() => {if (this.ws?.readyState !== WebSocket.OPEN) {if (!settled) {settled = true;reject(new Error("WebSocket connection timeout"));}}}, 10000);});}private async authenticate(): Promise<void> {if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {console.error("โ WebSocket is not connected");return;}if (this.authenticationSent) {console.log("โญ๏ธ Authentication already sent, skipping");return;}console.log("๐ Preparing authentication message...");try {const accessToken = await this.tokenManager.getAccessToken();// Correct Tradovate format: authorize\n[request ID]\n\n[access token]const requestId = 0;const authMessage = `authorize\n${requestId}\n\n${accessToken}`;console.log("๐ค Sending authentication message...");this.ws.send(authMessage);this.authenticationSent = true;console.log("โ Authentication message sent to WebSocket");} catch (error) {console.error("โ Failed to get access token for authentication:", error);}}private handleMessage(data: WebSocket.Data): void {const rawMessage = data.toString();// Update last server message timestampthis.lastServerMessageTime = Date.now();// Handle heartbeat messages (single character messages like "o")if (rawMessage.length === 1) {console.log("๐ Received frame:", rawMessage);// If this is the first 'o' and we haven't authenticated yet, authenticate immediatelyif (rawMessage === "o" && !this.authenticationSent) {console.log("๐ Received server open frame - authenticating immediately");this.authenticate();return;}// Handle subsequent heartbeats only if authenticatedif (rawMessage === "o" && this.isAuthenticated) {this.sendHeartbeat();this.resetHeartbeatTimer();} else if (rawMessage === "o" && !this.isAuthenticated) {console.log("๐ Heartbeat received but authentication still pending");}return;}// Handle messages with 'a' prefix (array responses)if (rawMessage.startsWith("a[")) {try {// Extract JSON array from 'a[...]' formatconst jsonPart = rawMessage.substring(1); // Remove 'a' prefixconst messageArray: SocketMessage[] = JSON.parse(jsonPart);console.log("๐จ Received array response:", JSON.stringify(messageArray, null, 2));// Check if this is specifically an authentication response (request ID 0)if (Array.isArray(messageArray) && messageArray.length > 0) {const response = messageArray[0];// Authentication response has request ID 0if (response.i === 0 && !this.isAuthenticated) {if (response.s === 200) {this.isAuthenticated = true;console.log("โ Authentication successful!");// Start heartbeat timer after successful authenticationthis.startHeartbeat();} else {console.error("โ Authentication failed:", response);this.cleanup();}}// Other responses (auth/me, etc.) with different request IDselse if (response.i && response.i > 1) {console.log(`๐ฌ Response for request ID ${response.i}:`, response);}}} catch (error) {console.error("โ Error parsing array response:", (error as Error).message);console.log("Raw message:", rawMessage);}return;}// Handle regular JSON messagestry {const message: SocketMessage = JSON.parse(rawMessage);console.log("๐จ Received JSON message:", JSON.stringify(message, null, 2));} catch (error) {console.log("๐ฆ Non-JSON message received:", rawMessage);// Don't treat non-JSON messages as errors, just log them}}/*** Send a message to the WebSocket server* @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')* @param body - Optional request body (will be JSON stringified if object)* @param requestId - Optional request ID (will auto-increment if not provided)* @returns The request ID used for this message*/public send(endpoint: string, body?: any, requestId?: number): number {if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {console.error("โ WebSocket is not connected");throw new Error("WebSocket is not connected");}if (!this.isAuthenticated) {console.error("โ WebSocket is not authenticated yet");throw new Error("WebSocket is not authenticated yet");}// Use provided requestId or auto-incrementconst reqId = requestId !== undefined ? requestId : this.requestIdCounter++;// Format message according to Tradovate protocol: endpoint\nrequestId\n\nbodylet message = `${endpoint}\n${reqId}\n\n`;if (body !== undefined) {// If body is an object, stringify it; otherwise use as-ismessage += typeof body === "object" ? JSON.stringify(body) : body;}console.log(`๐ค Sending message to ${endpoint} (Request ID: ${reqId})`);this.ws.send(message);return reqId;}/*** Get the current connection status*/public getConnectionStatus(): {connected: boolean;authenticated: boolean;reconnecting: boolean;reconnectAttempts: number;} {const reconnectState = this.reconnectionManager.getState();return {connected: this.ws?.readyState === WebSocket.OPEN,authenticated: this.isAuthenticated,reconnecting: reconnectState.isReconnecting,reconnectAttempts: reconnectState.reconnectAttempts,};}/*** Manually disconnect from the WebSocket (will not auto-reconnect)*/public disconnect(): void {console.log("๐ Manual disconnect requested");this.reconnectionManager.disable();this.stopHeartbeat();if (this.ws) {this.ws.close(1000, "Manual disconnect");}}/*** Manually reconnect to the WebSocket (useful after calling disconnect)*/public async reconnect(): Promise<void> {console.log("๐ Manual reconnect requested");this.reconnectionManager.enable();this.reconnectionManager.reset();await this.reconnectionManager.attempt();}private startHeartbeat(): void {if (this.heartbeatTimer) {clearInterval(this.heartbeatTimer);}console.log("๐ Starting heartbeat timer (2.5s interval) after authentication");this.heartbeatTimer = setInterval(() => {this.sendHeartbeat();}, this.heartbeatInterval);// Start the timeout checkerthis.startHeartbeatTimeoutChecker();}private stopHeartbeat(): void {if (this.heartbeatTimer) {clearInterval(this.heartbeatTimer);this.heartbeatTimer = null;console.log("๐ Heartbeat timer stopped");}// Stop the timeout checkerthis.stopHeartbeatTimeoutChecker();}private startHeartbeatTimeoutChecker(): void {if (this.heartbeatTimeoutTimer) {clearInterval(this.heartbeatTimeoutTimer);}// Check every 5 seconds if we've heard from the server recentlythis.heartbeatTimeoutTimer = setInterval(() => {this.checkServerTimeout();}, 5000);}private stopHeartbeatTimeoutChecker(): void {if (this.heartbeatTimeoutTimer) {clearInterval(this.heartbeatTimeoutTimer);this.heartbeatTimeoutTimer = null;}}private checkServerTimeout(): void {const timeSinceLastMessage = Date.now() - this.lastServerMessageTime;if (timeSinceLastMessage > this.serverTimeoutMs) {console.error(`โ ๏ธ No server response for ${timeSinceLastMessage}ms (threshold: ${this.serverTimeoutMs}ms)`);console.error("๐ Connection appears dead, forcing reconnection...");// Force close the connection to trigger reconnection logic// Use 1000 (normal closure) or 4000-4999 (custom application codes)if (this.ws) {this.ws.close(4000, "Server timeout - no heartbeat response");}}}private resetHeartbeatTimer(): void {// Reset the timer when we receive a heartbeat from serverthis.stopHeartbeat();this.startHeartbeat();}private sendHeartbeat(): void {if (this.ws && this.ws.readyState === WebSocket.OPEN) {this.ws.send("[]");this.heartbeatsSent++;console.log(`๐ Empty frame heartbeat #${this.heartbeatsSent} sent: []`);}}/*** Reset connection-related state (called when connection is lost)*/private resetConnectionState(): void {this.isAuthenticated = false;this.authenticationSent = false;this.stopHeartbeat();this.lastServerMessageTime = Date.now(); // Reset to avoid false timeout on reconnect}/*** Handle reconnection - called by ReconnectionManager*/private async handleReconnect(): Promise<void> {// Ensure old connection is cleaned upif (this.ws) {this.ws.removeAllListeners();// Only close if the connection is fully established (OPEN state)// Don't call close() on CONNECTING, CLOSING, or CLOSED statesif (this.ws.readyState === WebSocket.OPEN) {this.ws.close(1000, "Reconnecting");}this.ws = null;}// Get fresh access token if neededawait this.tokenManager.getAccessToken();// Attempt to reconnectawait this.connectWebSocket();}private cleanup(): void {// Disable reconnection before cleanupthis.reconnectionManager.disable();this.reconnectionManager.cleanup();this.stopHeartbeat();this.tokenManager.cleanup();if (this.ws) {this.ws.close();}console.log("๐งน Cleanup completed");process.exit(0);}}// Handle unhandled rejections and errorsprocess.on("unhandledRejection", (reason, promise) => {console.error("โ ๏ธ Unhandled Rejection:", reason);// Don't exit, let reconnection logic handle it});process.on("uncaughtException", (error) => {console.error("โ ๏ธ Uncaught Exception:", error.message);// Don't exit, let reconnection logic handle it});// Handle process terminationprocess.on("SIGINT", () => {console.log("\n๐ Received SIGINT, shutting down gracefully...");process.exit(0);});process.on("SIGTERM", () => {console.log("\n๐ Received SIGTERM, shutting down gracefully...");process.exit(0);});// Start the applicationconst client = new TradovateWebSocketClient();client.start().catch((error) => {console.error("๐ฅ Fatal error:", error.message);process.exit(1);});

