Skip to content

stoxkart

Stoxkart's two websockets: the broadcast quote feed Stoxkart's website streams from, and the order update socket.

Neither is documented for API users, so Stoxkart may change them without notice. Both read a synchronous websocket-client connection in a loop of their own, with their own silence and eviction handling, so neither subclasses BrokerWebsocket: StoxkartQuoteStream and StoxkartOrderSocket are the one exception to the package's shared reconnect loop.

Quotes. Stoxkart's API documentation gives a binary quote websocket at ws://inmob.stoxkart.com:7763, which could not be reached on 2026-09-15; the website streams from wss://broadcasting-v2.stoxkart.com/ instead, and the stream connects there. The feed takes no login: the connection header's token field is left blank, exactly as the website sends it. The protocol is Stoxkart's binary broadcast format, all little-endian. A request is an 83-byte header - a request code (1 byte), the message length (2 bytes), a 30-byte client name and a blank 50-byte token - followed for a subscription by the exchange segment (1 byte), -1 (4 bytes), a scrip count of 1 (1 byte), a blank 20-byte watchlist name and the token as 20 bytes, 129 bytes in all. The stream sends the connection header (code 10), then for each instrument a trade subscription (code 12) and a depth subscription (code 23). Every answer is a run of packets, each starting with an 11-byte header: segment (1 byte), scrip id (4), a second id (4), packet length (1) and packet code (1).

Code Packet Fields used
1 Trade last price, last quantity, volume, average price, open interest, last trade time, last update time
2 Depth five levels of bid quantity, ask quantity, bid orders, ask orders, bid price, ask price
3 OHLC open, high, low (its close is not used)
6 Top of book total quantity offered, total quantity bid
32 Previous close the previous session's close

Circuit limits (33), the 52-week range (36) and any other packet are skipped. The text ping is sent every five seconds and answered with pong; a socket silent for 30 seconds, or one that sends the text reconnect, is closed and opened again. Stoxkart sends each instrument's packets together in one frame, so one tick is handed on per instrument per frame, and only when it differs from the previous tick apart from received_at. Prices arrive as 32-bit floats in rupees and are rounded to four decimal places, which removes the float noise without touching a real tick. Stoxkart counts seconds from 1980-01-01, the trade time in UTC and the update time on India's wall clock, so 315532800 seconds are added and, for the update time, 19800 taken off.

Order updates. StoxkartOrderSocket first asks POST https://openapi-v2.stoxkart.com/websocket/authenticate for a RequestId, with the client code, the API access token, x-platform: api and the api key as headers, then opens wss://openapi-v2.stoxkart.com/websocket/v2/connect?x-client-id=<client>&x-platform=api&RequestId=<RequestId>. It sends {"type": "heartbeat"} on connecting and every 30 seconds, as the website does, and updates arrive as JSON text. Stoxkart keeps one order socket per client, and a new connection closes the older one with a close reason containing new incoming connection; the socket then waits five minutes before reclaiming it, so it and a logged-in website or app knock each other off at most that often. An authentication refused with AuthorizationError logs in again before retrying.

Neither stream writes Redis. The quote stream hands each frame's ticks to on_ticks, and the order socket hands each frame's messages to on_updates; the scripts in bin/stoxkart/ do the writing.

Classes:

Name Description
StoxkartBroadcastRequests

Builds the binary requests Stoxkart's broadcast websocket accepts.

StoxkartInstrumentState

The latest values Stoxkart's packets have given for one instrument, from which its tick is built.

StoxkartPacketDecoder

Reads the packets in one broadcast frame into the instruments' states.

StoxkartQuoteStream

One connection to Stoxkart's broadcast websocket, reconnected until stopped.

StoxkartSessionRefused

Stoxkart refused the order socket's authentication because the session is dead.

StoxkartSession

The Stoxkart API login the order socket authenticates with.

StoxkartOrderSocket

The order update websocket, authenticated with the API session and reconnected until stopped.

StoxkartBroadcastRequests

Builds the binary requests Stoxkart's broadcast websocket accepts.

Example programs

examples/stock_brokers/websockets/stoxkart/StoxkartBroadcastRequests/example_1_connection_header.py
"""Builds the connection header a Stoxkart broadcast stream sends first, and reads its fields back.

Every request to Stoxkart's broadcast websocket starts with an 83-byte header, little endian: a request code (1 byte), the whole request's length (2 bytes), a 30-byte client name and a 50-byte token. `StoxkartBroadcastRequests.connection_header` builds the request that opens a session, code 10, whose token is left blank exactly as Stoxkart's website sends it, because the feed takes no login.

This program builds the header and unpacks it with `struct`. It needs no stand-ins.

Notice that the length field says 83, the client name is padded with zero bytes, and the token is empty.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartBroadcastRequests/example_1_connection_header.py
"""

import struct

from stock_brokers.websockets.stoxkart import (
    StoxkartBroadcastRequests,
)


class ConnectionHeaderExample:
    """Builds and reads back the connection header.

    Attributes:
        requests (StoxkartBroadcastRequests): The request builder being shown.
    """

    def __init__(self):
        """Builds the request builder.

        Returns:
            None: This method returns nothing.
        """
        self.requests = StoxkartBroadcastRequests()

    def run(self):
        """Prints the header's size and fields.

        Returns:
            None: This method returns nothing.
        """
        header = self.requests.connection_header()
        code, length, client_name, token = struct.unpack('<BH30s50s', header)
        print(f'{len(header)} bytes: code={code} length={length}')
        print(f"client name={client_name.rstrip(bytes(1))!r} token={token.rstrip(bytes(1))!r}")


if __name__ == '__main__':
    ConnectionHeaderExample().run()

It prints:

83 bytes: code=10 length=83
client name=b'unified_broker_interface' token=b''
examples/stock_brokers/websockets/stoxkart/StoxkartBroadcastRequests/example_2_trade_and_depth_subscriptions.py
"""Builds the trade and depth subscriptions for one Stoxkart instrument and reads their fields back.

A Stoxkart subscription is the 83-byte header followed by the exchange segment (1 byte), -1 (4 bytes), a scrip count of 1 (1 byte), a blank 20-byte watchlist name and the token as 20 bytes, 129 bytes in all. The quote stream sends two per instrument: code 12 for trades and code 23 for depth. `StoxkartBroadcastRequests.subscription` builds either one.

This program builds both for NSE token 2885 (segment 1) and unpacks them with `struct`. It needs no stand-ins.

Notice that the two requests differ only in their first byte.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartBroadcastRequests/example_2_trade_and_depth_subscriptions.py
"""

import struct

from stock_brokers.websockets.stoxkart import (
    StoxkartBroadcastRequests,
)


class TradeAndDepthSubscriptionsExample:
    """Builds and reads back two subscriptions.

    Attributes:
        requests (StoxkartBroadcastRequests): The request builder being shown.
    """

    def __init__(self):
        """Builds the request builder.

        Returns:
            None: This method returns nothing.
        """
        self.requests = StoxkartBroadcastRequests()

    def run(self):
        """Prints each subscription's size and fields, and where the two differ.

        Returns:
            None: This method returns nothing.
        """
        trade = self.requests.subscription(12, 1, '2885')
        depth = self.requests.subscription(23, 1, '2885')
        subscriptions = [
            trade,
            depth,
        ]
        for request in subscriptions:
            code, length = struct.unpack_from('<BH', request)
            segment, minus_one, count, watchlist, token = struct.unpack_from('<Bib20s20s', request, 83)
            print(f'{len(request)} bytes: code={code} length={length} segment={segment} {minus_one} count={count} token={token.rstrip(bytes(1))!r}')
        differing = []
        for index in range(len(trade)):
            if trade[index] != depth[index]:
                differing.append(index)
        print(f'Byte positions that differ: {differing}')


if __name__ == '__main__':
    TradeAndDepthSubscriptionsExample().run()

It prints:

129 bytes: code=12 length=129 segment=1 -1 count=1 token=b'2885'
129 bytes: code=23 length=129 segment=1 -1 count=1 token=b'2885'
Byte positions that differ: [0]

Methods:

Name Description
connection_header

Builds the request that opens a session on the socket.

subscription

Builds a request that subscribes one instrument.

connection_header

connection_header()

Builds the request that opens a session on the socket.

Returns:

Type Description
bytes

The 83-byte connection header.

Example usage

From example_1_connection_header.py
header = self.requests.connection_header()
Source code in stock_brokers/websockets/stoxkart.py
def connection_header(self):
    """
    Builds the request that opens a session on the socket.

    Returns:
        bytes: The 83-byte connection header.
    """
    return self._header(CONNECTION_REQUEST_CODE, HEADER_LENGTH)

subscription

subscription(code, segment, token)

Builds a request that subscribes one instrument.

Parameters:

Name Type Description Default
code int

The subscription code, 12 for trades or 23 for depth.

required
segment int

The broadcast segment of the instrument's exchange.

required
token str

The instrument's token.

required

Returns:

Type Description
bytes

The 129-byte subscription request.

Example usage

From example_2_trade_and_depth_subscriptions.py
trade = self.requests.subscription(12, 1, '2885')
From example_2_trade_and_depth_subscriptions.py
depth = self.requests.subscription(23, 1, '2885')
Source code in stock_brokers/websockets/stoxkart.py
def subscription(self, code, segment, token):
    """
    Builds a request that subscribes one instrument.

    Args:
        code (int): The subscription code, 12 for trades or 23 for depth.
        segment (int): The broadcast segment of the instrument's exchange.
        token (str): The instrument's token.

    Returns:
        bytes: The 129-byte subscription request.
    """
    body = struct.pack("<Bib20s20s", segment, -1, 1, b"", token.encode())
    return self._header(code, SUBSCRIBE_LENGTH) + body

_header

_header(code, length)

Builds the header every request starts with.

Parameters:

Name Type Description Default
code int

The request code.

required
length int

The whole request's length in bytes.

required

Returns:

Type Description
bytes

The 83-byte header with a blank token.

Source code in stock_brokers/websockets/stoxkart.py
def _header(self, code, length):
    """
    Builds the header every request starts with.

    Args:
        code (int): The request code.
        length (int): The whole request's length in bytes.

    Returns:
        bytes: The 83-byte header with a blank token.
    """
    return struct.pack("<BH30s50s", code, length, CLIENT_NAME, b"")

StoxkartInstrumentState

StoxkartInstrumentState(instrument, name)

The latest values Stoxkart's packets have given for one instrument, from which its tick is built.

Attributes:

Name Type Description
instrument str

The instrument as subscribed, EXCHANGE:TOKEN.

name str

The instrument's name, or an empty string.

exchange str

Stoxkart's exchange.

values dict

The latest value of every field the packets have supplied.

depth dict

The latest buy and sell levels.

Starts with no values.

Parameters:

Name Type Description Default
instrument str

The instrument as subscribed, EXCHANGE:TOKEN.

required
name str

The instrument's name, or an empty string.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/stoxkart/StoxkartInstrumentState/example_1_ticks_only_when_changed.py
"""Builds ticks from a Stoxkart instrument's state, and shows that an unchanged state gives no tick.

`StoxkartInstrumentState` holds the latest value of every field Stoxkart's packets have given for one instrument in `values`, and its book in `depth`. `tick` builds a normalized tick from them, but only once a trade packet has supplied a last price, and only when the tick differs from the previous one apart from `received_at`, so the stream never repeats itself.

This program fills the state's values by hand, as `StoxkartPacketDecoder` would, and asks for a tick four times. It needs no stand-ins. `received_at` is a fixed number here; the stream passes the time it decoded the frame.

Notice that there is no tick before the last price, that the second call with nothing changed gives none, and that a changed price gives one again.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartInstrumentState/example_1_ticks_only_when_changed.py
"""


from stock_brokers.websockets.stoxkart import (
    StoxkartInstrumentState,
)


class TicksOnlyWhenChangedExample:
    """Asks one instrument state for ticks as its values change.

    Attributes:
        state (StoxkartInstrumentState): The state being shown.
    """

    def __init__(self):
        """Builds the state for RELIANCE.

        Returns:
            None: This method returns nothing.
        """
        self.state = StoxkartInstrumentState('NSE:2885', 'NSE:RELIANCE')

    def print_tick(self, tick):
        """Prints the main fields of a tick, or that there is none.

        Args:
            tick (dict | None): The tick.

        Returns:
            None: This method returns nothing.
        """
        if tick is None:
            print('No tick')
            return
        print(f"Tick {tick['id']} on {tick['exchange']}: last_price={tick['last_price']} change={tick['change']} ohlc={tick['ohlc']} received_at={tick['received_at']}")

    def run(self):
        """Prints the tick before a price, after one, unchanged, and after a change.

        Returns:
            None: This method returns nothing.
        """
        print(f'Instrument {self.state.instrument} named {self.state.name} on {self.state.exchange}')
        self.print_tick(self.state.tick(1000.0))
        self.state.values['last_price'] = 2950.5
        self.state.values['previous_close'] = 2910.0
        self.print_tick(self.state.tick(1001.0))
        self.print_tick(self.state.tick(1002.0))
        self.state.values['last_price'] = 2951.0
        self.print_tick(self.state.tick(1003.0))
        print(f'Values held: {self.state.values}')


