> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://partner.ninjatrader.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://partner.ninjatrader.com/_mcp/server.

# 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 <code>.ts</code> files for configuration and types.

\<details>
\<summary> **View** <code>TokenManager.ts</code> Source Code\</summary>

```typescript
// TokenManager.ts

import fs from "fs/promises";
import fetch from "node-fetch";
import {
  TRADOVATE_CONFIG,
  ENDPOINTS,
  TOKEN_REFRESH_INTERVAL,
  TOKEN_STORAGE_PATH,
} from "./config.js";
import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";

export class TokenManager {
  private tokenInfo: StoredTokenInfo | null = null;
  private refreshTimer: NodeJS.Timeout | null = null;

  constructor() {
    this.loadTokenFromFile();
  }

  /**
   * Get a valid access token, refreshing if necessary
   */
  async getAccessToken(): Promise<string> {
    // Check if we have a valid token
    if (this.tokenInfo && this.isTokenValid()) {
      return this.tokenInfo.accessToken;
    }

    // Get a new token
    console.log("🔄 Getting new access token...");
    await this.fetchNewToken();

    if (!this.tokenInfo) {
      throw new Error("Failed to get access token");
    }

    return this.tokenInfo.accessToken;
  }

  /**
   * Get user ID from stored token info
   */
  getUserId(): number {
    if (!this.tokenInfo) {
      throw new Error("No token info available. Please get access token first.");
    }
    return this.tokenInfo.userId;
  }

  /**
   * Check if current token is valid (not expired)
   */
  private isTokenValid(): boolean {
    if (!this.tokenInfo) {
      return false;
    }

    const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();
    const currentTime = Date.now();
    const bufferTime = 5 * 60 * 1000; // 5 minute buffer

    return currentTime < expirationTime - bufferTime;
  }

  /**
   * Fetch a new access token from the API
   */
  private async fetchNewToken(): Promise<void> {
    const authRequest: AuthRequest = {
      name: TRADOVATE_CONFIG.name,
      password: TRADOVATE_CONFIG.password,
      appId: TRADOVATE_CONFIG.appId,
      appVersion: TRADOVATE_CONFIG.appVersion,
      sec: TRADOVATE_CONFIG.sec,
      cid: TRADOVATE_CONFIG.cid,
    };

    try {
      console.log(`📡 Making auth request to: ${ENDPOINTS.AUTH_URL}`);
      const response = await fetch(ENDPOINTS.AUTH_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(authRequest),
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(
          `Auth request failed: ${response.status} ${response.statusText} - ${errorText}`
        );
      }

      const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;

      // Store with retrieval timestamp
      this.tokenInfo = {
        ...tokenResponse,
        retrievedAt: Date.now(),
      };

      // Save to file
      await this.saveTokenToFile();

      // Setup auto-refresh
      this.setupTokenRefresh();

      console.log("✅ Access token retrieved successfully");
      console.log(`👤 User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);
      console.log(`🏢 Organization: ${this.tokenInfo.orgName}`);
      console.log(`⏰ Expires: ${this.tokenInfo.expirationTime}`);
    } catch (error) {
      console.error("❌ Failed to fetch access token:", error);
      throw error;
    }
  }

  /**
   * Load token from storage file
   */
  private async loadTokenFromFile(): Promise<void> {
    try {
      const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");
      this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;
      console.log("✅ Loaded access token from storage");

      // Setup refresh timer if token is still valid
      if (this.isTokenValid()) {
        this.setupTokenRefresh();
      }
    } catch (error) {
      console.log("📝 No existing token file found or invalid token");
      this.tokenInfo = null;
    }
  }

  /**
   * Save token to storage file
   */
  private async saveTokenToFile(): Promise<void> {
    if (!this.tokenInfo) {
      return;
    }

    try {
      await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));
      console.log("💾 Access token saved to storage");
    } catch (error) {
      console.error("❌ Failed to save token to file:", error);
    }
  }

  /**
   * Setup automatic token refresh
   */
  private setupTokenRefresh(): void {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
    }

    console.log(`⏰ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);

    this.refreshTimer = setTimeout(async () => {
      console.log("🔄 Refreshing access token...");
      try {
        await this.fetchNewToken();
        console.log("✅ Token refreshed successfully");
      } catch (error) {
        console.error("❌ Failed to refresh token:", error);
      }
    }, TOKEN_REFRESH_INTERVAL);
  }

  /**
   * Clear the refresh timer
   */
  public cleanup(): void {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = null;
      console.log("🧹 Token refresh timer cleaned up");
    }
  }
}
```

\</details>

\<details>
\<summary> **View** <code>config.ts</code> Source Code\</summary>

```typescript
// config.ts

import dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files

//The .env file for this sample is located in the root of the project, and looks like this:
// TRADOVATE_NAME="your_username"
// TRADOVATE_PASSWORD="your_password"
// TRADOVATE_APP_ID="your_app_id"
// TRADOVATE_APP_VERSION=1.0
// TRADOVATE_SEC="your_secret_key"
// TRADOVATE_CID=0

// Load environment variables from .env file
dotenv.config();

export interface TradovateCredentials {
  name: string;
  password: string;
  appId: string;
  appVersion: string;
  sec: string;
  cid: number;
}

// Load credentials from environment variables
export const TRADOVATE_CONFIG: TradovateCredentials = {
  name: process.env.TRADOVATE_NAME || "",
  password: process.env.TRADOVATE_PASSWORD || "",
  appId: process.env.TRADOVATE_APP_ID || "",
  appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",
  sec: process.env.TRADOVATE_SEC || "",
  cid: parseInt(process.env.TRADOVATE_CID || "0", 10),
};

export const ENDPOINTS = {
  AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for development
  WS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",
} as const;

export const TOKEN_REFRESH_INTERVAL = 85 * 60 * 1000; // 85 minutes in milliseconds
export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
```

\</details>

\<details>
\<summary> **View** <code>types.ts</code> Source Code\</summary>

```typescript
// types.ts

export interface AccessTokenResponse {
  accessToken: string;
  mdAccessToken: string;
  expirationTime: string;
  userStatus: string;
  userId: number;
  name: string;
  hasLive: boolean;
  hasSimPlus: boolean;
  hasFunded: boolean;
  hasMarketData: boolean;
  outdatedLiquidationPolicy: boolean;
  outdatedSentimentPolicy: boolean;
  experience: string;
  orgName: string;
}

export interface StoredTokenInfo extends AccessTokenResponse {
  retrievedAt: number; // timestamp when token was retrieved
}

export interface AuthRequest {
  name: string;
  password: string;
  appId: string;
  appVersion: string;
  sec: string;
  cid: number;
}

export interface SocketMessage {
  i?: number; // request ID
  s?: number; // status code
  e?: SocketEventName;
  d?: any; // data payload
}

export type SocketEventName = "props" | "md" | "clock" | "shutdown";

export type PluralEntityName = string; // This would be dynamically generated from EntityName

export interface TdvCache {
  [key: string]: any[];
}

export type MessageEventHandler = (data: SocketMessage[]) => void;
```

\</details>

\<details>
\<summary> **View** <code>reconnection-manager.ts</code> Source Code\</summary>

```typescript
// reconnection-manager.ts

/**
 * ReconnectionManager
 *
 * Handles automatic reconnection logic with exponential backoff for WebSocket connections.
 */
export class ReconnectionManager {
  private reconnectAttempts: number = 0;
  private readonly maxReconnectAttempts: number;
  private readonly initialReconnectDelay: number;
  private readonly maxReconnectDelay: number;
  private reconnectTimer: NodeJS.Timeout | null = null;
  private shouldReconnect: boolean = true;
  private isReconnecting: boolean = false;
  private onReconnectCallback: (() => Promise<void>) | null = null;
  private onMaxAttemptsReached: (() => void) | null = null;

  constructor(
    options: {
      maxReconnectAttempts?: number;
      initialReconnectDelay?: number;
      maxReconnectDelay?: number;
    } = {}
  ) {
    this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
    this.initialReconnectDelay = options.initialReconnectDelay ?? 1000; // 1 second
    this.maxReconnectDelay = options.maxReconnectDelay ?? 60000; // 60 seconds
  }

  /**
   * Set the callback to execute when attempting to reconnect
   */
  public setOnReconnect(callback: () => Promise<void>): void {
    this.onReconnectCallback = callback;
  }

  /**
   * Set the callback to execute when max reconnection attempts are reached
   */
  public setOnMaxAttemptsReached(callback: () => void): void {
    this.onMaxAttemptsReached = callback;
  }

  /**
   * Get the current reconnection state
   */
  public getState(): {
    isReconnecting: boolean;
    reconnectAttempts: number;
    shouldReconnect: boolean;
  } {
    return {
      isReconnecting: this.isReconnecting,
      reconnectAttempts: this.reconnectAttempts,
      shouldReconnect: this.shouldReconnect,
    };
  }

  /**
   * Enable automatic reconnection
   */
  public enable(): void {
    this.shouldReconnect = true;
  }

  /**
   * Disable automatic reconnection and cancel any pending attempts
   */
  public disable(): void {
    this.shouldReconnect = false;
    this.cancel();
  }

  /**
   * Reset the reconnection counter (useful after successful connection)
   */
  public reset(): void {
    this.reconnectAttempts = 0;
    this.isReconnecting = false;
  }

  /**
   * Calculate exponential backoff delay
   * Formula: min(initialDelay * 2^attempt, maxDelay) + jitter
   */
  private calculateBackoffDelay(): number {
    const exponentialDelay = this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts);
    const cappedDelay = Math.min(exponentialDelay, this.maxReconnectDelay);

    // Add random jitter (0-10% of delay) to avoid thundering herd
    const jitter = Math.random() * cappedDelay * 0.1;

    return Math.floor(cappedDelay + jitter);
  }

  /**
   * Schedule a reconnection attempt with exponential backoff
   */
  public schedule(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error(
        `❌ Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`
      );
      if (this.onMaxAttemptsReached) {
        this.onMaxAttemptsReached();
      }
      return;
    }

    const delay = this.calculateBackoffDelay();
    console.log(
      `🔄 Scheduling reconnection attempt ${this.reconnectAttempts + 1}/${
        this.maxReconnectAttempts
      } in ${delay}ms...`
    );

    this.reconnectTimer = setTimeout(() => {
      this.attempt();
    }, delay);
  }

  /**
   * Attempt to reconnect immediately
   */
  public async attempt(): Promise<void> {
    if (!this.shouldReconnect) {
      console.log("⏹️ Reconnection cancelled - reconnection disabled");
      return;
    }

    if (!this.onReconnectCallback) {
      console.error("❌ No reconnection callback set");
      return;
    }

    this.reconnectAttempts++;
    this.isReconnecting = true;

    console.log(
      `🔄 Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`
    );

    try {
      await this.onReconnectCallback();

      // If successful, reset reconnection counter
      this.reconnectAttempts = 0;
      this.isReconnecting = false;
      console.log("✅ Reconnection successful!");
    } catch (error) {
      console.error(
        `❌ Reconnection attempt ${this.reconnectAttempts} failed:`,
        (error as Error).message
      );
      this.isReconnecting = false;

      // Schedule next attempt
      this.schedule();
    }
  }

  /**
   * Cancel any pending reconnection attempts
   */
  public cancel(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }

  /**
   * Cleanup resources
   */
  public cleanup(): void {
    this.cancel();
    this.onReconnectCallback = null;
    this.onMaxAttemptsReached = null;
  }
}
```

\</details>

### Initial boilerplate code for our WebSocket client:

```typescript
// index.ts

import WebSocket from "ws";
import readline from "readline";
import { TokenManager } from "./token-manager.js";
import { ReconnectionManager } from "./reconnection-manager.js";
import { ENDPOINTS } from "./config.js";
import { SocketMessage } from "./types.js";

export class TradovateWebSocketClient {
  private ws: WebSocket | null = null;
  private tokenManager: TokenManager;
  private reconnectionManager: ReconnectionManager;
  private isAuthenticated: boolean = false;
  private authenticationSent: boolean = false;
  private heartbeatTimer: NodeJS.Timeout | null = null;
  private readonly heartbeatInterval: number = 2500; // 2.5 seconds
  private heartbeatsSent: number = 0;
  private lastServerMessageTime: number = Date.now();
  private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connection
  private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;
  private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)
  private syncCompleted: boolean = false;

  constructor() {
    this.tokenManager = new TokenManager();
    this.reconnectionManager = new ReconnectionManager({
      maxReconnectAttempts: 10,
      initialReconnectDelay: 1000, // 1 second
      maxReconnectDelay: 60000, // 60 seconds
    });

    // Setup reconnection callbacks
    this.reconnectionManager.setOnReconnect(async () => {
      await this.handleReconnect();
    });

    this.reconnectionManager.setOnMaxAttemptsReached(() => {
      this.cleanup();
    });
  }

  async start(): Promise<void> {}

  private async connectWebSocket(): Promise<void> {
    return new Promise((resolve, reject) => {});
  }

  private async authenticate(): Promise<void> {}

  private handleMessage(data: WebSocket.Data): void {}

  private startHeartbeat(): void {}

  private stopHeartbeat(): void {}

  private resetHeartbeatTimer(): void {}

  private sendHeartbeat(): void {}

  private resetConnectionState(): void {}

  private async handleReconnect(): Promise<void> {}

  private cleanup(): void {}
}

// Handle unhandled rejections and errors
process.on("unhandledRejection", (reason, promise) => {
  console.error("⚠️ Unhandled Rejection:", reason);
  // Don't exit, let reconnection logic handle it
});

process.on("uncaughtException", (error) => {
  console.error("⚠️ Uncaught Exception:", error.message);
  // Don't exit, let reconnection logic handle it
});

// Handle process termination
process.on("SIGINT", () => {
  console.log("\n🛑 Received SIGINT, shutting down gracefully...");
  process.exit(0);
});

process.on("SIGTERM", () => {
  console.log("\n🛑 Received SIGTERM, shutting down gracefully...");
  process.exit(0);
});

// Start the application
const client = new TradovateWebSocketClient();
client.start().catch((error) => {
  console.error("💥 Fatal error:", error.message);
  process.exit(1);
});
```

### Step 2: Define our WebSocket Client Methods for basic functionality

***

#### <code>start</code>

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

<details open>
  <summary>
    **View**

     

    <code>start</code>

     Source Code
  </summary>

  ```typescript
  async start(): Promise<void> {
      console.log('🚀 Tradovate WebSocket Client - TypeScript Version');
      console.log('='.repeat(50));

      try {
        // Get access token (will fetch new one if needed)
        await this.tokenManager.getAccessToken();

        // Connect to WebSocket
        await this.connectWebSocket();

      } catch (error) {
        console.error('❌ Error starting client:', (error as Error).message);
        this.cleanup();
      }
    }
  ```
</details>

***

#### <code>connectWebSocket</code>

The <code>connectWebSocket</code> method will connect to the WebSocket server and authenticate the client.

\<details open>
\<summary>**View** <code>connectWebSocket</code> Source Code\</summary>

```typescript
private async connectWebSocket(): Promise<void> {
    return new Promise((resolve, reject) => {
      console.log('🔌 Connecting to WebSocket...');
      console.log(`📡 URL: ${ENDPOINTS.WS_URL}`);

      this.ws = new WebSocket(ENDPOINTS.WS_URL);
      let settled = false; // Track if promise has been settled

      const handleError = (error: Error) => {
        console.error('❌ WebSocket error:', error.message);
        if (!settled) {
          settled = true;
          reject(error);
        }
      };

      // Attach error handler
      this.ws.on('error', handleError);

      this.ws.on('open', () => {
        console.log('✅ WebSocket connection established');
        this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connection
        if (!settled) {
          settled = true;
          resolve();
        }
      });

      this.ws.on('message', (data) => {
        this.handleMessage(data);
      });

      this.ws.on('close', (code, reason) => {
        console.log(`🔌 WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);
        this.resetConnectionState();

        // Trigger reconnection if enabled
        const reconnectState = this.reconnectionManager.getState();
        if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {
          this.reconnectionManager.schedule();
        }
      });

      // Set connection timeout
      setTimeout(() => {
        if (this.ws?.readyState !== WebSocket.OPEN) {
          if (!settled) {
            settled = true;
            reject(new Error('WebSocket connection timeout'));
          }
        }
      }, 10000);
    });
  }
