Skip to content

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=1 is 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

wss://socket.arrow.trade?appID=<APP_ID>&token=<TOKEN>&zstd=1

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.

{
  "code": "string",
  "mode": "string",
  "symbols": ["string"],
  "latency": 1000
}

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 Option
  • P = Put Option
  • Strike prices are in rupees (drop trailing .0 for 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.SBIN      → State Bank of India
BSE.RELIANCE  → Reliance Industries
BSE.HDFC      → HDFC Bank

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

SENSEX01JAN26F → SENSEX Future, 01-Jan-2026 expiry

MCX Futures & Options (MCXFO)

Options Format: <UNDERLYING><DD><MON><YY><C|P><STRIKE>

  • C = Call Option
  • P = Put Option
GOLD27NOV26P155800 → Gold Put, 27-Nov-2026 expiry, Strike ₹155,800

Futures Format: <UNDERLYING><DD><MON><YY>F

GOLD05OCT26F → Gold Future, 05-Oct-2026 expiry

Subscription Examples

Subscribe Using Symbol Names

{
  "code": "sub",
  "mode": "full",
  "symbols": ["NSE.SBIN-EQ", "BSE.RELIANCE"],
  "latency": 200
}

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

{
  "code": "sub",
  "mode": "ltpc",
  "symbols": ["NSE.SBIN-EQ", "NSE.RELIANCE-EQ"],
  "latency": 100
}

Subscribe to CAS Mode

{
  "code": "sub",
  "mode": "cas",
  "symbols": ["NSE.RELIANCE-EQ"]
}

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

{
  "code": "unsub",
  "mode": "full",
  "symbols": ["NSE.SBIN-EQ"]
}

Using Shorthand Codes

{
  "code": "s",
  "mode": "l",
  "symbols": ["NSE.SBIN-EQ"],
  "l": 500
}

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.

{
  "code": "sub",
  "mode": "ltpc",
  "latency": 200,
  "symIds": [
    {
      "exch_seg": 4,
      "ids": [123456]
    }
  ]
}

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

  1. Connect with zstd=1 in the query string.
  2. On each binary message event, decompress the raw bytes with a zstd decoder.
  3. 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.

Pythonpip 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 ltpc for minimal last-traded-price data, full for complete market depth, and cas only 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_msg field 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)
    };
}

Volume Analysis

function analyzeVolume(tick) {
    return {
        totalVolume: tick.volume,
        buyVolume: tick.btv,
        sellVolume: tick.atv,
        buyRatio: (tick.btv / (tick.btv + tick.atv) * 100).toFixed(2),
        vwap: tick.vwap
    };
}