if __name__ == '__main__':
    TicksOnlyWhenChangedExample().run()

It prints:

Instrument NSE:2885 named NSE:RELIANCE on NSE
No tick
Tick NSE:RELIANCE on NSE: last_price=2950.5 change=1.391753 ohlc={'open': None, 'high': None, 'low': None, 'close': 2910.0} received_at=1001.0
No tick
Tick NSE:RELIANCE on NSE: last_price=2951.0 change=1.408935 ohlc={'open': None, 'high': None, 'low': None, 'close': 2910.0} received_at=1003.0
Values held: {'last_price': 2951.0, 'previous_close': 2910.0}
examples/stock_brokers/websockets/stoxkart/StoxkartInstrumentState/example_2_unnamed_with_depth.py
"""Builds a tick for an unnamed Stoxkart instrument with a book and no previous close.

A Stoxkart instrument whose name is an empty string is keyed by the `EXCHANGE:TOKEN` it was subscribed as. Its tick is always in full mode and carries a copy of the state's `depth`, so a later change to the book does not alter a tick already handed on. Without a previous close there is no percentage change.

This program fills a state's values and book by hand and builds one tick. It needs no stand-ins.

Notice the id, the `None` change and close, and that the tick's book is unchanged after the state's book is cleared.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartInstrumentState/example_2_unnamed_with_depth.py
"""


from stock_brokers.websockets.stoxkart import (
    StoxkartInstrumentState,
)


class UnnamedWithDepthExample:
    """Builds a tick for an unnamed instrument with a book.

    Attributes:
        state (StoxkartInstrumentState): The state being shown.
    """

    def __init__(self):
        """Builds the state and fills its values and book.

        Returns:
            None: This method returns nothing.
        """
        self.state = StoxkartInstrumentState('MCX:565899', '')
        self.state.values['last_price'] = 6120.0
        self.state.values['volume'] = 8450
        self.state.depth['buy'] = [
            {
                'quantity': 3,
                'price': 6119.0,
                'orders': 2,
            },
        ]
        self.state.depth['sell'] = [
            {
                'quantity': 5,
                'price': 6121.0,
                'orders': 1,
            },
        ]

    def run(self):
        """Prints the tick, clears the state's book, and prints the tick's book again.

        Returns:
            None: This method returns nothing.
        """
        tick = self.state.tick(1000.0)
        print(f"Tick {tick['id']} in {tick['mode']} mode: last_price={tick['last_price']} change={tick['change']} close={tick['ohlc']['close']}")
        print(f"Book: {tick['depth']}")
        self.state.depth['buy'].clear()
        print(f"Book after the state's buy side was cleared: {tick['depth']}")


if __name__ == '__main__':
    UnnamedWithDepthExample().run()

It prints:

Tick MCX:565899 in full mode: last_price=6120.0 change=None close=None
Book: {'buy': [{'quantity': 3, 'price': 6119.0, 'orders': 2}], 'sell': [{'quantity': 5, 'price': 6121.0, 'orders': 1}]}
Book after the state's buy side was cleared: {'buy': [{'quantity': 3, 'price': 6119.0, 'orders': 2}], 'sell': [{'quantity': 5, 'price': 6121.0, 'orders': 1}]}

Methods:

Name Description
tick

Builds the tick from the latest values.

Source code in stock_brokers/websockets/stoxkart.py
def __init__(self, instrument, name):
    """
    Starts with no values.

    Args:
        instrument (str): The instrument as subscribed, `EXCHANGE:TOKEN`.
        name (str): The instrument's name, or an empty string.

    Returns:
        None: This method returns nothing.
    """
    self.instrument = instrument
    self.name = name
    self.exchange = instrument.split(":")[0]
    self.values = {}
    self.depth = {
        "buy": [],
        "sell": [],
    }
    self._previous_fingerprint = None

tick

tick(received_at)

Builds the tick from the latest values.

Parameters:

Name Type Description Default
received_at float

The epoch at which the frame was decoded.

required

Returns:

Type Description
dict | None

The tick, or None when no trade packet has arrived yet or nothing changed since the last tick.

Example usage

From example_1_ticks_only_when_changed.py
self.print_tick(self.state.tick(1000.0))
From example_1_ticks_only_when_changed.py
self.print_tick(self.state.tick(1001.0))
From example_1_ticks_only_when_changed.py
self.print_tick(self.state.tick(1002.0))
Source code in stock_brokers/websockets/stoxkart.py
def tick(self, received_at):
    """
    Builds the tick from the latest values.

    Args:
        received_at (float): The epoch at which the frame was decoded.

    Returns:
        dict | None: The tick, or None when no trade packet has arrived yet or nothing changed since the last tick.
    """
    if "last_price" not in self.values:
        return None
    values = self.values
    previous_close = values.get("previous_close")
    change = None
    if previous_close:
        change = round((values["last_price"] - previous_close) / previous_close * 100, 6)
    tick = {
        "id": self.name or self.instrument,
        "broker": "stoxkart",
        "instrument_token": self.instrument,
        "exchange": self.exchange,
        "mode": "full",
        "last_price": values.get("last_price"),
        "last_quantity": values.get("last_quantity"),
        "average_price": values.get("average_price"),
        "volume": values.get("volume"),
        "buy_quantity": values.get("buy_quantity"),
        "sell_quantity": values.get("sell_quantity"),
        "ohlc": {
            "open": values.get("open"),
            "high": values.get("high"),
            "low": values.get("low"),
            "close": previous_close,
        },
        "change": change,
        "oi": values.get("oi"),
        "oi_day_high": None,
        "oi_day_low": None,
        "last_trade_time": values.get("last_trade_time"),
        "exchange_timestamp": values.get("exchange_timestamp"),
        "depth": {
            "buy": list(self.depth["buy"]),
            "sell": list(self.depth["sell"]),
        },
    }
    fingerprint = json.dumps(tick, sort_keys=True)
    if fingerprint == self._previous_fingerprint:
        return None
    self._previous_fingerprint = fingerprint
    tick["received_at"] = received_at
    return tick

StoxkartPacketDecoder

StoxkartPacketDecoder(states)

Reads the packets in one broadcast frame into the instruments' states.

Remembers where each instrument's state is kept.

Parameters:

Name Type Description Default
states dict

Each (segment, token) pair mapped to its StoxkartInstrumentState.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/stoxkart/StoxkartPacketDecoder/example_1_one_instrument_many_packets.py
"""Decodes one Stoxkart broadcast frame of five packets into an instrument's state.

`StoxkartPacketDecoder.decode` walks the packets of one binary frame, each starting with an 11-byte header of segment, scrip id, a second id, length and code, and applies each to the state of the instrument it names: trade (code 1), depth (2), OHLC (3), top of book (6) and previous close (32). It returns the states the frame changed, each once. Prices arrive as 32-bit floats and are rounded to four decimal places, and Stoxkart's times, counted in seconds from 1980, become epochs.

This program builds a frame with one packet of each kind for NSE token 2885 and decodes it. It needs no stand-ins.

Notice that five packets return one state, and that the trade time (UTC) and the update time (India's wall clock) come out one second apart as epochs.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartPacketDecoder/example_1_one_instrument_many_packets.py
"""

import struct

from stock_brokers.websockets.stoxkart import (
    StoxkartInstrumentState,
    StoxkartPacketDecoder,
)


class BroadcastPackets:
    """Builds Stoxkart broadcast packets, little endian, as `wss://broadcasting-v2.stoxkart.com/` sends them."""

    def packet(self, segment, scrip, code, body):
        """One packet: the 11-byte header, then its body.

        Args:
            segment (int): The broadcast segment, 1 for NSE.
            scrip (int): The instrument's token.
            code (int): The packet code.
            body (bytes): The packet's fields.

        Returns:
            bytes: The packet.
        """
        return struct.pack('<BIIBB', segment, scrip, 0, 11 + len(body), code) + body

    def trade(self, segment, scrip, last_price, volume):
        """A trade packet (code 1), with the times counted from 1980 as Stoxkart counts them.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            last_price (float): The last price in rupees.
            volume (int): The day's volume.

        Returns:
            bytes: The packet.
        """
        last_trade = 1790311528 - 315532800
        last_update = 1790311529 - 315532800 + 19800
        body = struct.pack('<fHIfiii', last_price, 25, volume, last_price - 1.5, 0, last_trade, last_update)
        return self.packet(segment, scrip, 1, body)

    def ohlc(self, segment, scrip, day_open, high, low):
        """An OHLC packet (code 3), whose close field is not used.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            day_open (float): The day's open.
            high (float): The day's high.
            low (float): The day's low.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 3, struct.pack('<ffff', day_open, 0.0, high, low))

    def previous_close(self, segment, scrip, close):
        """A previous close packet (code 32).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            close (float): The previous session's close.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 32, struct.pack('<f', close))

    def top_of_book(self, segment, scrip, total_offered, total_bid):
        """A top of book packet (code 6).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            total_offered (int): The total quantity offered.
            total_bid (int): The total quantity bid.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 6, struct.pack('<II', total_offered, total_bid))

    def depth(self, segment, scrip, best_bid):
        """A depth packet (code 2) with five levels a side.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            best_bid (float): The best bid; the best offer is 0.5 above it.

        Returns:
            bytes: The packet.
        """
        body = b''
        for level in range(5):
            body = body + struct.pack('<IIHHff', 100 + level, 200 + level, 3 + level, 4 + level, best_bid - level * 0.25, best_bid + 0.5 + level * 0.25)
        return self.packet(segment, scrip, 2, body)


class OneInstrumentManyPacketsExample:
    """Decodes one frame into one instrument's state.

    Attributes:
        state (StoxkartInstrumentState): The instrument's state.
        decoder (StoxkartPacketDecoder): The decoder being shown.
    """

    def __init__(self):
        """Builds the state and the decoder.

        Returns:
            None: This method returns nothing.
        """
        self.state = StoxkartInstrumentState('NSE:2885', 'NSE:RELIANCE')
        states = {
            (1, 2885): self.state,
        }
        self.decoder = StoxkartPacketDecoder(states)

    def run(self):
        """Decodes the frame and prints the state's values and best levels.

        Returns:
            None: This method returns nothing.
        """
        packets = BroadcastPackets()
        frame = packets.trade(1, 2885, 2950.5, 1204500)
        frame = frame + packets.depth(1, 2885, 2950.0)
        frame = frame + packets.ohlc(1, 2885, 2930.0, 2960.0, 2925.0)
        frame = frame + packets.top_of_book(1, 2885, 61000, 54000)
        frame = frame + packets.previous_close(1, 2885, 2910.0)
        touched = self.decoder.decode(frame)
        print(f'States changed: {len(touched)}, the first being {touched[0].instrument}')
        for field, value in sorted(self.state.values.items()):
            print(f'  {field}: {value}')
        print(f"  best bid: {self.state.depth['buy'][0]}")
        print(f"  best offer: {self.state.depth['sell'][0]}")


if __name__ == '__main__':
    OneInstrumentManyPacketsExample().run()

It prints:

States changed: 1, the first being NSE:2885
  average_price: 2949.0
  buy_quantity: 54000
  exchange_timestamp: 1790311529
  high: 2960.0
  last_price: 2950.5
  last_quantity: 25
  last_trade_time: 1790311528
  low: 2925.0
  oi: 0
  open: 2930.0
  previous_close: 2910.0
  sell_quantity: 61000
  volume: 1204500
  best bid: {'quantity': 100, 'price': 2950.0, 'orders': 3}
  best offer: {'quantity': 200, 'price': 2950.5, 'orders': 4}
examples/stock_brokers/websockets/stoxkart/StoxkartPacketDecoder/example_2_skipped_packets.py
"""Shows which Stoxkart packets the decoder skips: unread codes, unsubscribed instruments and a malformed header.

`StoxkartPacketDecoder` reads only the packets the stream uses. A circuit limit packet (code 33), a 52-week range (36) or any other code is skipped, a packet for an instrument that was not subscribed is skipped, and a packet whose header declares a length shorter than the header itself ends the frame, because nothing after it can be trusted. `decode` returns only the states a packet actually changed.

This program decodes three frames: one with a circuit limit packet and a trade for an unsubscribed token, one with a trade for each of two subscribed instruments, and one whose first packet declares a length of 5. It needs no stand-ins.

