Arrow HFT Datastream API
Real-time market data streaming via WebSocket with binary protocol for high-performance trading applications.
Overview
The Arrow Dataserver API provides institutional-grade real-time market data streaming through persistent WebSocket connections. Built with a binary protocol for maximum efficiency, it supports Last Traded Price Compact (LTPC) and Full market depth across NSE, BSE, and MCX, plus Closing Auction Session (CAS) data for CAS-enabled symbols.
Key Features
- Binary Protocol: Optimized binary frames for minimal latency and bandwidth
- Zstd Compression: Inbound market-data frames are zstd-compressed when
zstd=1is set on the connection URL - Three Data Modes: LTPC (compact), Full (complete market depth), or CAS (closing auction)
- Flexible Symbol Formats: Subscribe using symbol names or numeric IDs
- Cross-Exchange Coverage: Unified data stream across NSE CM, NSE FO, BSE CM, BSE FO, and MCX FO
- Configurable Latency: Adjust LTPC and Full tick intervals down to 50 ms
- Scalable Architecture: Up to 1,024 active symbol subscriptions per trading account across all HFT connections; up to 512 symbols per subscription request
Connection Setup
WebSocket Endpoint
Mandatory from 8 July 2026
Zstd compression becomes mandatory on 8 July 2026. After that date, the HFT feed sends only zstd-compressed inbound frames. Clients that connect without zstd=1 or that do not decompress payloads before parsing will not receive usable market data.
Upgrade to a current pyarrow-client release (which handles decompression automatically) or update any custom WebSocket client before the cutoff.
Connection Parameters
| Parameter | Value | Description |
|---|---|---|
appID |
APP_ID |
Application identifier |
token |
TOKEN |
Authentication token |
zstd |
1 |
Request zstd-compressed inbound frames (required from 8 July 2026) |
Establishing Connection
const wsUrl = 'wss://socket.arrow.trade?appID=APP_ID&token=TOKEN&zstd=1';
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = function(event) {
console.log('Connected to Arrow Dataserver');
};
ws.onmessage = function(event) {
// Decompress with a zstd library such as fzstd before parsing.
const payload = fzstd.decompress(new Uint8Array(event.data));
const marketData = parseMarketData(payload);
console.log(marketData);
};
import websocket
import struct
import zstandard as zstd
ws_url = "wss://socket.arrow.trade?appID=APP_ID&token=TOKEN&zstd=1"
zdec = zstd.ZstdDecompressor()
def on_message(ws, message):
payload = zdec.decompress(bytes(message))
market_data = parse_market_data(payload)
print(market_data)
def on_open(ws):
print("Connected to Arrow Dataserver")
ws = websocket.WebSocketApp(
ws_url,
on_message=on_message,
on_open=on_open
)
ws.run_forever()
# Test connection using websocat
websocat "wss://socket.arrow.trade?appID=<APP_ID>&token=<TOKEN>&zstd=1"
Subscription Management
Request Format
All subscription requests are sent as JSON strings over the WebSocket connection.
Subscription limits
A trading account can have at most 1,024 active symbol subscriptions in total across all HFT WebSocket connections. This allowance is shared by all connections for the account; opening another connection does not increase it.
A single subscription request can include at most 512 symbols (via symbols or symIds). Reaching the 1,024-symbol account allowance therefore requires at least two requests.
Request Fields
| Field | Required | Type | Description |
|---|---|---|---|
code |
Yes | string | Action: sub/s (subscribe) or unsub/u (unsubscribe) |
mode |
Yes | string | Data mode: ltpc/l (compact), full/f (complete), or cas (closing auction) |
symbols |
Conditional | array | Symbol names (mutually exclusive with symIds) |
symIds |
Conditional | array | Symbol ID objects containing daily-file ExchangeID values (mutually exclusive with symbols) |
latency |
No | integer | LTPC and Full tick interval from 50 to 60,000 ms (default: 1,000 ms); omit for CAS |
Use ExchangeID with symIds
When subscribing with symIds, place the ExchangeID values from the current daily symbol file in each ids array. Do not use Token: Token and ExchangeID are different fields and may contain different values.
Data Modes
| Mode | Shorthand | Packet Size | Description | Best For |
|---|---|---|---|---|
ltpc |
l |
40 bytes | Last Traded Price Compact | Basic price monitoring, high-frequency feeds |
full |
f |
196 bytes | Complete market data with depth | Order book analysis, full market view |
cas |
— | 168 bytes | Closing auction data with four price levels | Monitoring CAS order books and imbalance |
CAS symbols only
CAS mode works only for CAS-enabled symbols. Index symbols cannot be subscribed in CAS mode. See Closing Auction Session (CAS) for eligibility and session timing.
Symbol Naming Conventions
Symbol Format Overview
| Segment | Pattern | Example |
|---|---|---|
| NSE Cash Market | NSE.<UNDERLYING>-<SERIES> |
NSE.SBIN-EQ |
| NSE Futures | <UNDERLYING><DD><MON><YY>F |
BANKNIFTY30DEC25F |
| NSE Options | <UNDERLYING><DD><MON><YY><C\|P><STRIKE> |
NYKAA30DEC25C232.5 |
| BSE Cash Market | BSE.<UNDERLYING> |
BSE.SBIN |
| BSE Futures | <ASSET_TYPE><DD><MON><YY>F |
SENSEX01JAN26F |
| BSE Options | <ASSET_TYPE><DD><MON><YY><C\|P><STRIKE> |
SENSEX01JAN26C74900 |
| MCX Futures | <UNDERLYING><DD><MON><YY>F |
GOLD05OCT26F |
| MCX Options | <UNDERLYING><DD><MON><YY><C\|P><STRIKE> |
GOLD27NOV26P155800 |
NSE Cash Market (NSECM)
Format: NSE.<UNDERLYING>-<SERIES>
NSE.SBIN-EQ → State Bank of India, Equity series
NSE.RELIANCE-EQ → Reliance Industries, Equity series
NSE.TCS-EQ → Tata Consultancy Services, Equity series
NSE Futures & Options (NSEFO)
Options Format: <UNDERLYING><DD><MON><YY><C|P><STRIKE>
C= Call OptionP= Put Option- Strike prices are in rupees (drop trailing
.0for whole numbers)
NYKAA30DEC25C232.5 → NYKAA Call, 30-Dec-2025 expiry, Strike ₹232.50
NYKAA30DEC25P360 → NYKAA Put, 30-Dec-2025 expiry, Strike ₹360
NIFTY25JAN25C24000 → NIFTY Call, 25-Jan-2025 expiry, Strike ₹24,000
Futures Format: <UNDERLYING><DD><MON><YY>F
BANKNIFTY30DEC25F → Bank Nifty Future, 30-Dec-2025 expiry
NIFTY25JAN25F → Nifty Future, 25-Jan-2025 expiry
BSE Cash Market (BSECM)
Format: BSE.<UNDERLYING>
BSE Futures & Options (BSEFO)
Options Format: <ASSET_TYPE><DD><MON><YY><C|P><STRIKE>
SENSEX01JAN26C74900 → SENSEX Call, 01-Jan-2026 expiry, Strike ₹74,900
SENSEX01JAN26P74000 → SENSEX Put, 01-Jan-2026 expiry, Strike ₹74,000
Futures Format: <ASSET_TYPE><DD><MON><YY>F
MCX Futures & Options (MCXFO)
Options Format: <UNDERLYING><DD><MON><YY><C|P><STRIKE>
C= Call OptionP= Put Option
Futures Format: <UNDERLYING><DD><MON><YY>F
Subscription Examples
Subscribe Using Symbol Names
Subscribe Using Symbol IDs
The numbers below are illustrative ExchangeID values. Replace them with values from the current daily symbol file.
{
"code": "sub",
"mode": "full",
"symIds": [
{
"exch_seg": 1,
"ids": [5042, 4449, 91]
},
{
"exch_seg": 2,
"ids": [100, 200]
}
]
}
Subscribe to LTPC Mode
Subscribe to CAS Mode
The latency field is intentionally omitted because it is not used for CAS requests. CAS subscriptions work only for CAS-enabled symbols.
Unsubscribe from Symbols
Using Shorthand Codes
Exchange Segments
| Value | Segment | Description |
|---|---|---|
| 0 | NSE_CM | NSE Cash Market |
| 1 | NSE_FO | NSE Futures & Options; also used for NSE index symbols |
| 2 | BSE_CM | BSE Cash Market; also used for BSE index symbols |
| 3 | BSE_FO | BSE Futures & Options |
| 4 | MCX_FO | MCX Futures & Options |
Index symbol subscriptions
When subscribing to index symbols with symIds, use these exch_seg values:
- NSE index symbols:
1(NSE_FO) - BSE index symbols:
2(BSE_CM)
Index symbols cannot be subscribed in cas mode. After 3:20 PM IST, the Indicative Equilibrium Price (IEP) for an index symbol is delivered in the existing ltp field of its regular ltpc or full packet. Before then, do not interpret ltp as the IEP. No separate CAS subscription is required.
Subscribe to MCX Using ExchangeID
Use the instrument's current ExchangeID from the daily symbol file (GET /mcx) with exch_seg: 4. The numeric ID below is illustrative and must be replaced with that day's ExchangeID value.
Inbound compression (zstd)
When zstd=1 is present on the connection URL, inbound WebSocket binary messages are zstd-compressed. Outbound subscription commands remain plain JSON strings.
Client flow
- Connect with
zstd=1in the query string. - On each binary
messageevent, decompress the raw bytes with a zstd decoder. - Parse one or more binary frames from the decompressed buffer (frames may be concatenated in a single message).
Frame detection
| Frame type | Size (bytes) | Size field | Packet type offset |
|---|---|---|---|
| Response | 540 | uint32 at offset 0 | byte 4 (99) |
| LTP | 40 | int16 at offset 0 | byte 2 (1) |
| Full | 196 | int16 at offset 0 | byte 2 (2) |
| CAS | 168 | int16 at offset 0 | byte 2 (7) |
Inspect response frames first (the 540-byte layout differs from LTP, Full, and CAS). After identifying frame length n, consume payload[0:n] and continue with any remaining bytes.
Python — pip install zstandard, then ZstdDecompressor().decompress(raw_bytes).
JavaScript / Node.js — use a zstd library such as fzstd and decompress before parsing frames.
The official pyarrow-client SDK enables compression and decompression automatically in HFTDataStream; custom clients must implement the steps above.
Binary Response Format
Response Packet Structure (540 bytes)
After each subscription request, the server sends a binary acknowledgment packet.
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 4 | uint32 | size | Total packet size |
| 4 | 1 | uint8 | pktType | 99 (PKT_TYPE_RESPONSE) |
| 5 | 1 | uint8 | exchSeg | 0 (not used) |
| 6 | 16 | char[] | error_code | Error code string (null-terminated) |
| 22 | 512 | char[] | error_msg | Error message (null-terminated) |
| 534 | 1 | uint8 | request_type | 0=subscribe, 1=unsubscribe |
| 535 | 1 | uint8 | mode | 0=ltpc, 1=full, 3=cas |
| 536 | 2 | uint16 | success_count | Number of successful symbols |
| 538 | 2 | uint16 | error_count | Number of failed symbols |
Error Codes
| Code | Description |
|---|---|
SUCCESS |
All symbols processed successfully |
E_PARTIAL |
Some symbols failed |
E_ALL_INVALID |
All symbols failed |
E_INVALID_JSON |
Malformed JSON request |
E_MISSING_FIELD |
Required field missing |
E_INVALID_PARAM |
Invalid parameter value |
E_PARSE_ERROR |
General parsing error |
Market Data Packets
Futures volume vs other data feeds
When subscribing to futures (FO) instruments, the volume field on this feed is often lower than on other Arrow market data streams or third-party sources.
The HFT stream is built from exchange TBT (Tick-by-Tick) data. Volume counts only trades executed on that specific instrument token.
The standard Data Stream and most broadcast-based feeds use bcast data, where aggregated futures volume can include activity attributed to the spread order book across related legs—not just the single contract you subscribed to.
Do not compare HFT futures volume directly against bcast-based feeds without accounting for this difference.
LTP Packet (LTPC Mode - 40 bytes)
Compact price data for high-frequency monitoring.
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 2 | int16 | size | Packet size (40) |
| 2 | 1 | uint8 | pktType | 1 (PKT_TYPE_LTP) |
| 3 | 1 | uint8 | exchSeg | Exchange segment |
| 4 | 4 | int32 | symId | HFT symbol ID (ExchangeID in the daily symbol file) |
| 8 | 4 | int32 | ltp | Last traded price (paise); for index symbols, carries the IEP after 3:20 PM IST |
| 12 | 4 | int32 | vwap | Volume-weighted average price |
| 16 | 8 | int64 | volume | Traded volume (atv + btv + preMarketVolume) |
| 24 | 8 | uint64 | ltt | Last traded time (seconds) |
| 32 | 4 | uint32 | atv | Ask traded volume |
| 36 | 4 | uint32 | btv | Buy traded volume |
Full Packet (Full Mode - 196 bytes)
Complete market data with 5-level order book depth.
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 2 | int16 | size | Packet size (196) |
| 2 | 1 | uint8 | pktType | 2 (PKT_TYPE_FULL) |
| 3 | 1 | uint8 | exchSeg | Exchange segment |
| 4 | 4 | int32 | token | HFT symbol ID (ExchangeID in the daily symbol file) |
| 8 | 4 | int32 | ltp | Last traded price (paise); for index symbols, carries the IEP after 3:20 PM IST |
| 12 | 4 | int32 | ltq | Last traded quantity |
| 16 | 4 | int32 | vwap | Volume-weighted average price |
| 20 | 4 | int32 | open | Open price |
| 24 | 4 | int32 | high | High price |
| 28 | 4 | int32 | close | Close price |
| 32 | 4 | int32 | low | Low price |
| 36 | 4 | int32 | ltt | Last traded time (seconds) |
| 40 | 4 | int32 | dpr_l | Day price range low |
| 44 | 4 | int32 | dpr_h | Day price range high |
| 48 | 8 | int64 | tbq | Total buy quantity |
| 56 | 8 | int64 | tsq | Total sell quantity |
| 64 | 8 | int64 | volume | Total traded volume (atv + btv + preMarketVolume) |
| 72 | 20 | int32[5] | bid_px | Best 5 bid prices |
| 92 | 20 | int32[5] | ask_px | Best 5 ask prices |
| 112 | 20 | int32[5] | bid_size | Best 5 bid quantities |
| 132 | 20 | int32[5] | ask_size | Best 5 ask quantities |
| 152 | 10 | uint16[5] | bid_ord | Bid order counts (levels 1-5) |
| 162 | 10 | uint16[5] | ask_ord | Ask order counts (levels 1-5) |
| 172 | 8 | uint64 | oi | Open interest |
| 180 | 8 | uint64 | ts | Server timestamp (epoch ns) |
| 188 | 4 | uint32 | atv | Ask traded volume |
| 192 | 4 | uint32 | btv | Buy traded volume |
CAS Packet (CAS Mode - 168 bytes)
Closing Auction Session data with PRICE_LEVEL = 4 price levels on each side.
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 2 | int16 | size | Packet size (168) |
| 2 | 1 | uint8 | pkt_type | 7 (PKT_TYPE_CAS) |
| 3 | 1 | uint8 | exch_seg | Exchange segment |
| 4 | 4 | int32 | token | HFT symbol ID (ExchangeID in the daily symbol file) |
| 8 | 32 | uint64[4] | bid | Four CAS limit-bid price levels |
| 40 | 16 | uint32[4] | bid_size | Quantity at each bid level |
| 56 | 32 | uint64[4] | ask | Four CAS limit-ask price levels |
| 88 | 16 | uint32[4] | ask_size | Quantity at each ask level |
| 104 | 8 | uint64 | ts | Exchange timestamp in nanoseconds since epoch |
| 112 | 8 | int64 | imbalanceQty | Imbalance quantity magnitude; side is provided in imbalanceSide |
| 120 | 8 | int64 | imbalanceMktQty | Unmatched market/ATO order quantity; side is provided in imbalanceMktSide |
| 128 | 8 | uint64 | indicativePx | Indicative/equilibrium price; 0 means none or uncrossed |
| 136 | 8 | uint64 | closingRefPx | CAS closing reference price |
| 144 | 8 | uint64 | closePx | Official closing price after the CAS uncross; 0 until determined |
| 152 | 4 | uint32 | indicativeQty | Indicative matched quantity at equilibrium |
| 156 | 4 | uint32 | mktBidQty | Resting market/ATO bid quantity |
| 160 | 4 | uint32 | mktAskQty | Resting market/ATO ask quantity |
| 164 | 1 | uint8 | phase | Auction phase (AuctionPhase) |
| 165 | 1 | uint8 | imbalanceSide | Side of imbalanceQty (ImbalanceSide) |
| 166 | 1 | uint8 | imbalanceMktSide | Side of imbalanceMktQty (ImbalanceSide) |
| 167 | 1 | uint8 | onlyLimitOrders | 1 when only limit orders are allowed; otherwise 0 |
Implementation Examples
Complete WebSocket Client
class ArrowDataserverClient {
constructor() {
this.wsUrl = 'wss://socket.arrow.trade?appID=APP_ID&token=TOKEN&zstd=1';
this.ws = null;
this.callbacks = new Map();
}
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.wsUrl);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
console.log('Connected to Arrow Dataserver');
resolve();
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
this.ws.onclose = () => {
console.log('Disconnected from Arrow Dataserver');
this.reconnect();
};
});
}
subscribe(mode, symbols, latency, callback) {
const message = {
code: 'sub',
mode: mode,
symbols: symbols
};
if (mode !== 'cas') message.latency = latency || 1000;
this.ws.send(JSON.stringify(message));
symbols.forEach(sym => this.callbacks.set(sym, callback));
}
subscribeByIds(mode, symIds, latency, callback) {
const message = {
code: 'sub',
mode: mode,
symIds: symIds
};
if (mode !== 'cas') message.latency = latency || 1000;
this.ws.send(JSON.stringify(message));
}
unsubscribe(mode, symbols) {
const message = {
code: 'unsub',
mode: mode,
symbols: symbols
};
this.ws.send(JSON.stringify(message));
symbols.forEach(sym => this.callbacks.delete(sym));
}
handleMessage(data) {
// Decompress with your zstd library (e.g. fzstd in Node.js)
const payload = fzstd.decompress(new Uint8Array(data));
this.dispatchPayload(payload);
}
dispatchPayload(payload) {
let offset = 0;
while (offset < payload.length) {
const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.length - offset);
const frame = this.nextFrame(view);
if (!frame) break;
if (frame.type === 'ltp') {
this.notifyCallbacks(this.parseLTP(frame.bytes));
} else if (frame.type === 'full') {
this.notifyCallbacks(this.parseFull(frame.bytes));
} else if (frame.type === 'cas') {
this.notifyCallbacks(this.parseCAS(frame.bytes));
} else if (frame.type === 'response') {
console.log('Subscription response:', this.parseResponse(frame.bytes));
}
offset += frame.size;
}
}
nextFrame(view) {
if (view.byteLength >= 540 &&
view.getUint32(0, true) === 540 &&
view.getUint8(4) === 99) {
return { type: 'response', size: 540, bytes: new Uint8Array(view.buffer, view.byteOffset, 540) };
}
if (view.byteLength >= 40) {
const size = view.getInt16(0, true);
if (size === 40 && view.getUint8(2) === 1) {
return { type: 'ltp', size: 40, bytes: new Uint8Array(view.buffer, view.byteOffset, 40) };
}
}
if (view.byteLength >= 168) {
const size = view.getInt16(0, true);
if (size === 168 && view.getUint8(2) === 7) {
return { type: 'cas', size: 168, bytes: new Uint8Array(view.buffer, view.byteOffset, 168) };
}
}
if (view.byteLength >= 196) {
const size = view.getInt16(0, true);
if (size === 196 && view.getUint8(2) === 2) {
return { type: 'full', size: 196, bytes: new Uint8Array(view.buffer, view.byteOffset, 196) };
}
}
return null;
}
parseLTP(data) {
const buffer = new DataView(data.buffer, data.byteOffset, data.byteLength);
return {
pktType: buffer.getUint8(2),
exchSeg: buffer.getUint8(3),
symId: buffer.getInt32(4, true),
ltp: buffer.getInt32(8, true) / 100,
vwap: buffer.getInt32(12, true) / 100,
volume: Number(buffer.getBigInt64(16, true)),
ltt: Number(buffer.getBigUint64(24, true)),
atv: buffer.getUint32(32, true),
btv: buffer.getUint32(36, true)
};
}
parseFull(data) {
const buffer = new DataView(data.buffer, data.byteOffset, data.byteLength);
const tick = {
pktType: buffer.getUint8(2),
exchSeg: buffer.getUint8(3),
token: buffer.getInt32(4, true),
ltp: buffer.getInt32(8, true) / 100,
ltq: buffer.getInt32(12, true),
vwap: buffer.getInt32(16, true) / 100,
open: buffer.getInt32(20, true) / 100,
high: buffer.getInt32(24, true) / 100,
close: buffer.getInt32(28, true) / 100,
low: buffer.getInt32(32, true) / 100,
ltt: buffer.getInt32(36, true),
dprLow: buffer.getInt32(40, true) / 100,
dprHigh: buffer.getInt32(44, true) / 100,
tbq: Number(buffer.getBigInt64(48, true)),
tsq: Number(buffer.getBigInt64(56, true)),
volume: Number(buffer.getBigInt64(64, true)),
bidPrices: [],
askPrices: [],
bidSizes: [],
askSizes: [],
bidOrders: [],
askOrders: [],
oi: Number(buffer.getBigUint64(172, true)),
timestamp: Number(buffer.getBigUint64(180, true)),
atv: buffer.getUint32(188, true),
btv: buffer.getUint32(192, true)
};
// Parse 5-level depth
for (let i = 0; i < 5; i++) {
tick.bidPrices.push(buffer.getInt32(72 + i * 4, true) / 100);
tick.askPrices.push(buffer.getInt32(92 + i * 4, true) / 100);
tick.bidSizes.push(buffer.getInt32(112 + i * 4, true));
tick.askSizes.push(buffer.getInt32(132 + i * 4, true));
tick.bidOrders.push(buffer.getUint16(152 + i * 2, true));
tick.askOrders.push(buffer.getUint16(162 + i * 2, true));
}
return tick;
}
parseCAS(data) {
const buffer = new DataView(data.buffer, data.byteOffset, data.byteLength);
const tick = {
pktType: buffer.getUint8(2),
exchSeg: buffer.getUint8(3),
token: buffer.getInt32(4, true),
bidPrices: [],
bidSizes: [],
askPrices: [],
askSizes: [],
timestamp: buffer.getBigUint64(104, true),
imbalanceQty: buffer.getBigInt64(112, true),
imbalanceMktQty: buffer.getBigInt64(120, true),
indicativePx: Number(buffer.getBigUint64(128, true)) / 100,
closingRefPx: Number(buffer.getBigUint64(136, true)) / 100,
closePx: Number(buffer.getBigUint64(144, true)) / 100,
indicativeQty: buffer.getUint32(152, true),
mktBidQty: buffer.getUint32(156, true),
mktAskQty: buffer.getUint32(160, true),
phase: buffer.getUint8(164),
imbalanceSide: buffer.getUint8(165),
imbalanceMktSide: buffer.getUint8(166),
onlyLimitOrders: buffer.getUint8(167) === 1
};
for (let i = 0; i < 4; i++) {
tick.bidPrices.push(Number(buffer.getBigUint64(8 + i * 8, true)) / 100);
tick.bidSizes.push(buffer.getUint32(40 + i * 4, true));
tick.askPrices.push(Number(buffer.getBigUint64(56 + i * 8, true)) / 100);
tick.askSizes.push(buffer.getUint32(88 + i * 4, true));
}
return tick;
}
parseResponse(data) {
const buffer = new DataView(data.buffer, data.byteOffset, data.byteLength);
const decoder = new TextDecoder();
const errorCodeBytes = new Uint8Array(buffer.buffer, buffer.byteOffset + 6, 16);
const errorMsgBytes = new Uint8Array(buffer.buffer, buffer.byteOffset + 22, 512);
const responseModes = { 0: 'ltpc', 1: 'full', 3: 'cas' };
return {
size: buffer.getUint32(0, true),
pktType: buffer.getUint8(4),
errorCode: decoder.decode(errorCodeBytes).replace(/\0/g, ''),
errorMsg: decoder.decode(errorMsgBytes).replace(/\0/g, ''),
requestType: buffer.getUint8(534) === 0 ? 'subscribe' : 'unsubscribe',
mode: responseModes[buffer.getUint8(535)] || 'unknown',
successCount: buffer.getUint16(536, true),
errorCount: buffer.getUint16(538, true)
};
}
notifyCallbacks(tick) {
this.callbacks.forEach((callback) => {
callback(tick);
});
}
reconnect() {
setTimeout(() => {
console.log('Reconnecting...');
this.connect();
}, 5000);
}
}
// Usage
const client = new ArrowDataserverClient();
await client.connect();
client.subscribe('full', ['NSE.SBIN-EQ', 'NSE.RELIANCE-EQ'], 200, (tick) => {
console.log(`Token: ${tick.token}, LTP: ₹${tick.ltp}, Volume: ${tick.volume}`);
});
import websocket
import json
import struct
import zstandard as zstd
from threading import Thread
class ArrowDataserverClient:
def __init__(self):
self.ws_url = "wss://socket.arrow.trade?appID=APP_ID&token=TOKEN&zstd=1"
self.ws = None
self.callbacks = {}
self.zdec = zstd.ZstdDecompressor()
def connect(self):
self.ws = websocket.WebSocketApp(
self.ws_url,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close,
on_open=self.on_open
)
wst = Thread(target=self.ws.run_forever)
wst.daemon = True
wst.start()
def on_open(self, ws):
print("Connected to Arrow Dataserver")
def on_error(self, ws, error):
print(f"WebSocket error: {error}")
def on_close(self, ws, close_status_code, close_msg):
print("Disconnected from Arrow Dataserver")
def subscribe(self, mode, symbols, latency=1000, callback=None):
message = {
'code': 'sub',
'mode': mode,
'symbols': symbols
}
if mode != 'cas':
message['latency'] = latency
self.ws.send(json.dumps(message))
if callback:
for sym in symbols:
self.callbacks[sym] = callback
def subscribe_by_ids(self, mode, sym_ids, latency=1000):
message = {
'code': 'sub',
'mode': mode,
'symIds': sym_ids
}
if mode != 'cas':
message['latency'] = latency
self.ws.send(json.dumps(message))
def unsubscribe(self, mode, symbols):
message = {
'code': 'unsub',
'mode': mode,
'symbols': symbols
}
self.ws.send(json.dumps(message))
def on_message(self, ws, message):
payload = self.zdec.decompress(bytes(message))
self.dispatch_payload(payload)
def dispatch_payload(self, payload):
while payload:
frame, payload = self.next_frame(payload)
if frame is None:
break
if frame['type'] == 'ltp':
tick = self.parse_ltp(frame['data'])
self.notify_callbacks(tick)
elif frame['type'] == 'full':
tick = self.parse_full(frame['data'])
self.notify_callbacks(tick)
elif frame['type'] == 'cas':
tick = self.parse_cas(frame['data'])
self.notify_callbacks(tick)
elif frame['type'] == 'response':
response = self.parse_response(frame['data'])
print(f"Subscription response: {response}")
def next_frame(self, payload):
if len(payload) >= 540:
size = struct.unpack('<I', payload[0:4])[0]
if size == 540 and payload[4] == 99:
return {'type': 'response', 'data': payload[:540]}, payload[540:]
if len(payload) >= 40:
size = struct.unpack('<h', payload[0:2])[0]
if size == 40 and payload[2] == 1:
return {'type': 'ltp', 'data': payload[:40]}, payload[40:]
if len(payload) >= 168:
size = struct.unpack('<h', payload[0:2])[0]
if size == 168 and payload[2] == 7:
return {'type': 'cas', 'data': payload[:168]}, payload[168:]
if len(payload) >= 196:
size = struct.unpack('<h', payload[0:2])[0]
if size == 196 and payload[2] == 2:
return {'type': 'full', 'data': payload[:196]}, payload[196:]
return None, b''
def parse_ltp(self, data):
return {
'pkt_type': struct.unpack('<B', data[2:3])[0],
'exch_seg': struct.unpack('<B', data[3:4])[0],
'sym_id': struct.unpack('<i', data[4:8])[0],
'ltp': struct.unpack('<i', data[8:12])[0] / 100,
'vwap': struct.unpack('<i', data[12:16])[0] / 100,
'volume': struct.unpack('<q', data[16:24])[0],
'ltt': struct.unpack('<Q', data[24:32])[0],
'atv': struct.unpack('<I', data[32:36])[0],
'btv': struct.unpack('<I', data[36:40])[0]
}
def parse_full(self, data):
tick = {
'pkt_type': struct.unpack('<B', data[2:3])[0],
'exch_seg': struct.unpack('<B', data[3:4])[0],
'token': struct.unpack('<i', data[4:8])[0],
'ltp': struct.unpack('<i', data[8:12])[0] / 100,
'ltq': struct.unpack('<i', data[12:16])[0],
'vwap': struct.unpack('<i', data[16:20])[0] / 100,
'open': struct.unpack('<i', data[20:24])[0] / 100,
'high': struct.unpack('<i', data[24:28])[0] / 100,
'close': struct.unpack('<i', data[28:32])[0] / 100,
'low': struct.unpack('<i', data[32:36])[0] / 100,
'ltt': struct.unpack('<i', data[36:40])[0],
'dpr_low': struct.unpack('<i', data[40:44])[0] / 100,
'dpr_high': struct.unpack('<i', data[44:48])[0] / 100,
'tbq': struct.unpack('<q', data[48:56])[0],
'tsq': struct.unpack('<q', data[56:64])[0],
'volume': struct.unpack('<q', data[64:72])[0],
'bid_prices': [],
'ask_prices': [],
'bid_sizes': [],
'ask_sizes': [],
'bid_orders': [],
'ask_orders': [],
'oi': struct.unpack('<Q', data[172:180])[0],
'timestamp': struct.unpack('<Q', data[180:188])[0],
'atv': struct.unpack('<I', data[188:192])[0],
'btv': struct.unpack('<I', data[192:196])[0]
}
# Parse 5-level depth
for i in range(5):
tick['bid_prices'].append(struct.unpack('<i', data[72 + i*4:76 + i*4])[0] / 100)
tick['ask_prices'].append(struct.unpack('<i', data[92 + i*4:96 + i*4])[0] / 100)
tick['bid_sizes'].append(struct.unpack('<i', data[112 + i*4:116 + i*4])[0])
tick['ask_sizes'].append(struct.unpack('<i', data[132 + i*4:136 + i*4])[0])
tick['bid_orders'].append(struct.unpack('<H', data[152 + i*2:154 + i*2])[0])
tick['ask_orders'].append(struct.unpack('<H', data[162 + i*2:164 + i*2])[0])
return tick
def parse_cas(self, data):
tick = {
'pkt_type': struct.unpack_from('<B', data, 2)[0],
'exch_seg': struct.unpack_from('<B', data, 3)[0],
'token': struct.unpack_from('<i', data, 4)[0],
'bid_prices': [],
'bid_sizes': [],
'ask_prices': [],
'ask_sizes': [],
'timestamp': struct.unpack_from('<Q', data, 104)[0],
'imbalance_qty': struct.unpack_from('<q', data, 112)[0],
'imbalance_mkt_qty': struct.unpack_from('<q', data, 120)[0],
'indicative_px': struct.unpack_from('<Q', data, 128)[0] / 100,
'closing_ref_px': struct.unpack_from('<Q', data, 136)[0] / 100,
'close_px': struct.unpack_from('<Q', data, 144)[0] / 100,
'indicative_qty': struct.unpack_from('<I', data, 152)[0],
'mkt_bid_qty': struct.unpack_from('<I', data, 156)[0],
'mkt_ask_qty': struct.unpack_from('<I', data, 160)[0],
'phase': struct.unpack_from('<B', data, 164)[0],
'imbalance_side': struct.unpack_from('<B', data, 165)[0],
'imbalance_mkt_side': struct.unpack_from('<B', data, 166)[0],
'only_limit_orders': struct.unpack_from('<B', data, 167)[0] == 1
}
for i in range(4):
tick['bid_prices'].append(struct.unpack_from('<Q', data, 8 + i*8)[0] / 100)
tick['bid_sizes'].append(struct.unpack_from('<I', data, 40 + i*4)[0])
tick['ask_prices'].append(struct.unpack_from('<Q', data, 56 + i*8)[0] / 100)
tick['ask_sizes'].append(struct.unpack_from('<I', data, 88 + i*4)[0])
return tick
def parse_response(self, data):
error_code = data[6:22].decode('utf-8').rstrip('\x00')
error_msg = data[22:534].decode('utf-8').rstrip('\x00')
response_modes = {0: 'ltpc', 1: 'full', 3: 'cas'}
return {
'size': struct.unpack('<I', data[0:4])[0],
'pkt_type': struct.unpack('<B', data[4:5])[0],
'error_code': error_code,
'error_msg': error_msg,
'request_type': 'subscribe' if data[534] == 0 else 'unsubscribe',
'mode': response_modes.get(data[535], 'unknown'),
'success_count': struct.unpack('<H', data[536:538])[0],
'error_count': struct.unpack('<H', data[538:540])[0]
}
def notify_callbacks(self, tick):
for callback in self.callbacks.values():
callback(tick)
# Usage
client = ArrowDataserverClient()
client.connect()
def price_handler(tick):
print(f"Token: {tick['token']}, LTP: ₹{tick['ltp']}, Volume: {tick['volume']}")
import time
time.sleep(1) # Wait for connection
client.subscribe('full', ['NSE.SBIN-EQ', 'NSE.RELIANCE-EQ'], 200, price_handler)
# Keep running
while True:
time.sleep(1)
Error Handling and Reconnection
Connection Management
class ReconnectingClient {
constructor() {
this.reconnectInterval = 5000;
this.maxReconnectAttempts = 10;
this.reconnectAttempts = 0;
this.subscriptions = [];
}
connect() {
this.ws = new WebSocket('wss://socket.arrow.trade?appID=APP_ID&token=TOKEN&zstd=1');
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.resubscribeAll();
};
this.ws.onclose = () => {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
};
}
resubscribeAll() {
this.subscriptions.forEach(sub => {
this.ws.send(JSON.stringify(sub));
});
}
}
Common Error Scenarios
| Error | Cause | Solution |
|---|---|---|
E_INVALID_JSON |
Malformed JSON request | Validate JSON before sending |
E_MISSING_FIELD |
Required field missing | Include all required fields |
E_INVALID_PARAM |
Invalid parameter value | Check parameter constraints |
E_ALL_INVALID |
All symbols invalid | Verify symbol name formats |
E_PARTIAL |
Some symbols failed | Check error_msg for details |
| Connection timeout | Network issues | Implement reconnection with backoff |
Rate Limits and Constraints
| Constraint | Limit |
|---|---|
| Requests per second | 100 per connection |
| Maximum active symbol subscriptions per trading account (across all connections) | 1,024 |
| Maximum symbols per subscription request | 512 |
| Maximum request size | 16 KB |
| Latency range (LTPC and Full) | 50 ms - 60,000 ms |
Data Type Notes
| Type | Description |
|---|---|
| Prices | All prices are in paise (1 rupee = 100 paise) |
| Timestamps | ts uses nanoseconds since Unix epoch; ltt uses seconds since Unix epoch |
| Byte order | Little-endian for all multi-byte integers |
Best Practices
Subscription Management
- Batch subscriptions: Subscribe to multiple symbols in a single request to reduce overhead
- Mode selection: Use
ltpcfor minimal last-traded-price data,fullfor complete market depth, andcasonly for CAS-enabled symbols during the closing auction - Symbol validation: Verify symbol names/IDs before sending requests
- Latency tuning: For LTPC and Full modes, adjust latency based on your application needs (lower for real-time, higher for reduced bandwidth)
Error Handling
- Connection errors: Reconnect with exponential backoff
- Invalid symbols: Check the
error_msgfield for specific invalid symbols - Subscription limits: Keep each request within 512 symbols and the trading account's combined active subscriptions across all connections within 1,024
- Parse errors: Validate JSON format before sending
Performance Optimization
- Memory management: Parse and process data efficiently to prevent memory leaks
- Binary parsing: Use optimized binary parsers for high-frequency data
- Buffer handling: Implement proper buffer management for binary data streams
- Connection pooling: Reuse connections where possible
Market Data Applications
Real-Time Price Monitoring
function formatTick(tick) {
const change = ((tick.ltp - tick.close) / tick.close * 100).toFixed(2);
const direction = tick.ltp >= tick.close ? '▲' : '▼';
return `${tick.token}: ₹${tick.ltp} ${direction} ${change}%`;
}
Order Book Analysis
function analyzeOrderBook(tick) {
const bestBid = tick.bidPrices[0];
const bestAsk = tick.askPrices[0];
const spread = bestAsk - bestBid;
const spreadPercent = (spread / bestBid * 100).toFixed(4);
const totalBidDepth = tick.bidSizes.reduce((a, b) => a + b, 0);
const totalAskDepth = tick.askSizes.reduce((a, b) => a + b, 0);
return {
spread: spread,
spreadPercent: spreadPercent,
bidDepth: totalBidDepth,
askDepth: totalAskDepth,
imbalance: (totalBidDepth - totalAskDepth) / (totalBidDepth + totalAskDepth)
};
}