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 (LTP) and Full market depth data across NSE and BSE exchanges.
Key Features
- Binary Protocol: Optimized binary frames for minimal latency and bandwidth
- Dual Data Modes: LTPC (compact) or Full (complete market depth)
- Flexible Symbol Formats: Subscribe using symbol names or numeric IDs
- Cross-Exchange Coverage: Unified data stream across NSE CM, NSE FO, BSE CM, and BSE FO
- Configurable Latency: Adjust tick intervals upto 50ms (Super fast)
- Scalable Architecture: Support for up to 4,096 simultaneous symbol subscriptions
Connection Setup
WebSocket Endpoint
Connection Parameters
| Parameter | Value | Description |
|---|---|---|
appID |
APP_ID |
Application identifier |
token |
TOKEN |
Authentication token |
Establishing Connection
const wsUrl = 'wss://socket.arrow.trade?appID=APP_ID&token=TOKEN';
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = function(event) {
console.log('Connected to Arrow Dataserver');
};
ws.onmessage = function(event) {
const data = new Uint8Array(event.data);
const marketData = parseMarketData(data);
console.log(marketData);
};
import websocket
import struct
ws_url = "wss://socket.arrow.trade?appID=APP_ID&token=TOKEN"
def on_message(ws, message):
market_data = parse_market_data(message)
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()
Subscription Management
Request Format
All subscription requests are sent as JSON strings over the WebSocket connection.
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) or full/f (complete) |
symbols |
Conditional | array | Symbol names (mutually exclusive with symIds) |
symIds |
Conditional | array | Symbol ID objects (mutually exclusive with symbols) |
latency |
No | integer | Tick interval in ms (upto 50, default: 1000) |
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 |
192 bytes | Complete market data with depth | Order book analysis, full market view |
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 |
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
Subscription Examples
Subscribe Using Symbol Names
Subscribe Using Symbol IDs
{
"code": "sub",
"mode": "full",
"symIds": [
{
"exch_seg": 1,
"ids": [5042, 4449, 91]
},
{
"exch_seg": 2,
"ids": [100, 200]
}
]
}
Subscribe to LTPC Mode
Unsubscribe from Symbols
Using Shorthand Codes
Exchange Segments
| Value | Segment | Description |
|---|---|---|
| 0 | NSE_CM | NSE Cash Market |
| 1 | NSE_FO | NSE Futures & Options |
| 2 | BSE_CM | BSE Cash Market |
| 3 | BSE_FO | BSE Futures & Options |
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 |
| 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
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 | Symbol/Token ID |
| 8 | 4 | int32 | ltp | Last traded price (paise) |
| 12 | 4 | int32 | vwap | Volume-weighted average price |
| 16 | 8 | int64 | volume | Traded volume (units) |
| 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 - 192 bytes)
Complete market data with 5-level order book depth.
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 2 | int16 | size | Packet size (192) |
| 2 | 1 | uint8 | pktType | 2 (PKT_TYPE_FULL) |
| 3 | 1 | uint8 | exchSeg | Exchange segment |
| 4 | 4 | int32 | token | Symbol/Token ID |
| 8 | 4 | int32 | ltp | Last traded price |
| 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 volume |
| 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 | 4 | int32 | oi | Open interest |
| 176 | 8 | uint64 | ts | Server timestamp (epoch ns) |
| 184 | 4 | uint32 | atv | Ask traded volume |
| 188 | 4 | uint32 | btv | Buy traded volume |
Implementation Examples
Complete WebSocket Client
class ArrowDataserverClient {
constructor() {
this.wsUrl = 'wss://socket.arrow.trade?appID=APP_ID&token=TOKEN';
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,
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,
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) {
const buffer = new DataView(data);
const size = buffer.getInt16(0, true);
if (size === 40) {
const tick = this.parseLTP(buffer);
this.notifyCallbacks(tick);
} else if (size === 192) {
const tick = this.parseFull(buffer);
this.notifyCallbacks(tick);
} else if (size === 540) {
const response = this.parseResponse(buffer);
console.log('Subscription response:', response);
}
}
parseLTP(buffer) {
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(buffer) {
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: buffer.getInt32(172, true),
timestamp: Number(buffer.getBigUint64(176, true)),
atv: buffer.getUint32(184, true),
btv: buffer.getUint32(188, 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;
}
parseResponse(buffer) {
const decoder = new TextDecoder();
const errorCodeBytes = new Uint8Array(buffer.buffer, 6, 16);
const errorMsgBytes = new Uint8Array(buffer.buffer, 22, 512);
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: buffer.getUint8(535) === 0 ? 'ltpc' : 'full',
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
from threading import Thread
class ArrowDataserverClient:
def __init__(self):
self.ws_url = "wss://socket.arrow.trade?appID=APP_ID&token=TOKEN"
self.ws = None
self.callbacks = {}
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,
'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,
'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):
data = bytes(message)
size = struct.unpack('<h', data[0:2])[0]
if size == 40:
tick = self.parse_ltp(data)
self.notify_callbacks(tick)
elif size == 192:
tick = self.parse_full(data)
self.notify_callbacks(tick)
elif size == 540:
response = self.parse_response(data)
print(f"Subscription response: {response}")
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('<i', data[172:176])[0],
'timestamp': struct.unpack('<Q', data[176:184])[0],
'atv': struct.unpack('<I', data[184:188])[0],
'btv': struct.unpack('<I', data[188:192])[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_response(self, data):
error_code = data[6:22].decode('utf-8').rstrip('\x00')
error_msg = data[22:534].decode('utf-8').rstrip('\x00')
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': 'ltpc' if data[535] == 0 else 'full',
'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');
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 symbols per subscription | 4,096 |
| Maximum request size | 16 KB |
| Latency range | 50ms - 60,000ms |
Data Type Notes
| Type | Description |
|---|---|
| Prices | All prices are in paise (1 rupee = 100 paise) |
| Timestamps | Nanoseconds 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 data when you only need last traded prices; usefullfor market depth and complete price information - Symbol validation: Verify symbol names/IDs before sending requests
- Latency tuning: 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: Split large requests into smaller batches
- 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)
};
}