Notice that the first and third frames change no state, and that the second returns both instruments in the order their packets came.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartPacketDecoder/example_2_skipped_packets.py
"""

import struct

from stock_brokers.websockets.stoxkart import (
    StoxkartInstrumentState,
    StoxkartPacketDecoder,
)


class BroadcastPackets:
    """Builds Stoxkart broadcast packets, little endian, as `wss://broadcasting-v2.stoxkart.com/` sends them."""

    def packet(self, segment, scrip, code, body):
        """One packet: the 11-byte header, then its body.

        Args:
            segment (int): The broadcast segment, 1 for NSE.
            scrip (int): The instrument's token.
            code (int): The packet code.
            body (bytes): The packet's fields.

        Returns:
            bytes: The packet.
        """
        return struct.pack('<BIIBB', segment, scrip, 0, 11 + len(body), code) + body

    def trade(self, segment, scrip, last_price, volume):
        """A trade packet (code 1), with the times counted from 1980 as Stoxkart counts them.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            last_price (float): The last price in rupees.
            volume (int): The day's volume.

        Returns:
            bytes: The packet.
        """
        last_trade = 1790311528 - 315532800
        last_update = 1790311529 - 315532800 + 19800
        body = struct.pack('<fHIfiii', last_price, 25, volume, last_price - 1.5, 0, last_trade, last_update)
        return self.packet(segment, scrip, 1, body)

    def ohlc(self, segment, scrip, day_open, high, low):
        """An OHLC packet (code 3), whose close field is not used.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            day_open (float): The day's open.
            high (float): The day's high.
            low (float): The day's low.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 3, struct.pack('<ffff', day_open, 0.0, high, low))

    def previous_close(self, segment, scrip, close):
        """A previous close packet (code 32).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            close (float): The previous session's close.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 32, struct.pack('<f', close))

    def top_of_book(self, segment, scrip, total_offered, total_bid):
        """A top of book packet (code 6).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            total_offered (int): The total quantity offered.
            total_bid (int): The total quantity bid.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 6, struct.pack('<II', total_offered, total_bid))

    def depth(self, segment, scrip, best_bid):
        """A depth packet (code 2) with five levels a side.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            best_bid (float): The best bid; the best offer is 0.5 above it.

        Returns:
            bytes: The packet.
        """
        body = b''
        for level in range(5):
            body = body + struct.pack('<IIHHff', 100 + level, 200 + level, 3 + level, 4 + level, best_bid - level * 0.25, best_bid + 0.5 + level * 0.25)
        return self.packet(segment, scrip, 2, body)


class SkippedPacketsExample:
    """Decodes frames with packets the decoder skips.

    Attributes:
        decoder (StoxkartPacketDecoder): The decoder being shown.
    """

    def __init__(self):
        """Builds two subscribed states and the decoder.

        Returns:
            None: This method returns nothing.
        """
        states = {
            (1, 2885): StoxkartInstrumentState('NSE:2885', 'NSE:RELIANCE'),
            (1, 1594): StoxkartInstrumentState('NSE:1594', 'NSE:INFY'),
        }
        self.decoder = StoxkartPacketDecoder(states)

    def run(self):
        """Decodes the three frames and prints which states each changed.

        Returns:
            None: This method returns nothing.
        """
        packets = BroadcastPackets()
        circuit_limits = packets.packet(1, 2885, 33, struct.pack('<ff', 2619.0, 3201.0))
        unsubscribed = packets.trade(1, 11536, 4120.0, 5000)
        subscribed = packets.trade(1, 1594, 1500.25, 88000) + packets.trade(1, 2885, 2950.5, 1204500)
        malformed = struct.pack('<BIIBB', 1, 2885, 0, 5, 1) + packets.trade(1, 2885, 2951.0, 1204600)
        frames = [
            (
                'circuit limits and an unsubscribed trade',
                circuit_limits + unsubscribed,
            ),
            (
                'two subscribed trades',
                subscribed,
            ),
            (
                'a malformed header first',
                malformed,
            ),
        ]
        for description, frame in frames:
            touched = self.decoder.decode(frame)
            instruments = []
            for state in touched:
                instruments.append(state.instrument)
            print(f'{description}: {instruments}')


if __name__ == '__main__':
    SkippedPacketsExample().run()

It prints:

circuit limits and an unsubscribed trade: []
two subscribed trades: ['NSE:1594', 'NSE:2885']
a malformed header first: []

Methods:

Name Description
decode

Applies every packet in a frame to its instrument's state.

Source code in stock_brokers/websockets/stoxkart.py
def __init__(self, states):
    """
    Remembers where each instrument's state is kept.

    Args:
        states (dict): Each `(segment, token)` pair mapped to its StoxkartInstrumentState.

    Returns:
        None: This method returns nothing.
    """
    self._states = states

decode

decode(frame)

Applies every packet in a frame to its instrument's state.

Parameters:

Name Type Description Default
frame bytes

One binary websocket frame.

required

Returns:

Type Description
list

The states the frame changed, each once.

Example usage

From example_1_one_instrument_many_packets.py
touched = self.decoder.decode(frame)
From example_2_skipped_packets.py
touched = self.decoder.decode(frame)
Source code in stock_brokers/websockets/stoxkart.py
def decode(self, frame):
    """
    Applies every packet in a frame to its instrument's state.

    Args:
        frame (bytes): One binary websocket frame.

    Returns:
        list: The states the frame changed, each once.
    """
    touched = []
    offset = 0
    while offset + PACKET_HEADER_LENGTH <= len(frame):
        segment, scrip, _, length, code = struct.unpack_from("<BIIBB", frame, offset)
        if length < PACKET_HEADER_LENGTH:
            break
        body = frame[offset + PACKET_HEADER_LENGTH:offset + length]
        state = self._states.get((segment, scrip))
        if state is not None and self._apply(state, code, body) and state not in touched:
            touched.append(state)
        offset = offset + length
    return touched

_apply

_apply(state, code, body)

Applies one packet to an instrument's state.

Parameters:

Name Type Description Default
state StoxkartInstrumentState

The instrument's state.

required
code int

The packet code.

required
body bytes

The packet after its header.

required

Returns:

Type Description
bool

True when the packet was one the stream reads and was long enough to read.

Source code in stock_brokers/websockets/stoxkart.py
def _apply(self, state, code, body):
    """
    Applies one packet to an instrument's state.

    Args:
        state (StoxkartInstrumentState): The instrument's state.
        code (int): The packet code.
        body (bytes): The packet after its header.

    Returns:
        bool: True when the packet was one the stream reads and was long enough to read.
    """
    if code == TRADE_PACKET and len(body) >= 26:
        last_price, last_quantity, volume, average_price, open_interest, last_trade, last_update = struct.unpack_from("<fHIfiii", body)
        state.values["last_price"] = self._price(last_price)
        state.values["last_quantity"] = last_quantity
        state.values["volume"] = volume
        state.values["average_price"] = self._price(average_price)
        state.values["oi"] = open_interest
        state.values["last_trade_time"] = self._instant(last_trade, 0)
        state.values["exchange_timestamp"] = self._instant(last_update, INDIA_OFFSET_SECONDS)
        return True
    if code == DEPTH_PACKET and len(body) >= DEPTH_LEVELS * 20:
        buy_levels = []
        sell_levels = []
        for level in range(DEPTH_LEVELS):
            buy_quantity, sell_quantity, buy_orders, sell_orders, buy_price, sell_price = struct.unpack_from("<IIHHff", body, level * 20)
            buy_levels.append({
                "quantity": buy_quantity,
                "price": self._price(buy_price),
                "orders": buy_orders,
            })
            sell_levels.append({
                "quantity": sell_quantity,
                "price": self._price(sell_price),
                "orders": sell_orders,
            })
        state.depth["buy"] = buy_levels
        state.depth["sell"] = sell_levels
        return True
    if code == OHLC_PACKET and len(body) >= 16:
        day_open, _, day_high, day_low = struct.unpack_from("<ffff", body)
        state.values["open"] = self._price(day_open)
        state.values["high"] = self._price(day_high)
        state.values["low"] = self._price(day_low)
        return True
    if code == TOP_OF_BOOK_PACKET and len(body) >= 8:
        total_offered, total_bid = struct.unpack_from("<II", body)
        state.values["buy_quantity"] = total_bid
        state.values["sell_quantity"] = total_offered
        return True
    if code == PREVIOUS_CLOSE_PACKET and len(body) >= 4:
        state.values["previous_close"] = self._price(struct.unpack_from("<f", body)[0])
        return True
    return False

_price

_price(value)

Rounds a 32-bit float price, treating zero as missing.

Parameters:

Name Type Description Default
value float

The price as decoded.

required

Returns:

Type Description
float | None

The price rounded to four decimal places, or None when it is zero.

Source code in stock_brokers/websockets/stoxkart.py
def _price(self, value):
    """
    Rounds a 32-bit float price, treating zero as missing.

    Args:
        value (float): The price as decoded.

    Returns:
        float | None: The price rounded to four decimal places, or None when it is zero.
    """
    if not value:
        return None
    return round(value, PRICE_DECIMALS)

_instant

_instant(seconds, offset)

Converts Stoxkart's seconds since 1980 into epoch seconds.

Parameters:

Name Type Description Default
seconds int

The time as decoded.

required
offset int

Seconds to subtract, 19800 for a time counted on India's wall clock and 0 for UTC.

required

Returns:

Type Description
int | None

Epoch seconds, or None when the time is zero or negative.

Source code in stock_brokers/websockets/stoxkart.py
def _instant(self, seconds, offset):
    """
    Converts Stoxkart's seconds since 1980 into epoch seconds.

    Args:
        seconds (int): The time as decoded.
        offset (int): Seconds to subtract, 19800 for a time counted on India's wall clock and 0 for UTC.

    Returns:
        int | None: Epoch seconds, or None when the time is zero or negative.
    """
    if seconds <= 0:
        return None
    return seconds + EPOCH_1980_OFFSET_SECONDS - offset

StoxkartQuoteStream

StoxkartQuoteStream(instruments, names, on_ticks, logger)

One connection to Stoxkart's broadcast websocket, reconnected until stopped.

Attributes:

Name Type Description
logger Logger

Where connections, progress and failures are reported.

stop Event

Set when streaming should end.

Builds a state per instrument and the request builder.

Parameters:

Name Type Description Default
instruments list

The EXCHANGE:TOKEN strings to subscribe.

required
names dict

Each instrument mapped to its name, or to an empty string.

required
on_ticks callable

Called with each frame's list of changed ticks.

required
logger Logger

Where connections, progress and failures are reported.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/stoxkart/StoxkartQuoteStream/example_1_streaming_one_frame.py
"""Runs a Stoxkart broadcast quote stream through its requests and one frame of packets, and prints the tick.

`StoxkartQuoteStream` reads a synchronous websocket-client connection in a loop of its own rather than subclassing `BrokerWebsocket`. On connecting to `wss://broadcasting-v2.stoxkart.com/` it sends an 83-byte connection header (code 10), then for each instrument a 129-byte trade subscription (code 12) and depth subscription (code 23). Each binary frame is a run of packets for one or more instruments, decoded by `StoxkartPacketDecoder`, and one tick is handed on per instrument per frame. The feed takes no login.

This program subscribes `NSE:2885` and delivers one frame holding a trade, an OHLC, a top of book, a previous close and a depth packet. No real socket is opened: a stand-in `websocket` package's `create_connection` returns a scripted connection whose `recv_data` returns the frame, and which sets the stream's `stop` event once its frames have run out, so `run` returns.

Notice that the five packets make one tick, that the change is computed against the previous close, and that the trade and update times, counted from 1980 by Stoxkart, come out as ordinary epochs.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartQuoteStream/example_1_streaming_one_frame.py
"""

import logging
import struct
import sys

from stock_brokers.websockets.stoxkart import (
    StoxkartQuoteStream,
)


class ReceiveTimeout(Exception):
    """A stand-in for `websocket.WebSocketTimeoutException`: nothing arrived within the receive timeout."""


class FrameOpcodes:
    """The websocket frame opcodes the Stoxkart streams compare against, as `websocket.ABNF` holds them.

    Attributes:
        OPCODE_TEXT (int): A text frame.
        OPCODE_BINARY (int): A binary frame.
        OPCODE_CLOSE (int): A close frame.
    """

    OPCODE_TEXT = 1
    OPCODE_BINARY = 2
    OPCODE_CLOSE = 8


class ScriptedSynchronousConnection:
    """A stand-in for a synchronous `websocket.WebSocket` that returns scripted frames from `recv_data`.

    Attributes:
        frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
        when_empty (callable | None): Called once the last frame has been read, to stop the stream.
    """

    def __init__(self, frames, when_empty):
        """Keeps the frames.

        Args:
            frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
            when_empty (callable | None): Called once the last frame has been read, to stop the stream.

        Returns:
            None: This method returns nothing.
        """
        self.frames = frames
        self.when_empty = when_empty

    def send_binary(self, data):
        """Prints a binary request the stream sends.

        Args:
            data (bytes): The request.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent binary request with code {data[0]} and {len(data)} bytes')

    def send(self, data):
        """Prints a text message the stream sends.

        Args:
            data (str): The message.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def recv_data(self):
        """Returns the next scripted frame, or times out once they have run out.

        Returns:
            tuple: The opcode and the frame's bytes.

        Raises:
            ReceiveTimeout: When no scripted frame is left.
        """
        if not self.frames:
            raise ReceiveTimeout()
        frame = self.frames.pop(0)
        if not self.frames and self.when_empty is not None:
            self.when_empty()
        return frame

    def close(self):
        """Accepts the stream's request to close.

        Returns:
            None: This method returns nothing.
        """