```

\</details>

***

#### <code>authenticate</code>

The <code>authenticate</code> method will authenticate the client with the WebSocket server using our access token.

\<details open>
\<summary>**View** <code>authenticate</code> Source Code\</summary>

```typescript
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);
    }
  }
```

\</details>

***

#### <code>handleMessage</code>

The <code>handleMessage</code> method will handle the incoming messages from the WebSocket server.

\<details open>
\<summary>**View** <code>handleMessage</code> Source Code\</summary>

```typescript
private handleMessage(data: WebSocket.Data): void {
    const rawMessage = data.toString();

    // Update last server message timestamp
    this.lastServerMessageTime = Date.now();

    // Handle heartbeat messages (single character messages like "o")
    if (rawMessage.length === 1) {
      console.log('💓 Received frame:', rawMessage);

      // If this is the first 'o' and we haven't authenticated yet, authenticate immediately
      if (rawMessage === 'o' && !this.authenticationSent) {
        console.log('🚀 Received server open frame - authenticating immediately');
        this.authenticate();
        return;
      }

      // Handle subsequent heartbeats only if authenticated
      if (rawMessage === 'o' && this.isAuthenticated) {
        this.sendHeartbeat();
        this.resetHeartbeatTimer();
      } else if (rawMessage === 'o' && !this.isAuthenticated) {
        console.log('💓 Heartbeat received but authentication still pending');
      }
      return;
    }

    // Handle messages with 'a' prefix (array responses)
    if (rawMessage.startsWith('a[')) {
      try {
        // Extract JSON array from 'a[...]' format
        const jsonPart = rawMessage.substring(1); // Remove 'a' prefix
        const messageArray: SocketMessage[] = JSON.parse(jsonPart);
        console.log('📨 Received array response:', JSON.stringify(messageArray, null, 2));

        // Check if this is specifically an authentication response (request ID 0)
        if (Array.isArray(messageArray) && messageArray.length > 0) {
          const response = messageArray[0];

          // Authentication response has request ID 0
          if (response.i === 0 && !this.isAuthenticated) {
            if (response.s === 200) {
              this.isAuthenticated = true;
              console.log('✅ Authentication successful!');
              // Start heartbeat timer after successful authentication
              this.startHeartbeat();
            } else {
              console.error('❌ Authentication failed:', response);
              this.cleanup();
            }
          }
          // Other responses (auth/me, etc.) with different request IDs
          else if (response.i && response.i > 1) {
            console.log(`📬 Response for request ID ${response.i}:`, response);
          }
        }
      } catch (error) {
        console.error('❌ Error parsing array response:', (error as Error).message);
        console.log('Raw message:', rawMessage);
      }
      return;
    }

    // Handle regular JSON messages
    try {
      const message: SocketMessage = JSON.parse(rawMessage);
      console.log('📨 Received JSON message:', JSON.stringify(message, null, 2));

    } catch (error) {
      console.log('📦 Non-JSON message received:', rawMessage);
      // Don't treat non-JSON messages as errors, just log them
    }
  }
