Stream NinjaTrader 8 Market Data with Python Using WebSockets
Connect once over WebSocket, stream live quotes and P&L from NinjaTrader 8, and execute API calls, all from a single Python script.
CrossTrade's REST API is built around request-response: you ask for your positions, you get your positions. That works fine when you need a snapshot, but it falls apart when you need a continuous stream of market data or account P&L updating every second. Polling a REST endpoint 60 times a minute is wasteful, slow, and eats through your rate limit budget in a hurry.
The WebSocket API solves this. You open a single persistent connection over secure wss://, and from that point forward, data flows in both directions without the overhead of establishing a new HTTP connection for every interaction. Subscribe to instruments, enable P&L streaming, fire RPC commands, and receive responses — all multiplexed over one socket.
This post walks through a complete working Python client that subscribes to live market data, streams account P&L, and polls positions on a timer. By the end, you'll have a script you can run against your own CrossTrade account and a clear understanding of how the WebSocket protocol works under the hood.
What You Need
You need a CrossTrade account with an active subscription, the XT add-on (v1.12.0+) running on your NinjaTrader 8 instance, and a secret API key from your dashboard. On the Python side, you'll need websockets installed (pip install websockets). That's it — no other dependencies.
The WebSocket endpoint is wss://app.crosstrade.io/ws/stream. Authentication uses the same Bearer token as the REST API, passed as a header during the WebSocket handshake. If you've used the REST API before, you already have everything you need.
How the Protocol Works
Every message you send to the server is a JSON object with an action field. There are four actions available: rpc for calling any API function, subscribe and unsubscribe for market data, and streamPnl for toggling account P&L updates.
The server pushes three types of messages back to you:
- Market data arrives as
{"type": "marketData", "quotes": [...]}with bid/ask/last/volume for each subscribed instrument. - P&L updates come as
{"type": "pnlUpdate", "accounts": [...]}with realized and unrealized P&L per active account. - RPC responses arrive with the
idyou sent (or one the server generated), containing the result in adatafield. This means you can multiplex dozens of different API calls over the same connection and match responses back to requests using theidyou assigned.
Only one WebSocket connection is allowed per account at a time. If you open a second connection, the first one gets closed immediately. The server enforces this, so your client should be built to handle unexpected disconnections gracefully.
The Full Python Client
Here's a working script that demonstrates all three capabilities at once: subscribing to market data, enabling P&L streaming, and running periodic RPC calls on a background timer. You can copy this, drop in your API token, and run it.
import asyncio
import websockets
import json
from datetime import datetime
# CONFIGURATION
API_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
URI = "wss://app.crosstrade.io/ws/stream"
async def poll_positions(websocket):
"""Every 10 seconds, request ListPositions for Sim101."""
count = 0
while True:
await asyncio.sleep(10)
msg = {
"action": "rpc",
"id": f"poll-positions-{count}",
"api": "ListPositions",
"args": {"account": "Sim101"}
}
await websocket.send(json.dumps(msg))
print(datetime.now(), "📤 Sent ListPositions request", msg)
async def myapp():
headers = {
"Authorization": f"Bearer {API_TOKEN}"
}
print(f"Connecting to {URI}...")
try:
async with websockets.connect(URI, extra_headers=headers) as websocket:
print("✅ Connected!")
# 1. Subscribe to Market Data
await websocket.send(json.dumps({
"action": "subscribe",
"instruments": ["ES 06-26", "MNQ 06-26"]
}))
print("📤 Sent Market Sub")
# 2. Enable PnL Streaming
await websocket.send(json.dumps({
"action": "streamPnl",
"enabled": True
}))
print("📤 Sent PnL Enable")
# 3. Kick off the position poller as a background task
poller = asyncio.create_task(poll_positions(websocket))
# 4. Listen loop
try:
while True:
response = await websocket.recv()
data = json.loads(response)
msg_type = data.get('type')
if 'status' in data:
print(datetime.now(), f"ⓘ Status: {data}")
elif msg_type == 'marketData':
summary = [f"{q['instrument']}: {q['bid']} x {q['ask']}" for q in data.get('quotes', [])]
print(datetime.now(), f"📈 Market Data: {summary}")
elif msg_type == 'pnlUpdate':
accounts = data.get('accounts', [])
print(datetime.now(), f"💰 PnL Update: {len(accounts)} active account(s)")
# RPC responses — keyed by id
elif 'id' in data and 'data' in data:
rpc_id = data['id']
rpc_data = data['data']
if 'poll-positions' in rpc_id:
positions = rpc_data.get('positions', [])
print(datetime.now(), f"📋 Positions ({len(positions)}): {json.dumps(positions, indent=2)}")
else:
print(datetime.now(), f"🔁 RPC [{rpc_id}]: {json.dumps(rpc_data, indent=2)}")
else:
print(datetime.now(), f"❓ Unknown: {data}")
finally:
poller.cancel()
except websockets.exceptions.ConnectionClosed as e:
print(f"❌ Connection Closed: {e.code} - {e.reason}")
except Exception as e:
print(f"❌ Error: {e}")
if __name__ == "__main__":
asyncio.run(myapp())Breaking It Down
The script does three things simultaneously over a single WebSocket connection. Let's walk through each one.
Subscribing to market data happens immediately after the connection is established. The subscribe action takes an array of NinjaTrader instrument names, and the server begins pushing quote updates for those instruments at roughly 1-second intervals. Quotes come directly from your own NT8 data feed. If your add-on is receiving data for ES 06-26, you'll see it on the socket. You can subscribe to additional instruments at any time by sending another subscribe message; instruments accumulate across calls. To stop receiving quotes for a specific instrument, send an unsubscribe with the same instrument name.
P&L streaming is toggled with the streamPnl action. Once enabled, the server pushes account-level snapshots approximately every second, including realized P&L, unrealized P&L, and other account metrics for each active account. This is the same data that powers the P&L display in the CrossTrade dashboard. Set enabled to false to turn it off.
RPC calls are where the WebSocket really shines compared to REST. The rpc action accepts any API function available through the REST API — PlaceOrder, ListAccounts, GetPosition, CancelOrders, ClosePosition, MarketInfo, and everything else documented in the API reference. You send a JSON message with the function name and arguments, and the response arrives asynchronously on the same socket. The id field is your correlation key: include one in your request, and it comes back on the response so you can match them up. If you don't include an id, the server generates one for you.
In this example, the poll_positions coroutine fires a ListPositions call every 10 seconds as a background task. It uses incrementing IDs prefixed with poll-positions- so the listen loop can identify these responses and handle them differently from other RPC traffic. This pattern scales well — you could have multiple background pollers, each with their own ID prefix, all sharing the same connection.
Message Routing in the Listen Loop
The listen loop is where all incoming messages converge, and you need a routing strategy to tell them apart. The script uses a simple priority chain:
Messages with a status field are acknowledgments from subscription or streaming commands — the server confirming that your subscribe or streamPnl action was received. Messages with type: "marketData" contain quote arrays. Messages with type: "pnlUpdate" contain account snapshots. Everything else with both an id and data field is an RPC response, and the id tells you which request it belongs to.
This is a deliberately simple router. In a production system, you'd likely use a dictionary of pending RPC callbacks keyed by id, resolving futures or triggering handlers as responses arrive. But for a demo, string matching on the id prefix works fine and keeps the code readable.
Rate Limits
The WebSocket shares the same rate-limit budget as the REST API: 180 RPC requests per minute with a burst of 20. Only rpc messages count against this budget. Streaming data (market quotes, P&L updates) is free — it doesn't consume tokens. Subscription management actions like subscribe, unsubscribe, and streamPnl have their own separate limit of roughly 20 per minute.
If you exceed the RPC rate limit, the server returns an error on that specific message rather than killing the connection. But if you hit the limit repeatedly (10+ consecutive violations), the server will close your connection as a safety measure. The position poller in the example fires once every 10 seconds, which is well within budget.
For the full breakdown on how the token bucket algorithm works and how it recovers, see the Rate Limiting documentation.
Reconnection
Your connection can close for several reasons: server restart, add-on disconnect, session takeover from another connection, or rate-limit abuse. The example script doesn't include reconnection logic to keep things simple, but in production you'll want exponential backoff — wait 1 second after the first disconnect, 2 after the second, 4 after the third, capping at around 30 seconds. Reset the counter after a connection stays open for more than a minute.
After reconnecting, you need to resubscribe to instruments and re-enable P&L streaming. The server doesn't remember your previous session's subscriptions.
What You Can Build With This
The WebSocket API is the right foundation for anything that needs real-time data or sends frequent commands. Custom dashboards that show live quotes and P&L outside of NinjaTrader. Algo engines that receive market data, compute signals, and place orders all from a single Python process. Risk management bots that monitor P&L and flatten positions when drawdown thresholds are hit. Multi-account orchestration scripts that poll positions across accounts and rebalance.
The full list of RPC functions available over the WebSocket is the same as the REST API. Everything documented under Accounts, Positions, Orders, Strategies, Executions, Market, and Quotes works through rpc messages.
Try It Yourself
Drop your API token into the script, make sure your NT8 instance is running with the XT add-on connected, and run it. You should see subscription confirmations, followed by a stream of market data and P&L updates, with position snapshots appearing every 10 seconds. Swap out the instrument names and account for your own, and you've got a live data feed in under 100 lines of Python.
The complete WebSocket documentation is at docs.crosstrade.io/api/websocket-api. If you run into issues or want to share what you're building, join us on Discord.
New to CrossTrade? Start your free 7-day trial.