class ScriptedSynchronousModule:
    """A stand-in for the `websocket` package that hands out scripted synchronous connections in turn.

    Attributes:
        plans (list): Each connection's frames, in the order the stream connects.
        when_finished (callable | None): Called after the last connection's last frame, to stop the stream.
        ABNF (FrameOpcodes): The frame opcodes.
        WebSocketTimeoutException (type): The error `recv_data` raises when nothing arrived.
    """

    def __init__(self, plans):
        """Keeps the plans.

        Args:
            plans (list): Each connection's frames, in the order the stream connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None
        self.ABNF = FrameOpcodes()
        self.WebSocketTimeoutException = ReceiveTimeout

    def create_connection(self, url, timeout):
        """Opens the next scripted connection.

        Args:
            url (str): The socket URL.
            timeout (int): The receive timeout in seconds.

        Returns:
            ScriptedSynchronousConnection: The connection.
        """
        print(f'Connecting to {url}')
        frames = self.plans.pop(0)
        when_empty = None
        if not self.plans:
            when_empty = self.when_finished
        return ScriptedSynchronousConnection(frames, when_empty)


class BroadcastPackets:
    """Builds Stoxkart broadcast packets, little endian, as `wss://broadcasting-v2.stoxkart.com/` sends them."""

    def packet(self, segment, scrip, code, body):
        """One packet: the 11-byte header, then its body.

        Args:
            segment (int): The broadcast segment, 1 for NSE.
            scrip (int): The instrument's token.
            code (int): The packet code.
            body (bytes): The packet's fields.

        Returns:
            bytes: The packet.
        """
        return struct.pack('<BIIBB', segment, scrip, 0, 11 + len(body), code) + body

    def trade(self, segment, scrip, last_price, volume):
        """A trade packet (code 1), with the times counted from 1980 as Stoxkart counts them.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            last_price (float): The last price in rupees.
            volume (int): The day's volume.

        Returns:
            bytes: The packet.
        """
        last_trade = 1790311528 - 315532800
        last_update = 1790311529 - 315532800 + 19800
        body = struct.pack('<fHIfiii', last_price, 25, volume, last_price - 1.5, 0, last_trade, last_update)
        return self.packet(segment, scrip, 1, body)

    def ohlc(self, segment, scrip, day_open, high, low):
        """An OHLC packet (code 3), whose close field is not used.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            day_open (float): The day's open.
            high (float): The day's high.
            low (float): The day's low.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 3, struct.pack('<ffff', day_open, 0.0, high, low))

    def previous_close(self, segment, scrip, close):
        """A previous close packet (code 32).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            close (float): The previous session's close.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 32, struct.pack('<f', close))

    def top_of_book(self, segment, scrip, total_offered, total_bid):
        """A top of book packet (code 6).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            total_offered (int): The total quantity offered.
            total_bid (int): The total quantity bid.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 6, struct.pack('<II', total_offered, total_bid))

    def depth(self, segment, scrip, best_bid):
        """A depth packet (code 2) with five levels a side.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            best_bid (float): The best bid; the best offer is 0.5 above it.

        Returns:
            bytes: The packet.
        """
        body = b''
        for level in range(5):
            body = body + struct.pack('<IIHHff', 100 + level, 200 + level, 3 + level, 4 + level, best_bid - level * 0.25, best_bid + 0.5 + level * 0.25)
        return self.packet(segment, scrip, 2, body)


class StreamingOneFrameExample:
    """Runs a Stoxkart quote stream against one scripted frame.

    Attributes:
        stream (StoxkartQuoteStream): The stream being shown.
    """

    def __init__(self):
        """Builds the frame, the stand-in package and the stream.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        packets = BroadcastPackets()
        frame = packets.trade(1, 2885, 2950.5, 1204500)
        frame = frame + packets.ohlc(1, 2885, 2930.0, 2960.0, 2925.0)
        frame = frame + packets.top_of_book(1, 2885, 61000, 54000)
        frame = frame + packets.previous_close(1, 2885, 2910.0)
        frame = frame + packets.depth(1, 2885, 2950.0)
        plans = [
            [
                (
                    2,
                    frame,
                ),
            ],
        ]
        websocket_module = ScriptedSynchronousModule(plans)
        sys.modules['websocket'] = websocket_module
        instruments = [
            'NSE:2885',
        ]
        names = {
            'NSE:2885': 'NSE:RELIANCE',
        }
        self.stream = StoxkartQuoteStream(instruments, names, self.print_ticks, logging.getLogger('stoxkart.quotes'))
        websocket_module.when_finished = self.stream.stop.set

    def print_ticks(self, ticks):
        """Prints the main fields of each tick the stream handed on.

        Args:
            ticks (list): The changed ticks from one frame.

        Returns:
            None: This method returns nothing.
        """
        for tick in ticks:
            print(f"Tick {tick['id']} ({tick['instrument_token']}): last_price={tick['last_price']} volume={tick['volume']} change={tick['change']}")
            print(f"  ohlc={tick['ohlc']} buy_quantity={tick['buy_quantity']} sell_quantity={tick['sell_quantity']}")
            print(f"  last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
            if tick['depth']['buy']:
                print(f"  best bid={tick['depth']['buy'][0]} best offer={tick['depth']['sell'][0]}")

    def run(self):
        """Runs the stream until the scripted connection has delivered everything.

        Returns:
            None: This method returns nothing.
        """
        exit_code = self.stream.run()
        print(f'run returned {exit_code}')


if __name__ == '__main__':
    StreamingOneFrameExample().run()

It prints:

Connecting to wss://broadcasting-v2.stoxkart.com/
Sent binary request with code 10 and 83 bytes
Sent binary request with code 12 and 129 bytes
Sent binary request with code 23 and 129 bytes
INFO Connected to wss://broadcasting-v2.stoxkart.com/ and subscribed 1 instrument(s).
Tick NSE:RELIANCE (NSE:2885): last_price=2950.5 volume=1204500 change=1.391753
  ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0} buy_quantity=54000 sell_quantity=61000
  last_trade_time=1790311528 exchange_timestamp=1790311529
  best bid={'quantity': 100, 'price': 2950.0, 'orders': 3} best offer={'quantity': 200, 'price': 2950.5, 'orders': 4}
INFO Stopped
run returned 0
examples/stock_brokers/websockets/stoxkart/StoxkartQuoteStream/example_2_reconnect_and_unchanged_ticks.py
"""Shows a Stoxkart quote stream reconnecting when asked to, and handing on a tick only when it changed.

Stoxkart can send the text `reconnect`, and `StoxkartQuoteStream` then closes the connection and opens a new one after its backoff. A stream also hands on an instrument's tick only when it differs from the previous one apart from the time it was received, so a frame that repeats the same values produces nothing.

This program's first connection delivers a trade frame and then `reconnect`. The second delivers the same trade frame again, which is unchanged, and then a trade with a new price. No real socket is opened: a stand-in `websocket` package plays the connections, and the stream's `stop` event is replaced by a stand-in whose waits return at once and are printed, so the one second backoff does not slow the program.

Notice the warning, the skipped one second wait, that the repeated frame hands on no tick, and that the instrument without a name is keyed by `NSE:1594`.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartQuoteStream/example_2_reconnect_and_unchanged_ticks.py
"""

import logging
import struct
import sys

from stock_brokers.websockets.stoxkart import (
    StoxkartQuoteStream,
)


class ReceiveTimeout(Exception):
    """A stand-in for `websocket.WebSocketTimeoutException`: nothing arrived within the receive timeout."""


class FrameOpcodes:
    """The websocket frame opcodes the Stoxkart streams compare against, as `websocket.ABNF` holds them.

    Attributes:
        OPCODE_TEXT (int): A text frame.
        OPCODE_BINARY (int): A binary frame.
        OPCODE_CLOSE (int): A close frame.
    """

    OPCODE_TEXT = 1
    OPCODE_BINARY = 2
    OPCODE_CLOSE = 8


class ScriptedSynchronousConnection:
    """A stand-in for a synchronous `websocket.WebSocket` that returns scripted frames from `recv_data`.

    Attributes:
        frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
        when_empty (callable | None): Called once the last frame has been read, to stop the stream.
    """

    def __init__(self, frames, when_empty):
        """Keeps the frames.

        Args:
            frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
            when_empty (callable | None): Called once the last frame has been read, to stop the stream.

        Returns:
            None: This method returns nothing.
        """
        self.frames = frames
        self.when_empty = when_empty

    def send_binary(self, data):
        """Prints a binary request the stream sends.

        Args:
            data (bytes): The request.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent binary request with code {data[0]} and {len(data)} bytes')

    def send(self, data):
        """Prints a text message the stream sends.

        Args:
            data (str): The message.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def recv_data(self):
        """Returns the next scripted frame, or times out once they have run out.

        Returns:
            tuple: The opcode and the frame's bytes.

        Raises:
            ReceiveTimeout: When no scripted frame is left.
        """
        if not self.frames:
            raise ReceiveTimeout()
        frame = self.frames.pop(0)
        if not self.frames and self.when_empty is not None:
            self.when_empty()
        return frame

    def close(self):
        """Accepts the stream's request to close.

        Returns:
            None: This method returns nothing.
        """


class ScriptedSynchronousModule:
    """A stand-in for the `websocket` package that hands out scripted synchronous connections in turn.

    Attributes:
        plans (list): Each connection's frames, in the order the stream connects.
        when_finished (callable | None): Called after the last connection's last frame, to stop the stream.
        ABNF (FrameOpcodes): The frame opcodes.
        WebSocketTimeoutException (type): The error `recv_data` raises when nothing arrived.
    """

    def __init__(self, plans):
        """Keeps the plans.

        Args:
            plans (list): Each connection's frames, in the order the stream connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None
        self.ABNF = FrameOpcodes()
        self.WebSocketTimeoutException = ReceiveTimeout

    def create_connection(self, url, timeout):
        """Opens the next scripted connection.

        Args:
            url (str): The socket URL.
            timeout (int): The receive timeout in seconds.

        Returns:
            ScriptedSynchronousConnection: The connection.
        """
        print(f'Connecting to {url}')
        frames = self.plans.pop(0)
        when_empty = None
        if not self.plans:
            when_empty = self.when_finished
        return ScriptedSynchronousConnection(frames, when_empty)


class ImmediateStopEvent:
    """A stand-in for the stream's `threading.Event` whose waits return at once and are printed.

    Attributes:
        stopped (bool): Whether the stream has been told to stop.
    """

    def __init__(self):
        """Starts not stopped.

        Returns:
            None: This method returns nothing.
        """
        self.stopped = False

    def set(self):
        """Tells the stream to stop.

        Returns:
            None: This method returns nothing.
        """
        self.stopped = True

    def is_set(self):
        """Says whether the stream has been told to stop.

        Returns:
            bool: True once stopped.
        """
        return self.stopped

    def wait(self, timeout):
        """Prints the wait the stream asked for and returns at once.

        Args:
            timeout (float): How many seconds the stream would wait.

        Returns:
            bool: Whether the stream has been told to stop.
        """
        print(f'The stream waits {timeout} second(s) here; this program skips the wait.')
        return self.stopped


class BroadcastPackets:
    """Builds Stoxkart broadcast packets, little endian, as `wss://broadcasting-v2.stoxkart.com/` sends them."""

    def packet(self, segment, scrip, code, body):
        """One packet: the 11-byte header, then its body.

        Args:
            segment (int): The broadcast segment, 1 for NSE.
            scrip (int): The instrument's token.
            code (int): The packet code.
            body (bytes): The packet's fields.

        Returns:
            bytes: The packet.
        """
        return struct.pack('<BIIBB', segment, scrip, 0, 11 + len(body), code) + body

    def trade(self, segment, scrip, last_price, volume):
        """A trade packet (code 1), with the times counted from 1980 as Stoxkart counts them.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            last_price (float): The last price in rupees.
            volume (int): The day's volume.

        Returns:
            bytes: The packet.
        """
        last_trade = 1790311528 - 315532800
        last_update = 1790311529 - 315532800 + 19800
        body = struct.pack('<fHIfiii', last_price, 25, volume, last_price - 1.5, 0, last_trade, last_update)
        return self.packet(segment, scrip, 1, body)

    def ohlc(self, segment, scrip, day_open, high, low):
        """An OHLC packet (code 3), whose close field is not used.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            day_open (float): The day's open.
            high (float): The day's high.
            low (float): The day's low.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 3, struct.pack('<ffff', day_open, 0.0, high, low))

    def previous_close(self, segment, scrip, close):
        """A previous close packet (code 32).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            close (float): The previous session's close.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 32, struct.pack('<f', close))

    def top_of_book(self, segment, scrip, total_offered, total_bid):
        """A top of book packet (code 6).

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            total_offered (int): The total quantity offered.
            total_bid (int): The total quantity bid.

        Returns:
            bytes: The packet.
        """
        return self.packet(segment, scrip, 6, struct.pack('<II', total_offered, total_bid))

    def depth(self, segment, scrip, best_bid):
        """A depth packet (code 2) with five levels a side.

        Args:
            segment (int): The broadcast segment.
            scrip (int): The instrument's token.
            best_bid (float): The best bid; the best offer is 0.5 above it.

        Returns:
            bytes: The packet.
        """
        body = b''
        for level in range(5):
            body = body + struct.pack('<IIHHff', 100 + level, 200 + level, 3 + level, 4 + level, best_bid - level * 0.25, best_bid + 0.5 + level * 0.25)
        return self.packet(segment, scrip, 2, body)