```

\</details>

***

#### <code>startHeartbeat</code>

The <code>startHeartbeat</code> method starts the heartbeat timer and timeout checker.

\<details open>
\<summary>**View** <code>startHeartbeat</code> Source Code\</summary>

```typescript
private startHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
    }
    console.log('💓 Starting heartbeat timer (2.5s interval) after authentication');
    this.heartbeatTimer = setInterval(() => {
      this.sendHeartbeat();
    }, this.heartbeatInterval);

    // Start the timeout checker
    this.startHeartbeatTimeoutChecker();
  }
```

\</details>

***

#### <code>stopHeartbeat</code>

The <code>stopHeartbeat</code> method will stop the heartbeats and timeout checker.

\<details open>
\<summary>**View** <code>stopHeartbeat</code> Source Code\</summary>

```typescript
private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
      console.log('💓 Heartbeat timer stopped');
    }

    // Stop the timeout checker
    this.stopHeartbeatTimeoutChecker();
  }
```

\</details>

***

#### <code>resetHeartbeatTimer</code>

The <code>resetHeartbeatTimer</code> method will reset the heartbeat timer.

\<details open>
\<summary>**View** <code>resetHeartbeatTimer</code> Source Code\</summary>

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

\</details>

***

#### <code>sendHeartbeat</code>

The <code>sendHeartbeat</code> method will send the heartbeat message.

\<details open>
\<summary>**View** <code>sendHeartbeat</code> Source Code\</summary>

```typescript
private sendHeartbeat(): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send('[]');
      this.heartbeatsSent++;
      console.log(`💓 Empty frame heartbeat #${this.heartbeatsSent} sent: []`);
    }
  }
