> 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.

# Stage 5: Market Data

Market data access is essential for trading applications. This stage validates your integration's
ability to establish WebSocket connections, subscribe to real-time market data, and handle market
data streams effectively.

## Overview

The market data stage ensures your application can:

* Establish secure WebSocket connections for market data
* Subscribe to and receive real-time quotes, DOM, and chart data
* Handle market data streams with proper error handling
* Manage subscriptions and unsubscribe properly
* Process tick chart data correctly

## Required Tests

### 1. WebSocket Connection and Authorization

**Purpose:** Establish a secure WebSocket connection and authenticate for market data access.

**Test Steps:**

1. Open WebSocket connection to market data endpoint
2. Send authorization message with access token
3. Verify successful authentication response
4. Test connection stability and reconnection handling
5. Verify proper error handling for invalid tokens

**Example Implementation:**

```javascript
// WebSocket connection setup
const ws = new WebSocket("wss://md.tradovateapi.com/v1/websocket");
//wss://md-api.staging.ninjatrader.dev/v1/websocket for staging

ws.onopen = function () {
  // Send authorization message
  const authMessage = `authorize\n1\n\n${accessToken}`;
  ws.send(JSON.stringify(authMessage));
};

ws.onmessage = function (event) {
  const message = JSON.parse(event.data);

  if (message.i === 1 && message.s === 200) {
    console.log("Successfully authorized for market data");
    // Proceed with market data subscriptions
  } else if (message.d.errorText) {
    console.error("Authorization failed:", message.d);
  }
};
```

**Validation Criteria:**

* ✅ WebSocket connection establishes successfully
* ✅ Authorization message is accepted
* ✅ Connection remains stable during testing
* ✅ Invalid tokens are rejected appropriately
* ✅ Connection can be re-established after disconnection

### 2. Quote Subscription and Data Reception

**Endpoint:** `md/subscribeQuote`

**Purpose:** Subscribe to real-time quote data for specified contracts.

**Test Steps:**

1. Subscribe to quote data for a valid contract symbol
2. Verify subscription confirmation response
3. Receive and validate quote data messages
4. Test subscription with contract ID instead of symbol
5. Unsubscribe and verify data stops flowing

**Example Implementation:**

```javascript
// Subscribe to quotes
function subscribeToQuotes(symbol) {
  const subscribeMessage = {
    "md/subscribeQuote": {
      symbol: symbol,
    },
  };
  ws.send(JSON.stringify(subscribeMessage));
}

// Handle incoming quote data
ws.onmessage = function (event) {
  const message = JSON.parse(event.data);

  if (message.e === "md" && message.d.quotes) {
    message.d.quotes.forEach((quote) => {
      console.log("Quote received:", {
        contractId: quote.contractId,
        timestamp: quote.timestamp,
        bid: quote.entries.Bid,
        offer: quote.entries.Offer,
        trade: quote.entries.Trade,
      });
    });
  }
};

// Unsubscribe from quotes
function unsubscribeFromQuotes(symbol) {
  const unsubscribeMessage = {
    "md/unsubscribeQuote": {
      symbol: symbol,
    },
  };
  ws.send(JSON.stringify(unsubscribeMessage));
}
```

**Expected Quote Data:**

```json
{
  "e": "md",
  "d": {
    "quotes": [
      {
        "timestamp": "2024-01-01T12:00:00.000Z",
        "contractId": 123456,
        "entries": {
          "Bid": { "price": 18405.123, "size": 7.123 },
          "Offer": { "price": 18410.012, "size": 12.35 },
          "Trade": { "price": 18405.023, "size": 2.1 },
          "TotalTradeVolume": { "size": 4118.123 },
          "OpenInterest": { "size": 40702.024 },
          "HighPrice": { "price": 18520.125 },
          "LowPrice": { "price": 18355.23 },
          "OpeningPrice": { "price": 18515.123 },
          "SettlementPrice": { "price": 18520.257 }
        }
      }
    ]
  }
}
```

**Validation Criteria:**

* ✅ Quote subscription succeeds with valid symbol/contract ID
* ✅ Real-time quote data is received continuously
* ✅ Quote data contains all expected fields
* ✅ Data timestamps are accurate and recent
* ✅ Unsubscription stops data flow immediately
* ✅ Invalid symbols are rejected appropriately

### 3. DOM (Depth of Market) Subscription

**Endpoint:** `md/subscribeDOM`

**Purpose:** Subscribe to real-time depth of market data showing bid/offer levels.

**Test Steps:**

1. Subscribe to DOM data for a liquid contract
2. Verify DOM data structure and content
3. Test multiple price levels in bids and offers
4. Verify real-time updates to DOM data
5. Unsubscribe and verify data stops

**Example Implementation:**