class ReconnectAndUnchangedTicksExample:
    """Runs a Stoxkart quote stream through a reconnect request and a repeated frame.

    Attributes:
        stream (StoxkartQuoteStream): The stream being shown.
    """

    def __init__(self):
        """Builds the frames, the stand-ins and the stream.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        packets = BroadcastPackets()
        first_trade = packets.trade(1, 1594, 1500.25, 88000)
        second_trade = packets.trade(1, 1594, 1500.75, 88100)
        plans = [
            [
                (
                    2,
                    first_trade,
                ),
                (
                    1,
                    b'reconnect',
                ),
            ],
            [
                (
                    2,
                    first_trade,
                ),
                (
                    2,
                    second_trade,
                ),
            ],
        ]
        websocket_module = ScriptedSynchronousModule(plans)
        sys.modules['websocket'] = websocket_module
        instruments = [
            'NSE:1594',
        ]
        self.stream = StoxkartQuoteStream(instruments, {}, self.print_ticks, logging.getLogger('stoxkart.quotes'))
        self.stream.stop = ImmediateStopEvent()
        websocket_module.when_finished = self.stream.stop.set

    def print_ticks(self, ticks):
        """Prints the main fields of each tick the stream handed on.

        Args:
            ticks (list): The changed ticks from one frame.

        Returns:
            None: This method returns nothing.
        """
        for tick in ticks:
            print(f"Tick {tick['id']} ({tick['instrument_token']}): last_price={tick['last_price']} volume={tick['volume']} change={tick['change']}")
            print(f"  ohlc={tick['ohlc']} buy_quantity={tick['buy_quantity']} sell_quantity={tick['sell_quantity']}")
            print(f"  last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
            if tick['depth']['buy']:
                print(f"  best bid={tick['depth']['buy'][0]} best offer={tick['depth']['sell'][0]}")

    def run(self):
        """Runs the stream until the scripted connections have delivered everything.

        Returns:
            None: This method returns nothing.
        """
        exit_code = self.stream.run()
        print(f'run returned {exit_code}')


if __name__ == '__main__':
    ReconnectAndUnchangedTicksExample().run()

It prints:

Connecting to wss://broadcasting-v2.stoxkart.com/
Sent binary request with code 10 and 83 bytes
Sent binary request with code 12 and 129 bytes
Sent binary request with code 23 and 129 bytes
INFO Connected to wss://broadcasting-v2.stoxkart.com/ and subscribed 1 instrument(s).
Tick NSE:1594 (NSE:1594): last_price=1500.25 volume=88000 change=None
  ohlc={'open': None, 'high': None, 'low': None, 'close': None} buy_quantity=None sell_quantity=None
  last_trade_time=1790311528 exchange_timestamp=1790311529
WARNING Stoxkart asked for a reconnect.
The stream waits 1 second(s) here; this program skips the wait.
Connecting to wss://broadcasting-v2.stoxkart.com/
Sent binary request with code 10 and 83 bytes
Sent binary request with code 12 and 129 bytes
Sent binary request with code 23 and 129 bytes
INFO Connected to wss://broadcasting-v2.stoxkart.com/ and subscribed 1 instrument(s).
Tick NSE:1594 (NSE:1594): last_price=1500.75 volume=88100 change=None
  ohlc={'open': None, 'high': None, 'low': None, 'close': None} buy_quantity=None sell_quantity=None
  last_trade_time=1790311528 exchange_timestamp=1790311529
INFO Stopped
run returned 0

Methods:

Name Description
run

Connects, subscribes and hands on ticks, reconnecting with a backoff, until stopped.

Source code in stock_brokers/websockets/stoxkart.py
def __init__(self, instruments, names, on_ticks, logger):
    """
    Builds a state per instrument and the request builder.

    Args:
        instruments (list): The `EXCHANGE:TOKEN` strings to subscribe.
        names (dict): Each instrument mapped to its name, or to an empty string.
        on_ticks (callable): Called with each frame's list of changed ticks.
        logger (logging.Logger): Where connections, progress and failures are reported.

    Returns:
        None: This method returns nothing.
    """
    self.logger = logger
    self.stop = threading.Event()
    self._on_ticks = on_ticks
    self._requests = StoxkartBroadcastRequests()
    self._subscriptions = []
    states = {}
    for instrument in instruments:
        exchange, token = instrument.split(":")
        segment = SEGMENTS[exchange]
        states[(segment, int(token))] = StoxkartInstrumentState(instrument, names.get(instrument, ""))
        self._subscriptions.append((segment, token))
    self._decoder = StoxkartPacketDecoder(states)
    self._written_since_report = 0
    self._last_report_at = time.monotonic()

run

run()

Connects, subscribes and hands on ticks, reconnecting with a backoff, until stopped.

Returns:

Type Description
int

0, once stopped.

Example usage

From example_1_streaming_one_frame.py
exit_code = self.stream.run()
From example_2_reconnect_and_unchanged_ticks.py
exit_code = self.stream.run()
Source code in stock_brokers/websockets/stoxkart.py
def run(self):
    """
    Connects, subscribes and hands on ticks, reconnecting with a backoff, until stopped.

    Returns:
        int: 0, once stopped.
    """
    backoff = MINIMUM_BACKOFF_SECONDS
    while not self.stop.is_set():
        try:
            connected_for = self._stream_once()
        except Exception as error:
            self.logger.error(f"Broadcast socket failed, reconnecting in {backoff} seconds: {type(error).__name__}: {error}")
            connected_for = 0
        if self.stop.is_set():
            break
        if connected_for > MAXIMUM_BACKOFF_SECONDS:
            backoff = MINIMUM_BACKOFF_SECONDS
        self.stop.wait(backoff)
        backoff = min(backoff * 2, MAXIMUM_BACKOFF_SECONDS)
    self.logger.info("Stopped")
    return 0

_stream_once

_stream_once()

Opens one connection and reads it until it fails, goes silent or is stopped.

Returns:

Type Description
float

How many seconds the connection stayed up.

Raises:

Type Description
WebSocketException

The connection could not be opened or broke.

Source code in stock_brokers/websockets/stoxkart.py
def _stream_once(self):
    """
    Opens one connection and reads it until it fails, goes silent or is stopped.

    Returns:
        float: How many seconds the connection stayed up.

    Raises:
        websocket.WebSocketException: The connection could not be opened or broke.
    """
    import websocket

    socket = websocket.create_connection(FEED_URL, timeout=RECEIVE_TIMEOUT_SECONDS)
    opened_at = time.monotonic()
    try:
        socket.send_binary(self._requests.connection_header())
        for segment, token in self._subscriptions:
            socket.send_binary(self._requests.subscription(TRADE_SUBSCRIBE_CODE, segment, token))
            socket.send_binary(self._requests.subscription(DEPTH_SUBSCRIBE_CODE, segment, token))
        self.logger.info(f"Connected to {FEED_URL} and subscribed {len(self._subscriptions)} instrument(s).")
        self._read_until_closed(socket)
    finally:
        socket.close()
    return time.monotonic() - opened_at

_read_until_closed

_read_until_closed(socket)

Reads frames, pings the server and hands on ticks until the socket should be reopened or streaming stops.

Parameters:

Name Type Description Default
socket WebSocket

The open connection.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/stoxkart.py
def _read_until_closed(self, socket):
    """
    Reads frames, pings the server and hands on ticks until the socket should be reopened or streaming stops.

    Args:
        socket (websocket.WebSocket): The open connection.

    Returns:
        None: This method returns nothing.
    """
    import websocket

    last_message_at = time.monotonic()
    last_ping_at = time.monotonic()
    while not self.stop.is_set():
        now = time.monotonic()
        if now - last_ping_at >= PING_INTERVAL_SECONDS:
            socket.send("ping")
            last_ping_at = now
        if now - last_message_at > SILENCE_LIMIT_SECONDS:
            self.logger.warning(f"No message for {SILENCE_LIMIT_SECONDS} seconds, reconnecting.")
            return
        try:
            opcode, frame = socket.recv_data()
        except websocket.WebSocketTimeoutException:
            continue
        last_message_at = time.monotonic()
        if opcode == websocket.ABNF.OPCODE_TEXT:
            if frame == b"reconnect":
                self.logger.warning("Stoxkart asked for a reconnect.")
                return
            continue
        if opcode == websocket.ABNF.OPCODE_CLOSE:
            self.logger.warning("Stoxkart closed the broadcast socket.")
            return
        if opcode == websocket.ABNF.OPCODE_BINARY:
            self._hand_on(self._decoder.decode(frame))

_hand_on

_hand_on(states)

Hands on a tick for every instrument a frame changed.

Parameters:

Name Type Description Default
states list

The instrument states the frame changed.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/stoxkart.py
def _hand_on(self, states):
    """
    Hands on a tick for every instrument a frame changed.

    Args:
        states (list): The instrument states the frame changed.

    Returns:
        None: This method returns nothing.
    """
    received_at = time.time()
    ticks = []
    for state in states:
        tick = state.tick(received_at)
        if tick is not None:
            ticks.append(tick)
    if ticks:
        self._on_ticks(ticks)
    self._report(len(ticks))

_report

_report(written)

Adds a frame's count to the running total and logs the total once a minute.

Parameters:

Name Type Description Default
written int

How many ticks the frame handed on.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/stoxkart.py
def _report(self, written):
    """
    Adds a frame's count to the running total and logs the total once a minute.

    Args:
        written (int): How many ticks the frame handed on.

    Returns:
        None: This method returns nothing.
    """
    self._written_since_report = self._written_since_report + written
    if time.monotonic() - self._last_report_at < REPORT_INTERVAL_SECONDS:
        return
    self.logger.info(f"Wrote {self._written_since_report} tick(s) in the last minute to stoxkart:quotes:live")
    self._written_since_report = 0
    self._last_report_at = time.monotonic()

StoxkartSessionRefused

Bases: Exception

Stoxkart refused the order socket's authentication because the session is dead.

Example programs

examples/stock_brokers/websockets/stoxkart/StoxkartSessionRefused/example_1_raising_and_catching.py
"""Raises and catches `StoxkartSessionRefused`, the error that marks a dead Stoxkart session.

`StoxkartSessionRefused` is raised when Stoxkart refuses the order socket's authentication because the session is dead: HTTP 401 or a body whose `code` is `AuthorizationError`. The order socket catches it in its loop and logs in again instead of backing off. Its message is Stoxkart's own message, or the HTTP status when Stoxkart gave none.

This program raises it for both kinds of answer, the way the order socket's authentication step decides, and catches it. It needs no stand-ins.

Notice that an answer that is neither is not a refusal.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartSessionRefused/example_1_raising_and_catching.py
"""


from stock_brokers.websockets.stoxkart import (
    StoxkartSessionRefused,
)


class RaisingAndCatchingExample:
    """Checks three authentication answers and catches the refusals.

    Attributes:
        answers (list): Pairs of an HTTP status and a JSON body.
    """

    def __init__(self):
        """Lists the answers to check.

        Returns:
            None: This method returns nothing.
        """
        self.answers = [
            (
                200,
                {
                    'code': 'AuthorizationError',
                    'message': 'Token expired',
                },
            ),
            (
                401,
                {},
            ),
            (
                200,
                {
                    'data': {
                        'RequestId': 'request-1',
                    },
                },
            ),
        ]

    def check(self, status_code, body):
        """Raises a refusal when the answer says the session is dead.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            str: The RequestId when the answer is not a refusal.

        Raises:
            StoxkartSessionRefused: When the answer is HTTP 401 or an `AuthorizationError`.
        """
        if status_code == 401 or body.get('code') == 'AuthorizationError':
            raise StoxkartSessionRefused(body.get('message') or f'HTTP {status_code}')
        return body['data']['RequestId']

    def run(self):
        """Checks each answer and prints the outcome.

        Returns:
            None: This method returns nothing.
        """
        for status_code, body in self.answers:
            try:
                request_id = self.check(status_code, body)
                print(f'HTTP {status_code}: accepted with {request_id}')
            except StoxkartSessionRefused as refusal:
                print(f'HTTP {status_code}: StoxkartSessionRefused: {refusal}')


if __name__ == '__main__':
    RaisingAndCatchingExample().run()

It prints:

HTTP 200: StoxkartSessionRefused: Token expired
HTTP 401: StoxkartSessionRefused: HTTP 401
HTTP 200: accepted with request-1
examples/stock_brokers/websockets/stoxkart/StoxkartSessionRefused/example_2_handled_by_the_order_socket.py
"""Shows `StoxkartSessionRefused` raised and handled inside Stoxkart's order socket, which logs in again instead of backing off.