```

\</details>

***

#### <code>startHeartbeatTimeoutChecker</code>

The <code>startHeartbeatTimeoutChecker</code> method checks for server timeouts every 5 seconds.

\<details open>
\<summary>**View** <code>startHeartbeatTimeoutChecker</code> Source Code\</summary>

```typescript
private startHeartbeatTimeoutChecker(): void {
    if (this.heartbeatTimeoutTimer) {
      clearInterval(this.heartbeatTimeoutTimer);
    }

    // Check every 5 seconds if we've heard from the server recently
    this.heartbeatTimeoutTimer = setInterval(() => {
      this.checkServerTimeout();
    }, 5000);
  }
```

\</details>

***

#### <code>stopHeartbeatTimeoutChecker</code>

The <code>stopHeartbeatTimeoutChecker</code> method stops the timeout checker.

\<details open>
\<summary>**View** <code>stopHeartbeatTimeoutChecker</code> Source Code\</summary>

```typescript
private stopHeartbeatTimeoutChecker(): void {
    if (this.heartbeatTimeoutTimer) {
      clearInterval(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }
```

\</details>

***

#### <code>checkServerTimeout</code>

The <code>checkServerTimeout</code> method checks if the server has stopped responding and forces reconnection if needed.

\<details open>
\<summary>**View** <code>checkServerTimeout</code> Source Code\</summary>

```typescript
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');
      }
    }
  }
```

\</details>

***

#### <code>resetConnectionState</code>

The <code>resetConnectionState</code> method resets connection-related state when connection is lost.

\<details open>
\<summary>**View** <code>resetConnectionState</code> Source Code\</summary>

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

\</details>

***

#### <code>handleReconnect</code>

The <code>handleReconnect</code> method handles reconnection, called by ReconnectionManager.

\<details open>
\<summary>**View** <code>handleReconnect</code> Source Code\</summary>

```typescript
private async handleReconnect(): Promise<void> {
    // Ensure old connection is cleaned up
    if (this.ws) {
      this.ws.removeAllListeners();
      // Only close if the connection is fully established (OPEN state)
      // Don't call close() on CONNECTING, CLOSING, or CLOSED states
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.close(1000, 'Reconnecting');
      }
      this.ws = null;
    }

    // Get fresh access token if needed
    await this.tokenManager.getAccessToken();

    // Attempt to reconnect
    await this.connectWebSocket();
  }
```

\</details>

***

#### <code>getConnectionStatus</code>

The <code>getConnectionStatus</code> public method returns the current connection status.

\<details open>
\<summary>**View** <code>getConnectionStatus</code> Source Code\</summary>

```typescript
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
    };
  }