```javascript
// Subscribe to DOM
function subscribeToDOM(symbol) {
  const subscribeMessage = {
    "md/subscribeDOM": {
      symbol: symbol,
    },
  };
  ws.send(JSON.stringify(subscribeMessage));
}

// Handle DOM data
ws.onmessage = function (event) {
  const message = JSON.parse(event.data);

  if (message.e === "md" && message.d.doms) {
    message.d.doms.forEach((dom) => {
      console.log("DOM received:", {
        contractId: dom.contractId,
        timestamp: dom.timestamp,
        bidLevels: dom.bids.length,
        offerLevels: dom.offers.length,
        bestBid: dom.bids[0],
        bestOffer: dom.offers[0],
      });
    });
  }
};
```

**Expected DOM Data:**

```json
{
  "e": "md",
  "d": {
    "doms": [
      {
        "contractId": 123456,
        "timestamp": "2024-01-01T12:00:00.000Z",
        "bids": [
          { "price": 2335.25, "size": 33.54 },
          { "price": 2335.0, "size": 45.12 },
          { "price": 2334.75, "size": 22.88 }
        ],
        "offers": [
          { "price": 2335.5, "size": 255.12 },
          { "price": 2335.75, "size": 180.45 },
          { "price": 2336.0, "size": 95.67 }
        ]
      }
    ]
  }
}
```

**Validation Criteria:**

* ✅ DOM subscription succeeds with valid contract
* ✅ DOM data contains multiple bid/offer levels
* ✅ Price levels are properly ordered (bids descending, offers ascending)
* ✅ Real-time updates reflect market changes
* ✅ DOM data structure is consistent
* ✅ Unsubscription works correctly

### 4. Chart Data Subscription

**Endpoint:** `md/getChart`

**Purpose:** Request historical and real-time chart data for analysis.

**Test Steps:**

1. Request chart data with specific parameters
2. Verify historical data is returned
3. Verify real-time chart updates are received
4. Test different chart types (MinuteBar, DailyBar, Tick)
5. Properly cancel chart subscription using returned ID

**Example Implementation:**

```javascript
// Request chart data
function requestChart(symbol, chartType = "MinuteBar", elementSize = 15) {
  const chartRequest = {
    "md/getChart": {
      symbol: symbol,
      chartDescription: {
        underlyingType: chartType,
        elementSize: elementSize,
        elementSizeUnit: "UnderlyingUnits",
        withHistogram: false,
      },
      timeRange: {
        asMuchAsElements: 100,
      },
    },
  };
  ws.send(JSON.stringify(chartRequest));
}

// Handle chart response and data
ws.onmessage = function (event) {
  const message = JSON.parse(event.data);

  if (message.s === 200 && message.d) {
    // Store subscription IDs for later cancellation
    const historicalId = message.d.historicalId;
    const realtimeId = message.d.realtimeId;
    console.log("Chart subscription IDs:", { historicalId, realtimeId });
  }

  if (message.e === "chart" && message.d.charts) {
    message.d.charts.forEach((chart) => {
      console.log("Chart data received:", {
        subscriptionId: chart.id,
        tradeDate: chart.td,
        barCount: chart.bars ? chart.bars.length : 0,
        endOfHistory: chart.eoh,
      });
    });
  }
};

// Cancel chart subscription
function cancelChart(subscriptionId) {
  const cancelRequest = {
    "md/cancelChart": {
      subscriptionId: subscriptionId,
    },
  };
  ws.send(JSON.stringify(cancelRequest));
}
```

**Expected Chart Response:**

```json
{
  "s": 200,
  "i": 13,
  "d": {
    "historicalId": 32,
    "realtimeId": 31
  }
}
```

**Expected Chart Data:**

```json
{
  "e": "chart",
  "d": {
    "charts": [
      {
        "id": 31,
        "td": 20240101,
        "bars": [
          {
            "timestamp": "2024-01-01T11:00:00.000Z",
            "open": 2334.25,
            "high": 2334.5,
            "low": 2333.0,
            "close": 2333.75,
            "upVolume": 4712.234,
            "downVolume": 201.124,
            "upTicks": 1333.567,
            "downTicks": 82.89,
            "bidVolume": 2857.123,
            "offerVolume": 2056.224
          }
        ]
      }
    ]
  }
}
```

**Validation Criteria:**

* ✅ Chart request succeeds with valid parameters
* ✅ Historical data is returned with proper structure
* ✅ Real-time chart updates are received
* ✅ Subscription IDs are properly returned
* ✅ Chart cancellation works correctly
* ✅ Different chart types work as expected

### 5. Tick Chart Data Processing

**Purpose:** Request and process tick-level chart data for high-frequency analysis.

**Test Steps:**

1. Request tick chart data with elementSize: 1
2. Receive and process tick data packets
3. Calculate actual tick prices and timestamps from relative values
4. Handle multiple packets and end-of-history flag
5. Verify tick data accuracy and completeness

**Example Implementation:**