Inside `StoxkartOrderSocket.run`, a `StoxkartSessionRefused` from the authentication step is the one failure that leads to a login rather than a backoff: the socket logs the refusal as a warning with Stoxkart's message, calls `StoxkartSession.log_in_again`, and authenticates again at once. Any other failure waits and doubles the backoff.

This program refuses the first authentication and lets the second succeed; the connection then delivers one message and stops. No real socket or HTTP request is made: stand-in `requests` and `websocket` packages answer, and a stand-in `stock_brokers.api.stoxkart` module counts each `StoxkartAPI` construction as one login.

Notice the warning carrying `Invalid access token`, which is the refusal's message, and that no wait comes between the refusal and the second authentication.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartSessionRefused/example_2_handled_by_the_order_socket.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.stoxkart import (
    StoxkartOrderSocket,
    StoxkartSession,
)


class LoginRecord:
    """The Stoxkart login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInStoxkartAPI:
    """A stand-in for `StoxkartAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'stoxkart-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'ucc_code': 'SK12345',
            'api_key': 'stoxkart-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class AuthenticateResponse:
    """A stand-in for the `requests` response of Stoxkart's websocket authentication.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body


class StandInRequests:
    """A stand-in for the `requests` package that answers websocket authentications in turn.

    Attributes:
        refusals (int): How many of the first requests to refuse with `AuthorizationError`.
        request_count (int): How many requests have been answered.
    """

    def __init__(self, refusals):
        """Keeps how many requests to refuse.

        Args:
            refusals (int): How many of the first requests to refuse with `AuthorizationError`.

        Returns:
            None: This method returns nothing.
        """
        self.refusals = refusals
        self.request_count = 0

    def post(self, url, headers, timeout):
        """Answers one authentication request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            timeout (int): The request timeout in seconds.

        Returns:
            AuthenticateResponse: The answer.
        """
        self.request_count = self.request_count + 1
        print(f"Authenticating with {headers['x-access-token']}")
        if self.request_count <= self.refusals:
            body = {
                'code': 'AuthorizationError',
                'message': 'Invalid access token',
            }
            return AuthenticateResponse(401, body)
        body = {
            'data': {
                'RequestId': f'request-{self.request_count}',
            },
        }
        return AuthenticateResponse(200, body)


class ReceiveTimeout(Exception):
    """A stand-in for `websocket.WebSocketTimeoutException`: nothing arrived within the receive timeout."""


class FrameOpcodes:
    """The websocket frame opcodes the Stoxkart streams compare against, as `websocket.ABNF` holds them.

    Attributes:
        OPCODE_TEXT (int): A text frame.
        OPCODE_BINARY (int): A binary frame.
        OPCODE_CLOSE (int): A close frame.
    """

    OPCODE_TEXT = 1
    OPCODE_BINARY = 2
    OPCODE_CLOSE = 8


class ScriptedSynchronousConnection:
    """A stand-in for a synchronous `websocket.WebSocket` that returns scripted frames from `recv_data`.

    Attributes:
        frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
        when_empty (callable | None): Called once the last frame has been read, to stop the stream.
    """

    def __init__(self, frames, when_empty):
        """Keeps the frames.

        Args:
            frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
            when_empty (callable | None): Called once the last frame has been read, to stop the stream.

        Returns:
            None: This method returns nothing.
        """
        self.frames = frames
        self.when_empty = when_empty

    def send_binary(self, data):
        """Prints a binary request the stream sends.

        Args:
            data (bytes): The request.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent binary request with code {data[0]} and {len(data)} bytes')

    def send(self, data):
        """Prints a text message the stream sends.

        Args:
            data (str): The message.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def recv_data(self):
        """Returns the next scripted frame, or times out once they have run out.

        Returns:
            tuple: The opcode and the frame's bytes.

        Raises:
            ReceiveTimeout: When no scripted frame is left.
        """
        if not self.frames:
            raise ReceiveTimeout()
        frame = self.frames.pop(0)
        if not self.frames and self.when_empty is not None:
            self.when_empty()
        return frame

    def close(self):
        """Accepts the stream's request to close.

        Returns:
            None: This method returns nothing.
        """


class ScriptedSynchronousModule:
    """A stand-in for the `websocket` package that hands out scripted synchronous connections in turn.

    Attributes:
        plans (list): Each connection's frames, in the order the stream connects.
        when_finished (callable | None): Called after the last connection's last frame, to stop the stream.
        ABNF (FrameOpcodes): The frame opcodes.
        WebSocketTimeoutException (type): The error `recv_data` raises when nothing arrived.
    """

    def __init__(self, plans):
        """Keeps the plans.

        Args:
            plans (list): Each connection's frames, in the order the stream connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None
        self.ABNF = FrameOpcodes()
        self.WebSocketTimeoutException = ReceiveTimeout

    def create_connection(self, url, timeout):
        """Opens the next scripted connection.

        Args:
            url (str): The socket URL.
            timeout (int): The receive timeout in seconds.

        Returns:
            ScriptedSynchronousConnection: The connection.
        """
        print(f'Connecting to {url}')
        frames = self.plans.pop(0)
        when_empty = None
        if not self.plans:
            when_empty = self.when_finished
        return ScriptedSynchronousConnection(frames, when_empty)


class HandledByTheOrderSocketExample:
    """Runs the Stoxkart order socket through one refused authentication.

    Attributes:
        socket (StoxkartOrderSocket): The socket that handles the refusal.
    """

    def __init__(self):
        """Builds the frames, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.stoxkart')
        api_module.StoxkartAPI = StandInStoxkartAPI
        sys.modules['stock_brokers.api.stoxkart'] = api_module
        sys.modules['requests'] = StandInRequests(1)
        update = {
            'order_id': 'SK2609250002',
            'status': 'OPEN',
        }
        plans = [
            [
                (
                    1,
                    json.dumps(update).encode(),
                ),
            ],
        ]
        websocket_module = ScriptedSynchronousModule(plans)
        sys.modules['websocket'] = websocket_module
        self.socket = StoxkartOrderSocket(StoxkartSession(), self.print_updates, logging.getLogger('stoxkart.orders'))
        websocket_module.when_finished = self.socket.stop.set

    def print_updates(self, messages, received_at):
        """Prints each message the order socket handed on.

        Args:
            messages (list): The frame's messages, exactly as Stoxkart sent them.
            received_at (datetime.datetime): When the frame was read, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        print(f'Callback received {len(messages)} message(s)')

    def run(self):
        """Runs the socket until the scripted connection has delivered everything.

        Returns:
            None: This method returns nothing.
        """
        exit_code = self.socket.run()
        print(f'run returned {exit_code}')
        print(f'Logins made: {LoginRecord.login_count}')


if __name__ == '__main__':
    HandledByTheOrderSocketExample().run()

It prints:

Stand-in login number 1
Authenticating with stoxkart-token-1
WARNING Stoxkart session expired, logging in again: Invalid access token
Stand-in login number 2
Authenticating with stoxkart-token-2
Connecting to wss://openapi-v2.stoxkart.com/websocket/v2/connect?x-client-id=SK12345&x-platform=api&RequestId=request-2
INFO Connected to wss://openapi-v2.stoxkart.com/websocket/v2/connect.
Sent: {"type": "heartbeat"}
INFO Order socket message: {"order_id": "SK2609250002", "status": "OPEN"}
Callback received 1 message(s)
INFO Stopped
run returned 0
Logins made: 2

StoxkartSession

StoxkartSession()

The Stoxkart API login the order socket authenticates with.

Constructs StoxkartAPI, which logs in when the stored session is dead.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever StoxkartAPI raises when it cannot log in.

Example programs

examples/stock_brokers/websockets/stoxkart/StoxkartSession/example_1_authentication_headers.py
"""Reads the headers and client code Stoxkart's order socket authenticates with.

`StoxkartSession` holds the `StoxkartAPI` whose login authenticates the order socket. `authentication_headers` builds the headers the socket's authentication request carries, with the access token in force now, and `client_id` gives the client code the socket connects as. Both read the account's settings, which the real class loads from MongoDB.

Constructing the real `StoxkartAPI` logs in to Stoxkart, so the program registers a stand-in module under `stock_brokers.api.stoxkart` whose construction counts as a login and whose `_current_login` answers from a shared record standing in for the Redis `last_login` hash.

Notice that the headers pick up a token another process obtained without the session logging in.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartSession/example_1_authentication_headers.py
"""

import logging
import sys
import types

from stock_brokers.websockets.stoxkart import (
    StoxkartSession,
)


class LoginRecord:
    """The Stoxkart login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInStoxkartAPI:
    """A stand-in for `StoxkartAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'stoxkart-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'ucc_code': 'SK12345',
            'api_key': 'stoxkart-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class AuthenticationHeadersExample:
    """Prints a Stoxkart session's headers before and after another process logs in.

    Attributes:
        session (StoxkartSession): The session being shown.
    """

    def __init__(self):
        """Registers the stand-in API module and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.stoxkart')
        api_module.StoxkartAPI = StandInStoxkartAPI
        sys.modules['stock_brokers.api.stoxkart'] = api_module
        self.session = StoxkartSession()

    def run(self):
        """Prints the client code and the headers twice.

        Returns:
            None: This method returns nothing.
        """
        print(f'Client code: {self.session.client_id()}')
        print(f'Headers: {self.session.authentication_headers()}')
        LoginRecord.access_token = 'stoxkart-token-from-another-process'
        print(f'Headers after another process logged in: {self.session.authentication_headers()}')
        print(f'Logins made: {LoginRecord.login_count}')


if __name__ == '__main__':
    AuthenticationHeadersExample().run()

It prints:

Stand-in login number 1
Client code: SK12345
Headers: {'x-client-id': 'SK12345', 'x-access-token': 'stoxkart-token-1', 'x-platform': 'api', 'x-api-key': 'stoxkart-api-key'}
Headers after another process logged in: {'x-client-id': 'SK12345', 'x-access-token': 'stoxkart-token-from-another-process', 'x-platform': 'api', 'x-api-key': 'stoxkart-api-key'}
Logins made: 1
examples/stock_brokers/websockets/stoxkart/StoxkartSession/example_2_logging_in_again.py
"""Logs a Stoxkart session in again and shows the new token in its headers.

`StoxkartSession.log_in_again` logs in by constructing `StoxkartAPI` again. Unlike the other brokers' sessions it has no check for a token another process already replaced and no lock, because only the order socket uses it, and that socket calls it only after Stoxkart refused the token.

Constructing the real `StoxkartAPI` logs in to Stoxkart, so the program registers a stand-in module under `stock_brokers.api.stoxkart` whose constructions count as logins.

Notice the second login and the new `x-access-token`.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartSession/example_2_logging_in_again.py
"""

import logging
import sys
import types

from stock_brokers.websockets.stoxkart import (
    StoxkartSession,
)


class LoginRecord:
    """The Stoxkart login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInStoxkartAPI:
    """A stand-in for `StoxkartAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'stoxkart-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'ucc_code': 'SK12345',
            'api_key': 'stoxkart-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class LoggingInAgainExample:
    """Logs a Stoxkart session in again.

    Attributes:
        session (StoxkartSession): The session being shown.
    """

    def __init__(self):
        """Registers the stand-in API module and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.stoxkart')
        api_module.StoxkartAPI = StandInStoxkartAPI
        sys.modules['stock_brokers.api.stoxkart'] = api_module
        self.session = StoxkartSession()

    def run(self):
        """Prints the token before and after logging in again.

        Returns:
            None: This method returns nothing.
        """
        print(f"Token before: {self.session.authentication_headers()['x-access-token']}")
        self.session.log_in_again()
        print(f"Token after: {self.session.authentication_headers()['x-access-token']}")
        print(f'Logins made: {LoginRecord.login_count}')


if __name__ == '__main__':
    LoggingInAgainExample().run()

It prints:

Stand-in login number 1
Token before: stoxkart-token-1
Stand-in login number 2
Token after: stoxkart-token-2
Logins made: 2

Methods:

Name Description
authentication_headers

The headers the order socket's authentication request carries, with the access token in force now.

client_id

The client code the order socket connects as.

log_in_again

Logs in again by constructing StoxkartAPI again.

Source code in stock_brokers/websockets/stoxkart.py
def __init__(self):
    """
    Constructs `StoxkartAPI`, which logs in when the stored session is dead.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `StoxkartAPI` raises when it cannot log in.
    """
    from stock_brokers.api.stoxkart import StoxkartAPI

    self._api_class = StoxkartAPI
    self._stoxkart = StoxkartAPI()

authentication_headers

authentication_headers()

The headers the order socket's authentication request carries, with the access token in force now.

Returns:

Type Description
dict

The headers.

Example usage