```

\</details>

***

#### <code>disconnect</code>

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

\<details open>
\<summary>**View** <code>disconnect</code> Source Code\</summary>

```typescript
public disconnect(): void {
    console.log('🔌 Manual disconnect requested');
    this.reconnectionManager.disable();
    this.stopHeartbeat();

    if (this.ws) {
      this.ws.close(1000, 'Manual disconnect');
    }
  }
```

\</details>

***

#### <code>reconnect</code>

The <code>reconnect</code> public method manually reconnects to the WebSocket.

\<details open>
\<summary>**View** <code>reconnect</code> Source Code\</summary>

```typescript
public async reconnect(): Promise<void> {
    console.log('🔄 Manual reconnect requested');
    this.reconnectionManager.enable();
    this.reconnectionManager.reset();

    await this.reconnectionManager.attempt();
  }
```

\</details>

***

#### <code>cleanup</code>

The <code>cleanup</code> method will cleanup the client.

\<details open>
\<summary>**View** <code>cleanup</code> Source Code\</summary>

```typescript
private cleanup(): void {
    // Disable reconnection before cleanup
    this.reconnectionManager.disable();
    this.reconnectionManager.cleanup();
    this.stopHeartbeat();
    this.tokenManager.cleanup();

    if (this.ws) {
      this.ws.close();
    }

    console.log('🧹 Cleanup completed');
    process.exit(0);
  }
```

\</details>

***

## 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 <code>user/syncrequest</code>. For a detailed explanation of how <code>user/syncrequest</code> works, see the [user/syncrequest guide](/overview/core-concepts/web-sockets/user-syncrequest). We will implement <code>user/syncrequest</code> in this page.

### <code>send</code>

Add the <code>send</code> method to the <code>TradovateWebSocketClient</code> class.

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

\<details open>
\<summary>**View** <code>send</code> Source Code\</summary>

```typescript
/**
 * Send a message to the WebSocket server
 * @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')
 * @param body - Optional request body (will be JSON stringified if object)
 * @param requestId - Optional request ID (will auto-increment if not provided)
 * @returns The request ID used for this message
 */
public send(endpoint: string, body?: any, requestId?: number): number {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      console.error('❌ WebSocket is not connected');
      throw new Error('WebSocket is not connected');
    }

    if (!this.isAuthenticated) {
      console.error('❌ WebSocket is not authenticated yet');
      throw new Error('WebSocket is not authenticated yet');
    }

    // Use provided requestId or auto-increment
    const reqId = requestId !== undefined ? requestId : this.requestIdCounter++;

    // Format message according to Tradovate protocol: endpoint\nrequestId\n\nbody
    let message = `${endpoint}\n${reqId}\n\n`;

    if (body !== undefined) {
      // If body is an object, stringify it; otherwise use as-is
      message += typeof body === 'object' ? JSON.stringify(body) : body;
    }

    console.log(`📤 Sending message to ${endpoint} (Request ID: ${reqId})`);
    this.ws.send(message);

    return reqId;
  }
```

\</details>

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

```typescript
this.send("account/list");
```

***

## The Full Code

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

\<details>
\<summary> **View** <code>TokenManager.ts</code> Source Code\</summary>

```typescript
// TokenManager.ts

import fs from "fs/promises";
import fetch from "node-fetch";
import {
  TRADOVATE_CONFIG,
  ENDPOINTS,
  TOKEN_REFRESH_INTERVAL,
  TOKEN_STORAGE_PATH,
} from "./config.js";
import { AccessTokenResponse, StoredTokenInfo, AuthRequest } from "./types.js";

export class TokenManager {
  private tokenInfo: StoredTokenInfo | null = null;
  private refreshTimer: NodeJS.Timeout | null = null;

  constructor() {
    this.loadTokenFromFile();
  }

  /**
   * Get a valid access token, refreshing if necessary
   */
  async getAccessToken(): Promise<string> {
    // Check if we have a valid token
    if (this.tokenInfo && this.isTokenValid()) {
      return this.tokenInfo.accessToken;
    }

    // Get a new token
    console.log("🔄 Getting new access token...");
    await this.fetchNewToken();

    if (!this.tokenInfo) {
      throw new Error("Failed to get access token");
    }

    return this.tokenInfo.accessToken;
  }

  /**
   * Get user ID from stored token info
   */
  getUserId(): number {
    if (!this.tokenInfo) {
      throw new Error("No token info available. Please get access token first.");
    }
    return this.tokenInfo.userId;
  }

  /**
   * Check if current token is valid (not expired)
   */
  private isTokenValid(): boolean {
    if (!this.tokenInfo) {
      return false;
    }

    const expirationTime = new Date(this.tokenInfo.expirationTime).getTime();
    const currentTime = Date.now();
    const bufferTime = 5 * 60 * 1000; // 5 minute buffer

    return currentTime < expirationTime - bufferTime;
  }

  /**
   * Fetch a new access token from the API
   */
  private async fetchNewToken(): Promise<void> {
    const authRequest: AuthRequest = {
      name: TRADOVATE_CONFIG.name,
      password: TRADOVATE_CONFIG.password,
      appId: TRADOVATE_CONFIG.appId,
      appVersion: TRADOVATE_CONFIG.appVersion,
      sec: TRADOVATE_CONFIG.sec,
      cid: TRADOVATE_CONFIG.cid,
    };

    try {
      console.log(`📡 Making auth request to: ${ENDPOINTS.AUTH_URL}`);
      const response = await fetch(ENDPOINTS.AUTH_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(authRequest),
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(
          `Auth request failed: ${response.status} ${response.statusText} - ${errorText}`
        );
      }

      const tokenResponse: AccessTokenResponse = (await response.json()) as AccessTokenResponse;

      // Store with retrieval timestamp
      this.tokenInfo = {
        ...tokenResponse,
        retrievedAt: Date.now(),
      };

      // Save to file
      await this.saveTokenToFile();

      // Setup auto-refresh
      this.setupTokenRefresh();

      console.log("✅ Access token retrieved successfully");
      console.log(`👤 User: ${this.tokenInfo.name} (ID: ${this.tokenInfo.userId})`);
      console.log(`🏢 Organization: ${this.tokenInfo.orgName}`);
      console.log(`⏰ Expires: ${this.tokenInfo.expirationTime}`);
    } catch (error) {
      console.error("❌ Failed to fetch access token:", error);
      throw error;
    }
  }

