> This page is for NT Prop.

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

# Charting Best Practices

This guide describes the recommended end-to-end process for building charts on top of the market data WebSocket API. It reflects the approach teams use in production for large symbol universes (hundreds to thousands of contracts), and it builds on the chart request and response shapes in the

[Market Data Request Reference](/prop/overview/core-concepts/web-sockets/market-data/market-data-request-reference)

.

## At a Glance

* A reliable charting pipeline has four stages: shard your connections, order the incoming data, calculate and warehouse candles, then overlay the live edge.
* Shard a large symbol universe across multiple WebSocket connections so one disconnect takes down only a slice of your charts.
* Each symbol is its own `md/getChart` subscription. Route incoming data by the subscription ID from the acknowledgment, not the request ID.
* Buffer and sort ticks before aggregating them. Plotting as-received is the most common source of chart errors.
* Warehouse calculated candles and serve history from storage, so you do not open an exchange subscription per viewer.

## Stage 1: Shard Your Connections

A single WebSocket connection can hold subscriptions for many symbols, but putting a large symbol universe on one socket creates a single point of failure and a processing bottleneck: every symbol's messages serialize through one socket, and one disconnect takes every chart down at once.

Sharding distributes your symbol universe across multiple connections instead. It limits the blast radius of any single disconnect, keeps message processing parallel, and helps you avoid bursting past

[rate limits](/prop/overview/core-concepts/rate-limits)