From example_1_authentication_headers.py
print(f'Headers: {self.session.authentication_headers()}')
From example_1_authentication_headers.py
print(f'Headers after another process logged in: {self.session.authentication_headers()}')
From example_2_logging_in_again.py
print(f"Token before: {self.session.authentication_headers()['x-access-token']}")
Source code in stock_brokers/websockets/stoxkart.py
def authentication_headers(self):
    """
    The headers the order socket's authentication request carries, with the access token in force now.

    Returns:
        dict: The headers.
    """
    login = self._stoxkart._current_login() or {}
    settings = self._stoxkart._settings
    return {
        "x-client-id": settings["ucc_code"],
        "x-access-token": login.get("access_token") or "",
        "x-platform": "api",
        "x-api-key": settings["api_key"],
    }

client_id

client_id()

The client code the order socket connects as.

Returns:

Type Description
str

The ucc_code setting.

Example usage

From example_1_authentication_headers.py
print(f'Client code: {self.session.client_id()}')
Source code in stock_brokers/websockets/stoxkart.py
def client_id(self):
    """
    The client code the order socket connects as.

    Returns:
        str: The `ucc_code` setting.
    """
    return self._stoxkart._settings["ucc_code"]

log_in_again

log_in_again()

Logs in again by constructing StoxkartAPI again.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever StoxkartAPI raises when it cannot log in.

Example usage

From example_2_logging_in_again.py
self.session.log_in_again()
Source code in stock_brokers/websockets/stoxkart.py
def log_in_again(self):
    """
    Logs in again by constructing `StoxkartAPI` again.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `StoxkartAPI` raises when it cannot log in.
    """
    self._stoxkart = self._api_class()

StoxkartOrderSocket

StoxkartOrderSocket(session, on_updates, logger)

The order update websocket, authenticated with the API session and reconnected until stopped.

Attributes:

Name Type Description
logger Logger

Where connections, updates and failures are reported.

stop Event

Set when streaming should end.

Remembers the session and where updates go.

Parameters:

Name Type Description Default
session StoxkartSession

The logged-in API session whose token authenticates the socket.

required
on_updates callable

Called with each frame's messages, as dictionaries exactly as Stoxkart sent them, and the datetime the frame was read.

required
logger Logger

Where connections, updates and failures are reported.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/stoxkart/StoxkartOrderSocket/example_1_authenticate_and_read_updates.py
"""Runs Stoxkart's order socket through authentication, its heartbeat and a frame of order updates.

`StoxkartOrderSocket` first posts to Stoxkart's websocket authentication endpoint with the client code, the access token and the api key, and gets back a RequestId. It then opens `wss://openapi-v2.stoxkart.com/websocket/v2/connect` with that RequestId, sends `{"type": "heartbeat"}` as Stoxkart's website does, and hands each text frame's messages on exactly as sent, logging each one. Like the quote stream it reads a synchronous connection in a loop of its own.

This program scripts one connection delivering a JSON list of two order messages and a frame that is not JSON. No real socket or HTTP request is made: a stand-in `requests` package answers the authentication, a stand-in `websocket` package's `create_connection` returns a scripted connection, which sets the socket's `stop` event once its frames have run out, and a stand-in `stock_brokers.api.stoxkart` module lets the real `StoxkartSession` supply the headers without logging in.

Notice the RequestId in the socket URL, the heartbeat, that both messages reach the callback in one call, and that `run` returns 0 once stopped.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartOrderSocket/example_1_authenticate_and_read_updates.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.stoxkart import (
    StoxkartOrderSocket,
    StoxkartSession,
)


class LoginRecord:
    """The Stoxkart login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInStoxkartAPI:
    """A stand-in for `StoxkartAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'stoxkart-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'ucc_code': 'SK12345',
            'api_key': 'stoxkart-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class AuthenticateResponse:
    """A stand-in for the `requests` response of Stoxkart's websocket authentication.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body


class StandInRequests:
    """A stand-in for the `requests` package that answers websocket authentications in turn.

    Attributes:
        refusals (int): How many of the first requests to refuse with `AuthorizationError`.
        request_count (int): How many requests have been answered.
    """

    def __init__(self, refusals):
        """Keeps how many requests to refuse.

        Args:
            refusals (int): How many of the first requests to refuse with `AuthorizationError`.

        Returns:
            None: This method returns nothing.
        """
        self.refusals = refusals
        self.request_count = 0

    def post(self, url, headers, timeout):
        """Answers one authentication request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            timeout (int): The request timeout in seconds.

        Returns:
            AuthenticateResponse: The answer.
        """
        self.request_count = self.request_count + 1
        print(f"Authenticating with {headers['x-access-token']}")
        if self.request_count <= self.refusals:
            body = {
                'code': 'AuthorizationError',
                'message': 'Invalid access token',
            }
            return AuthenticateResponse(401, body)
        body = {
            'data': {
                'RequestId': f'request-{self.request_count}',
            },
        }
        return AuthenticateResponse(200, body)


class ReceiveTimeout(Exception):
    """A stand-in for `websocket.WebSocketTimeoutException`: nothing arrived within the receive timeout."""


class FrameOpcodes:
    """The websocket frame opcodes the Stoxkart streams compare against, as `websocket.ABNF` holds them.

    Attributes:
        OPCODE_TEXT (int): A text frame.
        OPCODE_BINARY (int): A binary frame.
        OPCODE_CLOSE (int): A close frame.
    """

    OPCODE_TEXT = 1
    OPCODE_BINARY = 2
    OPCODE_CLOSE = 8


class ScriptedSynchronousConnection:
    """A stand-in for a synchronous `websocket.WebSocket` that returns scripted frames from `recv_data`.

    Attributes:
        frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
        when_empty (callable | None): Called once the last frame has been read, to stop the stream.
    """

    def __init__(self, frames, when_empty):
        """Keeps the frames.

        Args:
            frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
            when_empty (callable | None): Called once the last frame has been read, to stop the stream.

        Returns:
            None: This method returns nothing.
        """
        self.frames = frames
        self.when_empty = when_empty

    def send_binary(self, data):
        """Prints a binary request the stream sends.

        Args:
            data (bytes): The request.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent binary request with code {data[0]} and {len(data)} bytes')

    def send(self, data):
        """Prints a text message the stream sends.

        Args:
            data (str): The message.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def recv_data(self):
        """Returns the next scripted frame, or times out once they have run out.

        Returns:
            tuple: The opcode and the frame's bytes.

        Raises:
            ReceiveTimeout: When no scripted frame is left.
        """
        if not self.frames:
            raise ReceiveTimeout()
        frame = self.frames.pop(0)
        if not self.frames and self.when_empty is not None:
            self.when_empty()
        return frame

    def close(self):
        """Accepts the stream's request to close.

        Returns:
            None: This method returns nothing.
        """