  /**
   * Load token from storage file
   */
  private async loadTokenFromFile(): Promise<void> {
    try {
      const fileContent = await fs.readFile(TOKEN_STORAGE_PATH, "utf-8");
      this.tokenInfo = JSON.parse(fileContent) as StoredTokenInfo;
      console.log("✅ Loaded access token from storage");

      // Setup refresh timer if token is still valid
      if (this.isTokenValid()) {
        this.setupTokenRefresh();
      }
    } catch (error) {
      console.log("📝 No existing token file found or invalid token");
      this.tokenInfo = null;
    }
  }

  /**
   * Save token to storage file
   */
  private async saveTokenToFile(): Promise<void> {
    if (!this.tokenInfo) {
      return;
    }

    try {
      await fs.writeFile(TOKEN_STORAGE_PATH, JSON.stringify(this.tokenInfo, null, 2));
      console.log("💾 Access token saved to storage");
    } catch (error) {
      console.error("❌ Failed to save token to file:", error);
    }
  }

  /**
   * Setup automatic token refresh
   */
  private setupTokenRefresh(): void {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
    }

    console.log(`⏰ Setting up token refresh in ${TOKEN_REFRESH_INTERVAL / 60000} minutes`);

    this.refreshTimer = setTimeout(async () => {
      console.log("🔄 Refreshing access token...");
      try {
        await this.fetchNewToken();
        console.log("✅ Token refreshed successfully");
      } catch (error) {
        console.error("❌ Failed to refresh token:", error);
      }
    }, TOKEN_REFRESH_INTERVAL);
  }

  /**
   * Clear the refresh timer
   */
  public cleanup(): void {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = null;
      console.log("🧹 Token refresh timer cleaned up");
    }
  }
}
```

\</details>

\<details>
\<summary> **View** <code>config.ts</code> Source Code\</summary>

```typescript
// config.ts

import dotenv from "dotenv"; //See https://www.dotenv.org/docs/ for more information on how to use .env files

//The .env file for this sample is located in the root of the project, and looks like this:
// TRADOVATE_NAME="your_username"
// TRADOVATE_PASSWORD="your_password"
// TRADOVATE_APP_ID="your_app_id"
// TRADOVATE_APP_VERSION=1.0
// TRADOVATE_SEC="your_secret_key"
// TRADOVATE_CID=0

// Load environment variables from .env file
dotenv.config();

export interface TradovateCredentials {
  name: string;
  password: string;
  appId: string;
  appVersion: string;
  sec: string;
  cid: number;
}

// Load credentials from environment variables
export const TRADOVATE_CONFIG: TradovateCredentials = {
  name: process.env.TRADOVATE_NAME || "",
  password: process.env.TRADOVATE_PASSWORD || "",
  appId: process.env.TRADOVATE_APP_ID || "",
  appVersion: process.env.TRADOVATE_APP_VERSION || "1.0",
  sec: process.env.TRADOVATE_SEC || "",
  cid: parseInt(process.env.TRADOVATE_CID || "0", 10),
};

export const ENDPOINTS = {
  AUTH_URL: "https://live-api.staging.ninjatrader.dev/v1/auth/accesstokenrequest", //using the staging API for development
  WS_URL: "wss://live-api.staging.ninjatrader.dev/v1/websocket",
} as const;

export const TOKEN_REFRESH_INTERVAL = 85 * 60 * 1000; // 85 minutes in milliseconds
export const TOKEN_STORAGE_PATH = "./access-token.json"; //file path to store the access token
```

\</details>

\<details>
\<summary> **View** <code>types.ts</code> Source Code\</summary>

```typescript
// types.ts

export interface AccessTokenResponse {
  accessToken: string;
  mdAccessToken: string;
  expirationTime: string;
  userStatus: string;
  userId: number;
  name: string;
  hasLive: boolean;
  hasSimPlus: boolean;
  hasFunded: boolean;
  hasMarketData: boolean;
  outdatedLiquidationPolicy: boolean;
  outdatedSentimentPolicy: boolean;
  experience: string;
  orgName: string;
}

export interface StoredTokenInfo extends AccessTokenResponse {
  retrievedAt: number; // timestamp when token was retrieved
}

export interface AuthRequest {
  name: string;
  password: string;
  appId: string;
  appVersion: string;
  sec: string;
  cid: number;
}

export interface SocketMessage {
  i?: number; // request ID
  s?: number; // status code
  e?: SocketEventName;
  d?: any; // data payload
}

export type SocketEventName = "props" | "md" | "clock" | "shutdown";

export type PluralEntityName = string; // This would be dynamically generated from EntityName

export interface TdvCache {
  [key: string]: any[];
}

export type MessageEventHandler = (data: SocketMessage[]) => void;
```

\</details>

```typescript
// index.ts

import WebSocket from "ws";
import readline from "readline";
import { TokenManager } from "./token-manager.js";
import { ReconnectionManager } from "./reconnection-manager.js";
import { ENDPOINTS } from "./config.js";
import { SocketMessage } from "./types.js";

export class TradovateWebSocketClient {
  private ws: WebSocket | null = null;
  private tokenManager: TokenManager;
  private reconnectionManager: ReconnectionManager;
  private isAuthenticated: boolean = false;
  private authenticationSent: boolean = false;
  private heartbeatTimer: NodeJS.Timeout | null = null;
  private readonly heartbeatInterval: number = 2500; // 2.5 seconds
  private heartbeatsSent: number = 0;
  private lastServerMessageTime: number = Date.now();
  private readonly serverTimeoutMs: number = 10000; // 10 seconds without server message = dead connection
  private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;
  private requestIdCounter: number = 2; // Start at 2 (0=auth, 1=sync)
  private syncCompleted: boolean = false;