```javascript
// Request tick chart
function requestTickChart(symbol) {
  const tickRequest = {
    "md/getChart": {
      symbol: symbol,
      chartDescription: {
        underlyingType: "Tick",
        elementSize: 1,
        elementSizeUnit: "UnderlyingUnits",
      },
      timeRange: {
        asMuchAsElements: 1000,
      },
    },
  };
  ws.send(JSON.stringify(tickRequest));
}

// Process tick chart data
function processTickChartMessage(msg) {
  const result = [];
  if (msg.charts && msg.charts.length) {
    for (let i = 0; i < msg.charts.length; ++i) {
      const packet = msg.charts[i];
      if (packet.eoh) {
        console.log("End of historical ticks reached");
      } else if (packet.tks && packet.tks.length) {
        for (let j = 0; j < packet.tks.length; ++j) {
          const tick = packet.tks[j];

          const timestamp = packet.bt + tick.t; // Actual tick timestamp
          const price = packet.bp + tick.p; // Actual tick price

          const bid = tick.bs && packet.bp + tick.b; // Actual bid price
          const ask = tick.as && packet.bp + tick.a; // Actual ask price

          result.push({
            id: tick.id,
            timestamp: new Date(timestamp),
            price: price * packet.ts, // Tick price as contract price
            size: tick.s, // Tick size (volume)
            bidPrice: bid && bid * packet.ts,
            bidSize: tick.bs,
            askPrice: ask && ask * packet.ts,
            askSize: tick.as,
          });
        }
      }
    }
  }
  return result;
}

// Handle tick chart data
ws.onmessage = function (event) {
  const message = JSON.parse(event.data);

  if (message.e === "chart" && message.d.charts) {
    const processedTicks = processTickChartMessage(message.d);
    console.log("Processed ticks:", processedTicks.length);
  }
};
```

**Expected Tick Data:**

```json
{
  "charts": [
    {
      "id": 16335,
      "s": "db",
      "td": 20240101,
      "bp": 11917,
      "bt": 1704111179735,
      "ts": 0.25,
      "tks": [
        {
          "t": 0,
          "p": 0,
          "s": 3,
          "b": -1,
          "a": 0,
          "bs": 122.21,
          "as": 28.35,
          "id": 11768401
        }
      ]
    }
  ]
}
```

**Validation Criteria:**

* ✅ Tick chart request succeeds
* ✅ Tick data packets are received and processed
* ✅ Relative prices and timestamps are calculated correctly
* ✅ End-of-history flag is properly handled
* ✅ Tick data accuracy is maintained
* ✅ Multiple packets are processed correctly

### 6. Histogram Subscription

**Endpoint:** `md/subscribeHistogram`

**Purpose:** Subscribe to real-time histogram data showing price distribution.

**Test Steps:**

1. Subscribe to histogram data for a contract
2. Verify histogram data structure
3. Test histogram updates and refresh flags
4. Verify price distribution data accuracy
5. Unsubscribe and verify data stops

**Example Implementation:**

```javascript
// Subscribe to histogram
function subscribeToHistogram(symbol) {
  const subscribeMessage = {
    "md/subscribeHistogram": {
      symbol: symbol,
    },
  };
  ws.send(JSON.stringify(subscribeMessage));
}

// Handle histogram data
ws.onmessage = function (event) {
  const message = JSON.parse(event.data);

  if (message.e === "md" && message.d.histograms) {
    message.d.histograms.forEach((histogram) => {
      console.log("Histogram received:", {
        contractId: histogram.contractId,
        timestamp: histogram.timestamp,
        base: histogram.base,
        refresh: histogram.refresh,
        itemCount: Object.keys(histogram.items).length,
      });
    });
  }
};
```

**Expected Histogram Data:**

```json
{
  "e": "md",
  "d": {
    "histograms": [
      {
        "contractId": 123456,
        "timestamp": "2024-01-01T12:00:00.000Z",
        "tradeDate": {
          "year": 2024,
          "month": 1,
          "day": 1
        },
        "base": 2338.75,
        "items": {
          "-14": 5906.67,
          "-13": 4521.23,
          "-12": 3890.45,
          "0": 12345.67,
          "1": 2345.89,
          "2": 1234.55
        },
        "refresh": false
      }
    ]
  }
}
```

**Validation Criteria:**

* ✅ Histogram subscription succeeds
* ✅ Histogram data contains price distribution
* ✅ Base price and items are properly structured
* ✅ Refresh flag indicates data updates
* ✅ Unsubscription works correctly

## Best Practices

1. **Connection Management**: Implement robust WebSocket connection handling with automatic
   reconnection
2. **Subscription Tracking**: Keep track of all active subscriptions to properly unsubscribe
3. **Data Processing**: Handle market data efficiently to avoid memory leaks
4. **Error Recovery**: Implement proper error handling and recovery mechanisms
5. **Rate Limiting**: Respect rate limits and implement backoff strategies
6. **Data Validation**: Validate all incoming market data before processing
7. **Resource Cleanup**: Always unsubscribe from data streams when no longer needed

## Testing Checklist

* [ ] WebSocket connection establishes successfully
* [ ] Authorization with access token works correctly
* [ ] Quote subscription and data reception works
* [ ] DOM subscription provides proper depth data
* [ ] Chart data request returns historical and real-time data
* [ ] Tick chart data is processed correctly
* [ ] Histogram subscription provides data
* [ ] All subscription types can be unsubscribed properly
* [ ] Error handling works for invalid requests
* [ ] Connection stability is maintained during testing
* [ ] Resource cleanup is complete