class ScriptedSynchronousModule:
    """A stand-in for the `websocket` package that hands out scripted synchronous connections in turn.

    Attributes:
        plans (list): Each connection's frames, in the order the stream connects.
        when_finished (callable | None): Called after the last connection's last frame, to stop the stream.
        ABNF (FrameOpcodes): The frame opcodes.
        WebSocketTimeoutException (type): The error `recv_data` raises when nothing arrived.
    """

    def __init__(self, plans):
        """Keeps the plans.

        Args:
            plans (list): Each connection's frames, in the order the stream connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None
        self.ABNF = FrameOpcodes()
        self.WebSocketTimeoutException = ReceiveTimeout

    def create_connection(self, url, timeout):
        """Opens the next scripted connection.

        Args:
            url (str): The socket URL.
            timeout (int): The receive timeout in seconds.

        Returns:
            ScriptedSynchronousConnection: The connection.
        """
        print(f'Connecting to {url}')
        frames = self.plans.pop(0)
        when_empty = None
        if not self.plans:
            when_empty = self.when_finished
        return ScriptedSynchronousConnection(frames, when_empty)


class AuthenticateAndReadUpdatesExample:
    """Runs the Stoxkart order socket against one scripted connection.

    Attributes:
        socket (StoxkartOrderSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the frames, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.stoxkart')
        api_module.StoxkartAPI = StandInStoxkartAPI
        sys.modules['stock_brokers.api.stoxkart'] = api_module
        sys.modules['requests'] = StandInRequests(0)
        updates = [
            {
                'order_id': 'SK2609250001',
                'status': 'OPEN',
                'symbol': 'INFY',
            },
            {
                'order_id': 'SK2609250001',
                'status': 'COMPLETE',
                'symbol': 'INFY',
            },
        ]
        plans = [
            [
                (
                    1,
                    json.dumps(updates).encode(),
                ),
                (
                    1,
                    b'connected',
                ),
            ],
        ]
        websocket_module = ScriptedSynchronousModule(plans)
        sys.modules['websocket'] = websocket_module
        self.socket = StoxkartOrderSocket(StoxkartSession(), self.print_updates, logging.getLogger('stoxkart.orders'))
        websocket_module.when_finished = self.socket.stop.set

    def print_updates(self, messages, received_at):
        """Prints each message the order socket handed on.

        Args:
            messages (list): The frame's messages, exactly as Stoxkart sent them.
            received_at (datetime.datetime): When the frame was read, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        print(f'Callback received {len(messages)} message(s)')

    def run(self):
        """Runs the socket until the scripted connection has delivered everything.

        Returns:
            None: This method returns nothing.
        """
        exit_code = self.socket.run()
        print(f'run returned {exit_code}')


if __name__ == '__main__':
    AuthenticateAndReadUpdatesExample().run()

It prints:

Stand-in login number 1
Authenticating with stoxkart-token-1
Connecting to wss://openapi-v2.stoxkart.com/websocket/v2/connect?x-client-id=SK12345&x-platform=api&RequestId=request-1
INFO Connected to wss://openapi-v2.stoxkart.com/websocket/v2/connect.
Sent: {"type": "heartbeat"}
INFO Order socket message: {"order_id": "SK2609250001", "status": "OPEN", "symbol": "INFY"}
INFO Order socket message: {"order_id": "SK2609250001", "status": "COMPLETE", "symbol": "INFY"}
Callback received 2 message(s)
INFO Ignoring a frame that is not JSON: connected
INFO Stopped
run returned 0
examples/stock_brokers/websockets/stoxkart/StoxkartOrderSocket/example_2_refused_then_evicted.py
"""Shows Stoxkart's order socket logging in again after a refused authentication, then being evicted by another session.

When Stoxkart refuses the authentication with `AuthorizationError` or HTTP 401, `StoxkartOrderSocket` raises `StoxkartSessionRefused` inside its loop, logs in again through `StoxkartSession.log_in_again`, and retries at once. Stoxkart keeps one order socket per client, and a newer connection, such as the website's, closes the older one with a close reason containing `new incoming connection`; the socket then waits five minutes before reclaiming it.

This program's first authentication is refused and the second succeeds; the connection then receives a close frame whose reason is Stoxkart's eviction message. No real socket or HTTP request is made: stand-in `requests` and `websocket` packages play the answers and the frames, a stand-in `stock_brokers.api.stoxkart` module counts each `StoxkartAPI` construction as one login, and the socket's `stop` event is replaced by a stand-in whose waits return at once and are printed, so the five minute wait is shown but skipped.

Notice the new token on the second authentication, the eviction warning, and the 300 second wait.

Run it from the project root:

    python examples/stock_brokers/websockets/stoxkart/StoxkartOrderSocket/example_2_refused_then_evicted.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.stoxkart import (
    StoxkartOrderSocket,
    StoxkartSession,
)


class LoginRecord:
    """The Stoxkart login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInStoxkartAPI:
    """A stand-in for `StoxkartAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'stoxkart-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'ucc_code': 'SK12345',
            'api_key': 'stoxkart-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class AuthenticateResponse:
    """A stand-in for the `requests` response of Stoxkart's websocket authentication.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body


class StandInRequests:
    """A stand-in for the `requests` package that answers websocket authentications in turn.

    Attributes:
        refusals (int): How many of the first requests to refuse with `AuthorizationError`.
        request_count (int): How many requests have been answered.
    """

    def __init__(self, refusals):
        """Keeps how many requests to refuse.

        Args:
            refusals (int): How many of the first requests to refuse with `AuthorizationError`.

        Returns:
            None: This method returns nothing.
        """
        self.refusals = refusals
        self.request_count = 0

    def post(self, url, headers, timeout):
        """Answers one authentication request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            timeout (int): The request timeout in seconds.

        Returns:
            AuthenticateResponse: The answer.
        """
        self.request_count = self.request_count + 1
        print(f"Authenticating with {headers['x-access-token']}")
        if self.request_count <= self.refusals:
            body = {
                'code': 'AuthorizationError',
                'message': 'Invalid access token',
            }
            return AuthenticateResponse(401, body)
        body = {
            'data': {
                'RequestId': f'request-{self.request_count}',
            },
        }
        return AuthenticateResponse(200, body)


class ReceiveTimeout(Exception):
    """A stand-in for `websocket.WebSocketTimeoutException`: nothing arrived within the receive timeout."""


class FrameOpcodes:
    """The websocket frame opcodes the Stoxkart streams compare against, as `websocket.ABNF` holds them.

    Attributes:
        OPCODE_TEXT (int): A text frame.
        OPCODE_BINARY (int): A binary frame.
        OPCODE_CLOSE (int): A close frame.
    """

    OPCODE_TEXT = 1
    OPCODE_BINARY = 2
    OPCODE_CLOSE = 8


class ScriptedSynchronousConnection:
    """A stand-in for a synchronous `websocket.WebSocket` that returns scripted frames from `recv_data`.

    Attributes:
        frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
        when_empty (callable | None): Called once the last frame has been read, to stop the stream.
    """

    def __init__(self, frames, when_empty):
        """Keeps the frames.

        Args:
            frames (list): Pairs of an opcode and the frame's bytes, in the order they arrive.
            when_empty (callable | None): Called once the last frame has been read, to stop the stream.

        Returns:
            None: This method returns nothing.
        """
        self.frames = frames
        self.when_empty = when_empty

    def send_binary(self, data):
        """Prints a binary request the stream sends.

        Args:
            data (bytes): The request.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent binary request with code {data[0]} and {len(data)} bytes')

    def send(self, data):
        """Prints a text message the stream sends.

        Args:
            data (str): The message.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def recv_data(self):
        """Returns the next scripted frame, or times out once they have run out.

        Returns:
            tuple: The opcode and the frame's bytes.

        Raises:
            ReceiveTimeout: When no scripted frame is left.
        """
        if not self.frames:
            raise ReceiveTimeout()
        frame = self.frames.pop(0)
        if not self.frames and self.when_empty is not None:
            self.when_empty()
        return frame

    def close(self):
        """Accepts the stream's request to close.

        Returns:
            None: This method returns nothing.
        """


class ScriptedSynchronousModule:
    """A stand-in for the `websocket` package that hands out scripted synchronous connections in turn.

    Attributes:
        plans (list): Each connection's frames, in the order the stream connects.
        when_finished (callable | None): Called after the last connection's last frame, to stop the stream.
        ABNF (FrameOpcodes): The frame opcodes.
        WebSocketTimeoutException (type): The error `recv_data` raises when nothing arrived.
    """

    def __init__(self, plans):
        """Keeps the plans.

        Args:
            plans (list): Each connection's frames, in the order the stream connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None
        self.ABNF = FrameOpcodes()
        self.WebSocketTimeoutException = ReceiveTimeout

    def create_connection(self, url, timeout):
        """Opens the next scripted connection.

        Args:
            url (str): The socket URL.
            timeout (int): The receive timeout in seconds.

        Returns:
            ScriptedSynchronousConnection: The connection.
        """
        print(f'Connecting to {url}')
        frames = self.plans.pop(0)
        when_empty = None
        if not self.plans:
            when_empty = self.when_finished
        return ScriptedSynchronousConnection(frames, when_empty)


class ImmediateStopEvent:
    """A stand-in for the stream's `threading.Event` whose waits return at once and are printed.

    Attributes:
        stopped (bool): Whether the stream has been told to stop.
    """

    def __init__(self):
        """Starts not stopped.

        Returns:
            None: This method returns nothing.
        """
        self.stopped = False

    def set(self):
        """Tells the stream to stop.

        Returns:
            None: This method returns nothing.
        """
        self.stopped = True

    def is_set(self):
        """Says whether the stream has been told to stop.

        Returns:
            bool: True once stopped.
        """
        return self.stopped

    def wait(self, timeout):
        """Prints the wait the stream asked for and returns at once.

        Args:
            timeout (float): How many seconds the stream would wait.

        Returns:
            bool: Whether the stream has been told to stop.
        """
        print(f'The stream waits {timeout} second(s) here; this program skips the wait.')
        return self.stopped


class RefusedThenEvictedExample:
    """Runs the Stoxkart order socket through a refused authentication and an eviction.

    Attributes:
        socket (StoxkartOrderSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the frames, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.stoxkart')
        api_module.StoxkartAPI = StandInStoxkartAPI
        sys.modules['stock_brokers.api.stoxkart'] = api_module
        sys.modules['requests'] = StandInRequests(1)
        close_frame = (4000).to_bytes(2, 'big') + b'Closing old connection: new incoming connection'
        plans = [
            [
                (
                    8,
                    close_frame,
                ),
            ],
        ]
        websocket_module = ScriptedSynchronousModule(plans)
        sys.modules['websocket'] = websocket_module
        self.socket = StoxkartOrderSocket(StoxkartSession(), self.print_updates, logging.getLogger('stoxkart.orders'))
        self.socket.stop = ImmediateStopEvent()
        websocket_module.when_finished = self.socket.stop.set

    def print_updates(self, messages, received_at):
        """Prints each message the order socket handed on.

        Args:
            messages (list): The frame's messages, exactly as Stoxkart sent them.
            received_at (datetime.datetime): When the frame was read, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        print(f'Callback received {len(messages)} message(s)')

    def run(self):
        """Runs the socket until the scripted connection has delivered everything.

        Returns:
            None: This method returns nothing.
        """
        exit_code = self.socket.run()
        print(f'run returned {exit_code}')
        print(f'Logins made: {LoginRecord.login_count}')


if __name__ == '__main__':
    RefusedThenEvictedExample().run()

It prints:

Stand-in login number 1
Authenticating with stoxkart-token-1
WARNING Stoxkart session expired, logging in again: Invalid access token
Stand-in login number 2
Authenticating with stoxkart-token-2
Connecting to wss://openapi-v2.stoxkart.com/websocket/v2/connect?x-client-id=SK12345&x-platform=api&RequestId=request-2
INFO Connected to wss://openapi-v2.stoxkart.com/websocket/v2/connect.
Sent: {"type": "heartbeat"}
WARNING Stoxkart closed the order socket: 'Closing old connection: new incoming connection'
WARNING Another Stoxkart session took the order socket; reclaiming it in 300 seconds.
The stream waits 300 second(s) here; this program skips the wait.
INFO Stopped
run returned 0
Logins made: 2

Methods:

Name Description
run

Authenticates, connects and hands on updates, reconnecting with a backoff, until stopped.

Source code in stock_brokers/websockets/stoxkart.py
def __init__(self, session, on_updates, logger):
    """
    Remembers the session and where updates go.

    Args:
        session (StoxkartSession): The logged-in API session whose token authenticates the socket.
        on_updates (callable): Called with each frame's messages, as dictionaries exactly as Stoxkart sent them, and the `datetime` the frame was read.
        logger (logging.Logger): Where connections, updates and failures are reported.

    Returns:
        None: This method returns nothing.
    """
    self.logger = logger
    self.stop = threading.Event()
    self._session = session
    self._on_updates = on_updates

run

run()

Authenticates, connects and hands on updates, reconnecting with a backoff, until stopped.

Returns:

Type Description
int

0 once stopped, and 1 when logging in again after a refused session failed.

Example usage

From example_1_authenticate_and_read_updates.py
exit_code = self.socket.run()
From example_2_refused_then_evicted.py
exit_code = self.socket.run()
Source code in stock_brokers/websockets/stoxkart.py
def run(self):
    """
    Authenticates, connects and hands on updates, reconnecting with a backoff, until stopped.

    Returns:
        int: 0 once stopped, and 1 when logging in again after a refused session failed.
    """
    backoff = MINIMUM_BACKOFF_SECONDS
    while not self.stop.is_set():
        wait = backoff
        try:
            request_id = self._authenticate()
            close_reason = self._stream(request_id)
            backoff = MINIMUM_BACKOFF_SECONDS
            wait = MINIMUM_BACKOFF_SECONDS
            if EVICTION_REASON in close_reason.lower():
                self.logger.warning(f"Another Stoxkart session took the order socket; reclaiming it in {EVICTED_WAIT_SECONDS} seconds.")
                wait = EVICTED_WAIT_SECONDS
        except StoxkartSessionRefused as error:
            self.logger.warning(f"Stoxkart session expired, logging in again: {error}")
            try:
                self._session.log_in_again()
            except Exception as login_error:
                self.logger.error(f"Could not log in to Stoxkart: {type(login_error).__name__}: {login_error}")
                return 1
            wait = 0
        except Exception as error:
            self.logger.error(f"Order socket failed, reconnecting in {backoff} seconds: {type(error).__name__}: {error}")
            backoff = min(backoff * 2, MAXIMUM_BACKOFF_SECONDS)
        if wait:
            self.stop.wait(wait)
    self.logger.info("Stopped")
    return 0

_authenticate

_authenticate()

Asks Stoxkart for the RequestId the socket connects with.

Returns:

Type Description
str

The RequestId.

Raises:

Type Description
StoxkartSessionRefused

Stoxkart refused the access token.

RuntimeError

Stoxkart answered without a RequestId.

RequestException

The request could not be sent.

Source code in stock_brokers/websockets/stoxkart.py
def _authenticate(self):
    """
    Asks Stoxkart for the RequestId the socket connects with.

    Returns:
        str: The RequestId.

    Raises:
        StoxkartSessionRefused: Stoxkart refused the access token.
        RuntimeError: Stoxkart answered without a RequestId.
        requests.RequestException: The request could not be sent.
    """
    import requests

    response = requests.post(AUTHENTICATE_URL, headers=self._session.authentication_headers(), timeout=TIMEOUT_SECONDS)
    try:
        body = response.json()
    except ValueError:
        body = {}
    if response.status_code == 401 or body.get("code") == "AuthorizationError":
        raise StoxkartSessionRefused(body.get("message") or f"HTTP {response.status_code}")
    request_id = (body.get("data") or {}).get("RequestId")
    if response.status_code >= 300 or not request_id:
        raise RuntimeError(f"authentication answered HTTP {response.status_code}: {response.text[:200]}")
    return request_id

_stream

_stream(request_id)

Opens the socket and reads it until it closes or streaming stops.

Parameters:

Name Type Description Default
request_id str

The RequestId from the authentication.

required

Returns:

Type Description
str

The close reason Stoxkart gave, or an empty string.

Raises:

Type Description
WebSocketException

The socket could not be opened or broke.

Source code in stock_brokers/websockets/stoxkart.py
def _stream(self, request_id):
    """
    Opens the socket and reads it until it closes or streaming stops.

    Args:
        request_id (str): The RequestId from the authentication.

    Returns:
        str: The close reason Stoxkart gave, or an empty string.

    Raises:
        websocket.WebSocketException: The socket could not be opened or broke.
    """
    import websocket

    url = f"{ORDER_SOCKET_URL}?x-client-id={self._session.client_id()}&x-platform=api&RequestId={request_id}"
    socket = websocket.create_connection(url, timeout=RECEIVE_TIMEOUT_SECONDS)
    self.logger.info(f"Connected to {ORDER_SOCKET_URL}.")
    try:
        return self._read(socket)
    finally:
        socket.close()

_read

_read(socket)

Sends heartbeats and hands on updates until the socket closes or streaming stops.

Parameters:

Name Type Description Default
socket WebSocket

The open connection.

required

Returns:

Type Description
str

The close reason Stoxkart gave, or an empty string.

Source code in stock_brokers/websockets/stoxkart.py
def _read(self, socket):
    """
    Sends heartbeats and hands on updates until the socket closes or streaming stops.

    Args:
        socket (websocket.WebSocket): The open connection.

    Returns:
        str: The close reason Stoxkart gave, or an empty string.
    """
    import websocket

    heartbeat = json.dumps({"type": "heartbeat"})
    socket.send(heartbeat)
    last_heartbeat_at = time.monotonic()
    while not self.stop.is_set():
        if time.monotonic() - last_heartbeat_at >= HEARTBEAT_SECONDS:
            socket.send(heartbeat)
            last_heartbeat_at = time.monotonic()
        try:
            opcode, frame = socket.recv_data()
        except websocket.WebSocketTimeoutException:
            continue
        if opcode == websocket.ABNF.OPCODE_CLOSE:
            reason = frame[2:].decode("utf-8", errors="replace")
            self.logger.warning(f"Stoxkart closed the order socket: {reason!r}")
            return reason
        if opcode == websocket.ABNF.OPCODE_TEXT:
            self._handle(frame.decode("utf-8", errors="replace"))
    return ""

_handle

_handle(text)

Logs every message in one text frame and hands them on.

Parameters:

Name Type Description Default
text str

The frame's text.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/stoxkart.py
def _handle(self, text):
    """
    Logs every message in one text frame and hands them on.

    Args:
        text (str): The frame's text.

    Returns:
        None: This method returns nothing.
    """
    try:
        payload = json.loads(text)
    except ValueError:
        self.logger.info(f"Ignoring a frame that is not JSON: {text[:200]}")
        return
    items = payload
    if not isinstance(payload, list):
        items = [payload]

    received_at = datetime.now()
    messages = []
    for item in items:
        if not isinstance(item, dict):
            continue
        self.logger.info(f"Order socket message: {json.dumps(item)[:300]}")
        messages.append(item)
    if messages:
        self._on_updates(messages, received_at)