  constructor() {
    this.tokenManager = new TokenManager();
    this.reconnectionManager = new ReconnectionManager({
      maxReconnectAttempts: 10,
      initialReconnectDelay: 1000, // 1 second
      maxReconnectDelay: 60000, // 60 seconds
    });

    // Setup reconnection callbacks
    this.reconnectionManager.setOnReconnect(async () => {
      await this.handleReconnect();
    });

    this.reconnectionManager.setOnMaxAttemptsReached(() => {
      this.cleanup();
    });
  }

  async start(): Promise<void> {
    console.log("🚀 Tradovate WebSocket Client - TypeScript Version");
    console.log("=".repeat(50));

    try {
      // Get access token (will fetch new one if needed)
      await this.tokenManager.getAccessToken();

      // Connect to WebSocket
      await this.connectWebSocket();
    } catch (error) {
      console.error("❌ Error starting client:", (error as Error).message);
      this.cleanup();
    }
  }

  private async connectWebSocket(): Promise<void> {
    return new Promise((resolve, reject) => {
      console.log("🔌 Connecting to WebSocket...");
      console.log(`📡 URL: ${ENDPOINTS.WS_URL}`);

      this.ws = new WebSocket(ENDPOINTS.WS_URL);
      let settled = false; // Track if promise has been settled

      const handleError = (error: Error) => {
        console.error("❌ WebSocket error:", error.message);
        if (!settled) {
          settled = true;
          reject(error);
        }
      };

      // Attach error handler
      this.ws.on("error", handleError);

      this.ws.on("open", () => {
        console.log("✅ WebSocket connection established");
        this.lastServerMessageTime = Date.now(); // Reset timeout tracking on new connection
        if (!settled) {
          settled = true;
          resolve();
        }
      });

      this.ws.on("message", (data) => {
        this.handleMessage(data);
      });

      this.ws.on("close", (code, reason) => {
        console.log(`🔌 WebSocket connection closed. Code: ${code}, Reason: ${reason.toString()}`);
        this.resetConnectionState();

        // Trigger reconnection if enabled
        const reconnectState = this.reconnectionManager.getState();
        if (reconnectState.shouldReconnect && !reconnectState.isReconnecting) {
          this.reconnectionManager.schedule();
        }
      });

      // Set connection timeout
      setTimeout(() => {
        if (this.ws?.readyState !== WebSocket.OPEN) {
          if (!settled) {
            settled = true;
            reject(new Error("WebSocket connection timeout"));
          }
        }
      }, 10000);
    });
  }

  private async authenticate(): Promise<void> {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      console.error("❌ WebSocket is not connected");
      return;
    }

    if (this.authenticationSent) {
      console.log("⏭️ Authentication already sent, skipping");
      return;
    }

    console.log("🔐 Preparing authentication message...");