when you subscribe to many symbols at startup. (This is distinct from the `shardingExpression` sharding on the

[user sync socket](/prop/overview/core-concepts/web-sockets/user-syncrequest#sharding)

, which routes user and account events rather than distributing market data symbols.)

Recommended strategies:

* **Fixed-size buckets:** assign symbols round-robin so each connection carries a similar count (for example, 50 to 100 symbols per socket).
* **Group by product family:** keep related contracts together (all ES contract months on one shard, all NQ on another). This makes operational debugging clearer.
* **Load-weighted sharding:** split by expected message volume rather than symbol count, since actively traded contracts generate far more ticks than thin ones.
* **Keep a static symbol-to-socket map** so reconnect logic knows exactly which subset to re-subscribe, without re-deriving the scheme.

### Routing Data to the Right Symbol

Each symbol subscription is its own message. There is no request body that lists multiple symbols, so subscribing to two symbols on one connection means sending two separate `md/getChart` messages, each with a unique request ID. Every WebSocket request follows the format (endpoint, request ID, an optional query, and the body, each on its own line):

```
endpoint
requestId

body
```

For example, subscribing to ESU6 as request 101 and NQU6 as request 102:

```
md/getChart
101

{"symbol":"ESU6","chartDescription":{"underlyingType":"MinuteBar","elementSize":15,"elementSizeUnit":"UnderlyingUnits","withHistogram":false},"timeRange":{"asMuchAsElements":100}}

md/getChart
102

{"symbol":"NQU6","chartDescription":{"underlyingType":"MinuteBar","elementSize":15,"elementSizeUnit":"UnderlyingUnits","withHistogram":false},"timeRange":{"asMuchAsElements":100}}
```

Each `md/getChart` acknowledgment echoes your request ID in the `i` field and returns the subscription ID in both `d.historicalId` and `d.realtimeId`:

```json
{"s":200,"i":101,"d":{"mode":"Delayed","historicalId":12432,"realtimeId":12432}}
{"s":200,"i":102,"d":{"mode":"Delayed","historicalId":12434,"realtimeId":12434}}
```

`historicalId` and `realtimeId` carry the same value, so read the subscription ID off the acknowledgment. Build a lookup table from the acknowledgments:

| Request ID (`i`) | Symbol | Subscription ID |
| ---------------- | ------ | --------------- |
| 101              | ESU6   | 12432           |
| 102              | NQU6   | 12434           |

From that point on, every chart data packet arrives with no request ID, tagged only with the subscription ID inside each chart entry:

```json
{"e":"chart","d":{"charts":[{"id":12432,"s":"db","td":20260818,"bars":[]}]}}
{"e":"chart","d":{"charts":[{"id":12434,"s":"","td":20260818,"bars":[]}]}}
```

Match the `id` field against the lookup table to know which symbol each packet belongs to: here `12432` routes to ESU6 and `12434` to NQU6. Keep both mappings: request ID to symbol (to match acknowledgments) and subscription ID to symbol (to match every message afterward, and to call `md/cancelChart` later). Two other fields are worth tracking per chart entry:

* **`s`**: a status tag on the batch. `db` marks bars replayed from historical storage, an empty string marks live bars, and `s` marks the in-progress bar at the moment history ends.
* **`eoh`**: sent as its own chart entry (for example, `{"id":12432,"eoh":true}`) once the historical replay for that subscription ID finishes. This is your per-symbol signal to switch that symbol from backfilling history to overlaying live updates.

## Stage 2: Order the Incoming Data

Before you calculate any candle, choose the right data granularity and order what arrives correctly.

**Choose raw ticks only if you need custom bars.** If standard bar types such as `MinuteBar` or `DailyBar` meet your needs, request them directly from `md/getChart`. This is simpler and avoids the ordering work below. Use raw `Tick` data only when you need full control over bar construction, for bar types or intervals that `md/getChart` does not build natively.

**Buffer and sort before aggregating.** Ticks can arrive out of order: multiple packets per message, overlap at the historical-to-real-time boundary, and normal network batching all contribute. Building candles from an unsorted stream produces gaps and false price spikes. Buffer incoming ticks, sort by timestamp with the tick ID as a tiebreaker, and only then aggregate into candles. Raw ticks also arrive as deltas from a per-packet base price and timestamp rather than absolute values. For how to reconstruct them, see

[Tick Charts](/prop/overview/core-concepts/web-sockets/market-data/tick-charts)

.

## Stage 3: Calculate and Warehouse Candles

Rather than re-requesting historical data every time a user opens a chart, calculate candles once and persist them to a warehouse that serves all users.

* Aggregate sorted ticks, or accept pre-built bars from `md/getChart`, into OHLCV candles at the interval you need.
* Write each closed candle to a database, keyed by symbol and interval.
* Serve historical chart requests from the warehouse rather than from a live exchange subscription. This scales to many concurrent viewers without opening a new subscription per user.

### Choosing a Time Range

The `timeRange` field controls which slice of history a `md/getChart` request loads. The strategies are mutually exclusive, so pick one:

| Goal                                        | `timeRange` to use                                     |
| ------------------------------------------- | ------------------------------------------------------ |
| Load the last N bars, then stream live      | `{ "asMuchAsElements": N }`                            |
| Load a fixed window                         | `{ "closestTimestamp": "…", "asFarAsTimestamp": "…" }` |
| Load from a point in time, then stream live | `{ "closestTimestamp": "…" }`                          |
| Paginate back through history               | Loop `closestTimestamp` backward (see below)           |

For example, to load the 100 most recent bars and then stream live:

```json
{ "asMuchAsElements": 100 }
```

To load a fixed window, give both ends (`closestTimestamp` is the most recent end of the range, `asFarAsTimestamp` is how far back to go):

```json
{
  "closestTimestamp": "2025-03-20T16:00:00.000Z",
  "asFarAsTimestamp": "2025-01-01T00:00:00.000Z"
}
```

A chart's controls map onto these fields: increasing `asMuchAsElements` loads more bars and decreasing it loads fewer (the zoom control), changing it cancels the old subscription and re-requests, the Size input sets `elementSize`, and the Type dropdown sets `underlyingType`.

```js
function requestChart(sym) {
  var type = el('chart-type').value;           // MinuteBar / DailyBar / Tick
  var size = parseInt(el('chart-size').value) || 5;    // elementSize
  var bars = parseInt(el('chart-bars').value) || 100;  // asMuchAsElements

  ws.send('md/getChart\n' + (wsMsgId++) + '\n\n' + JSON.stringify({
    symbol: sym,
    chartDescription: {
      underlyingType: type,
      elementSize: size,
      elementSizeUnit: 'UnderlyingUnits',
      withHistogram: false
    },
    timeRange: { asMuchAsElements: bars }
  }));
}
```

### Backfilling Long History Windows

The data-size limit per request is flexible rather than a fixed hard cap, and it can vary from day to day, but `MinuteBar` and `Tick` requests tend to return at most about two months of data per request. To backfill further:

1. When the `eoh` entry arrives for that subscription, find the oldest bar you have gathered.
2. Issue a new request using that bar's timestamp as `closestTimestamp`, keeping your original target date as `asFarAsTimestamp`.
3. Repeat until the oldest bar received reaches your target start date.

For more on this pattern, see [How to Handle Data Size Limits](https://github.com/tradovate/example-api-faq/blob/main/docs/HowToHandleDataSizeLimits.md).

## Stage 4: Overlay Real-Time Data

With history warehoused, the display layer combines two sources:

* **Closed historical candles**, read directly from the warehouse for fast chart loads.
* **The current, still-forming candle**, updated live from the real-time stream until it closes. When it closes, write it to the warehouse and begin a new live candle.

This gives users fast-loading historical charts with a live-updating right edge, without keeping an exchange subscription open per viewer for historical data.

## Best Practices Checklist

| Area           | Recommendation                                                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Connections    | Use multiple WebSocket connections for market data. Do not put an entire large symbol universe on a single socket.             |
| Sharding       | Group symbols by product family or load, and keep a static symbol-to-socket map for clean reconnects.                          |
| Subscriptions  | Send one `md/getChart` request per symbol with a unique request ID. Track request ID to symbol and subscription ID to symbol.  |
| Bar source     | Prefer native `MinuteBar` or `DailyBar` from `md/getChart`. Use raw `Tick` data only when custom bar types are required.       |
| Tick decoding  | Reconstruct price and time from packet-relative deltas using the base price, base time, and tick size.                         |
| Ordering       | Always buffer and sort ticks by timestamp and ID before aggregating. Never plot as-received.                                   |
| Storage        | Persist calculated candles to a warehouse, and serve historical chart data from storage rather than a live request per viewer. |
| Live rendering | Read closed candles from the warehouse, and overlay only the in-progress candle from the live stream.                          |
| Backfill       | Paginate long history requests using `closestTimestamp` walked backward against a fixed `asFarAsTimestamp` target.             |
| Resilience     | Handle disconnects per shard. A drop on one socket should not require re-subscribing symbols on unaffected sockets.            |

The core idea across all four stages is the same: do not make the exchange subscription do double duty as your storage and distribution layer. Shard connections so any single socket stays resilient, calculate candles once from correctly ordered data, warehouse the result, and overlay only the live edge on top.