    try {
      const accessToken = await this.tokenManager.getAccessToken();

      // Correct Tradovate format: authorize\n[request ID]\n\n[access token]
      const requestId = 0;
      const authMessage = `authorize\n${requestId}\n\n${accessToken}`;

      console.log("📤 Sending authentication message...");
      this.ws.send(authMessage);
      this.authenticationSent = true;
      console.log("✅ Authentication message sent to WebSocket");
    } catch (error) {
      console.error("❌ Failed to get access token for authentication:", error);
    }
  }

  private handleMessage(data: WebSocket.Data): void {
    const rawMessage = data.toString();

    // Update last server message timestamp
    this.lastServerMessageTime = Date.now();

    // Handle heartbeat messages (single character messages like "o")
    if (rawMessage.length === 1) {
      console.log("💓 Received frame:", rawMessage);

      // If this is the first 'o' and we haven't authenticated yet, authenticate immediately
      if (rawMessage === "o" && !this.authenticationSent) {
        console.log("🚀 Received server open frame - authenticating immediately");
        this.authenticate();
        return;
      }

      // Handle subsequent heartbeats only if authenticated
      if (rawMessage === "o" && this.isAuthenticated) {
        this.sendHeartbeat();
        this.resetHeartbeatTimer();
      } else if (rawMessage === "o" && !this.isAuthenticated) {
        console.log("💓 Heartbeat received but authentication still pending");
      }
      return;
    }

    // Handle messages with 'a' prefix (array responses)
    if (rawMessage.startsWith("a[")) {
      try {
        // Extract JSON array from 'a[...]' format
        const jsonPart = rawMessage.substring(1); // Remove 'a' prefix
        const messageArray: SocketMessage[] = JSON.parse(jsonPart);
        console.log("📨 Received array response:", JSON.stringify(messageArray, null, 2));

        // Check if this is specifically an authentication response (request ID 0)
        if (Array.isArray(messageArray) && messageArray.length > 0) {
          const response = messageArray[0];

          // Authentication response has request ID 0
          if (response.i === 0 && !this.isAuthenticated) {
            if (response.s === 200) {
              this.isAuthenticated = true;
              console.log("✅ Authentication successful!");
              // Start heartbeat timer after successful authentication
              this.startHeartbeat();
            } else {
              console.error("❌ Authentication failed:", response);
              this.cleanup();
            }
          }
          // Other responses (auth/me, etc.) with different request IDs
          else if (response.i && response.i > 1) {
            console.log(`📬 Response for request ID ${response.i}:`, response);
          }
        }
      } catch (error) {
        console.error("❌ Error parsing array response:", (error as Error).message);
        console.log("Raw message:", rawMessage);
      }
      return;
    }

    // Handle regular JSON messages
    try {
      const message: SocketMessage = JSON.parse(rawMessage);
      console.log("📨 Received JSON message:", JSON.stringify(message, null, 2));
    } catch (error) {
      console.log("📦 Non-JSON message received:", rawMessage);
      // Don't treat non-JSON messages as errors, just log them
    }
  }

  /**
   * Send a message to the WebSocket server
   * @param endpoint - The API endpoint (e.g., 'order/placeorder', 'user/find')
   * @param body - Optional request body (will be JSON stringified if object)
   * @param requestId - Optional request ID (will auto-increment if not provided)
   * @returns The request ID used for this message
   */
  public send(endpoint: string, body?: any, requestId?: number): number {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      console.error("❌ WebSocket is not connected");
      throw new Error("WebSocket is not connected");
    }

    if (!this.isAuthenticated) {
      console.error("❌ WebSocket is not authenticated yet");
      throw new Error("WebSocket is not authenticated yet");
    }

    // Use provided requestId or auto-increment
    const reqId = requestId !== undefined ? requestId : this.requestIdCounter++;

    // Format message according to Tradovate protocol: endpoint\nrequestId\n\nbody
    let message = `${endpoint}\n${reqId}\n\n`;

    if (body !== undefined) {
      // If body is an object, stringify it; otherwise use as-is
      message += typeof body === "object" ? JSON.stringify(body) : body;
    }

    console.log(`📤 Sending message to ${endpoint} (Request ID: ${reqId})`);
    this.ws.send(message);

    return reqId;
  }

  /**
   * Get the current connection status
   */
  public getConnectionStatus(): {
    connected: boolean;
    authenticated: boolean;
    reconnecting: boolean;
    reconnectAttempts: number;
  } {
    const reconnectState = this.reconnectionManager.getState();
    return {
      connected: this.ws?.readyState === WebSocket.OPEN,
      authenticated: this.isAuthenticated,
      reconnecting: reconnectState.isReconnecting,
      reconnectAttempts: reconnectState.reconnectAttempts,
    };
  }

  /**
   * Manually disconnect from the WebSocket (will not auto-reconnect)
   */
  public disconnect(): void {
    console.log("🔌 Manual disconnect requested");
    this.reconnectionManager.disable();
    this.stopHeartbeat();

    if (this.ws) {
      this.ws.close(1000, "Manual disconnect");
    }
  }

  /**
   * Manually reconnect to the WebSocket (useful after calling disconnect)
   */
  public async reconnect(): Promise<void> {
    console.log("🔄 Manual reconnect requested");
    this.reconnectionManager.enable();
    this.reconnectionManager.reset();

    await this.reconnectionManager.attempt();
  }

  private startHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
    }
    console.log("💓 Starting heartbeat timer (2.5s interval) after authentication");
    this.heartbeatTimer = setInterval(() => {
      this.sendHeartbeat();
    }, this.heartbeatInterval);

    // Start the timeout checker
    this.startHeartbeatTimeoutChecker();
  }

  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
      console.log("💓 Heartbeat timer stopped");
    }

    // Stop the timeout checker
    this.stopHeartbeatTimeoutChecker();
  }

  private startHeartbeatTimeoutChecker(): void {
    if (this.heartbeatTimeoutTimer) {
      clearInterval(this.heartbeatTimeoutTimer);
    }

    // Check every 5 seconds if we've heard from the server recently
    this.heartbeatTimeoutTimer = setInterval(() => {
      this.checkServerTimeout();
    }, 5000);
  }

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

  private checkServerTimeout(): void {
    const timeSinceLastMessage = Date.now() - this.lastServerMessageTime;

    if (timeSinceLastMessage > this.serverTimeoutMs) {
      console.error(
        `⚠️ No server response for ${timeSinceLastMessage}ms (threshold: ${this.serverTimeoutMs}ms)`
      );
      console.error("💀 Connection appears dead, forcing reconnection...");

      // Force close the connection to trigger reconnection logic
      // Use 1000 (normal closure) or 4000-4999 (custom application codes)
      if (this.ws) {
        this.ws.close(4000, "Server timeout - no heartbeat response");
      }
    }
  }

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

  private sendHeartbeat(): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send("[]");
      this.heartbeatsSent++;
      console.log(`💓 Empty frame heartbeat #${this.heartbeatsSent} sent: []`);
    }
  }

  /**
   * Reset connection-related state (called when connection is lost)
   */
  private resetConnectionState(): void {
    this.isAuthenticated = false;
    this.authenticationSent = false;
    this.stopHeartbeat();
    this.lastServerMessageTime = Date.now(); // Reset to avoid false timeout on reconnect
  }

  /**
   * Handle reconnection - called by ReconnectionManager
   */
  private async handleReconnect(): Promise<void> {
    // Ensure old connection is cleaned up
    if (this.ws) {
      this.ws.removeAllListeners();
      // Only close if the connection is fully established (OPEN state)
      // Don't call close() on CONNECTING, CLOSING, or CLOSED states
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.close(1000, "Reconnecting");
      }
      this.ws = null;
    }

    // Get fresh access token if needed
    await this.tokenManager.getAccessToken();

    // Attempt to reconnect
    await this.connectWebSocket();
  }

  private cleanup(): void {
    // Disable reconnection before cleanup
    this.reconnectionManager.disable();
    this.reconnectionManager.cleanup();
    this.stopHeartbeat();
    this.tokenManager.cleanup();

    if (this.ws) {
      this.ws.close();
    }

    console.log("🧹 Cleanup completed");
    process.exit(0);
  }
}

// Handle unhandled rejections and errors
process.on("unhandledRejection", (reason, promise) => {
  console.error("⚠️ Unhandled Rejection:", reason);
  // Don't exit, let reconnection logic handle it
});

process.on("uncaughtException", (error) => {
  console.error("⚠️ Uncaught Exception:", error.message);
  // Don't exit, let reconnection logic handle it
});

// Handle process termination
process.on("SIGINT", () => {
  console.log("\n🛑 Received SIGINT, shutting down gracefully...");
  process.exit(0);
});

process.on("SIGTERM", () => {
  console.log("\n🛑 Received SIGTERM, shutting down gracefully...");
  process.exit(0);
});

// Start the application
const client = new TradovateWebSocketClient();
client.start().catch((error) => {
  console.error("💥 Fatal error:", error.message);
  process.exit(1);
});
```