Skip to content

zerodha

Zerodha's two websockets: Kite's market quotes ticker and the account's order updates.

Both connect to Kite's ticker at wss://ws.kite.trade, with the api key and the access token in the URL, so there is no login message. Kite refuses a dead token at the handshake with HTTP 403 (or 401), which BrokerWebsocket answers by logging in again once.

The login. ZerodhaSession holds the ZerodhaAPI every socket reads its credentials from, and reads them afresh on every connect, so a login made by any process is picked up. Kite issues one token per session and every login invalidates the last, so the session logs in again one socket at a time, and only when no other socket or process has already replaced the token that failed.

Quotes. ZerodhaQuotesSocket subscribes its batch of instrument tokens and puts them in full mode. Market data arrives as binary frames: a two byte packet count, then each packet after its two byte length. A packet's length says what it holds:

Length Packet
8 Last price
28 Index quote
32 Index quote with the exchange timestamp
44 Quote
184 Full quote with open interest and five levels of depth on each side

Every value is a big endian unsigned integer. The low byte of an instrument token names its exchange segment (738561 is NSE RELIANCE). Prices are integers, divided into rupees by segment: by 100, by 10,000,000 for NSE currency and by 10,000 for BSE currency. Index packets carry no quantities, and bytes 24 to 28 of an index packet are the change in points, so the percentage change is computed from the last price and the close instead. Text frames are Kite's own messages; an error is logged and the rest are ignored.

Order updates. Kite has no separate order endpoint: order updates arrive as JSON text messages on the same ticker socket. ZerodhaOrderUpdatesSocket subscribes to no instruments, so the only traffic on it is order updates, a quotes reconnect can never drop an order event, and no instrument quota is spent. Every change to an order arrives as {"type": "order", "data": {...}}, the data being Kite's order postback, which is the same object as an order book row. Binary frames are market data it never subscribed to and are ignored.

Neither socket writes Redis. The quotes socket hands each frame's ticks to on_ticks, and the order socket hands each message's orders to on_updates; the scripts in bin/zerodha/ do the writing.

Classes:

Name Description
ZerodhaSession

The Kite login every socket authenticates with, logged in again by one socket at a time.

ZerodhaQuotesSocket

One Kite ticker websocket carrying the market quotes of a batch of instruments.

ZerodhaOrderUpdatesSocket

A Kite ticker websocket subscribed to nothing, carrying the account's order updates.

ZerodhaSession

ZerodhaSession(logger)

The Kite login every socket authenticates with, logged in again by one socket at a time.

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

Parameters:

Name Type Description Default
logger Logger

Where logins are reported.

required

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever ZerodhaAPI raises when it cannot log in.

Example programs

examples/stock_brokers/websockets/zerodha/ZerodhaSession/example_1_reading_fresh_credentials.py
"""Reads the Kite credentials a socket connects with, including a token another process has just obtained.

A `ZerodhaSession` builds one `ZerodhaAPI` and asks it for the current login every time a socket connects, so a token that another script or process obtained is picked up without restarting anything. This program shows that: it reads the credentials once, lets a pretend other process log in, and reads them again.

Constructing the real `ZerodhaAPI` logs in to Kite, so the program registers a stand-in module under `stock_brokers.api.zerodha` before building the session. The stand-in `ZerodhaAPI` counts its constructions as logins, keeps the api key in `_settings` the way the real class loads it from MongoDB, and answers `_current_login` from a shared record that stands in for the Redis `last_login` hash. No data store, network or broker is touched.

Notice that the second read returns the other process's token although the session itself logged in only once.

Run it from the project root:

    python examples/stock_brokers/websockets/zerodha/ZerodhaSession/example_1_reading_fresh_credentials.py
"""

import logging
import sys
import types

from stock_brokers.websockets.zerodha import (
    ZerodhaSession,
)


class KiteLoginRecord:
    """The 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 StandInZerodhaAPI:
    """A stand-in for `ZerodhaAPI` 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.
        """
        KiteLoginRecord.login_count = KiteLoginRecord.login_count + 1
        KiteLoginRecord.access_token = f'kite-token-{KiteLoginRecord.login_count}'
        print(f'Stand-in login number {KiteLoginRecord.login_count}')
        self._settings = {
            'api_key': 'kite-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 KiteLoginRecord.access_token is None:
            return None
        return {
            'access_token': KiteLoginRecord.access_token,
        }


class ReadingFreshCredentialsExample:
    """Reads a session's credentials before and after another process logs in.

    Attributes:
        session (ZerodhaSession): 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.zerodha')
        api_module.ZerodhaAPI = StandInZerodhaAPI
        sys.modules['stock_brokers.api.zerodha'] = api_module
        self.session = ZerodhaSession(logging.getLogger('zerodha.session'))

    def run(self):
        """Prints the credentials, pretends another process logged in, and prints them again.

        Returns:
            None: This method returns nothing.
        """
        api_key, access_token = self.session.credentials()
        print(f'Credentials now: api_key={api_key} access_token={access_token}')
        KiteLoginRecord.access_token = 'kite-token-from-another-process'
        print('Another process logged in.')
        api_key, access_token = self.session.credentials()
        print(f'Credentials now: api_key={api_key} access_token={access_token}')
        print(f'Logins made by this program: {KiteLoginRecord.login_count}')


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

It prints:

Stand-in login number 1
Credentials now: api_key=kite-api-key access_token=kite-token-1
Another process logged in.
Credentials now: api_key=kite-api-key access_token=kite-token-from-another-process
Logins made by this program: 1
examples/stock_brokers/websockets/zerodha/ZerodhaSession/example_2_logging_in_again_once.py
"""Logs in to Kite again only when the token that failed is still the one in force.

Kite issues one token per session and every login invalidates the last, so when several sockets are refused at once only one of them should log in. `ZerodhaSession.log_in_again` takes the token the failing connection used and logs in only if that token is still current. This program asks it twice: first with the current token, which causes a login, and then with the same, now stale, token, which is skipped because the token has already been replaced.

Constructing the real `ZerodhaAPI` logs in to Kite, so the program registers a stand-in module under `stock_brokers.api.zerodha` before building the session. Each construction of the stand-in counts as one login and issues the next numbered token. No data store, network or broker is touched.

Notice that there are two logins in total, the one the session makes when it is built and the one the first request makes, and that the second request logs nothing.

Run it from the project root:

    python examples/stock_brokers/websockets/zerodha/ZerodhaSession/example_2_logging_in_again_once.py
"""

import logging
import sys
import types

from stock_brokers.websockets.zerodha import (
    ZerodhaSession,
)


class KiteLoginRecord:
    """The 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 StandInZerodhaAPI:
    """A stand-in for `ZerodhaAPI` 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.
        """
        KiteLoginRecord.login_count = KiteLoginRecord.login_count + 1
        KiteLoginRecord.access_token = f'kite-token-{KiteLoginRecord.login_count}'
        print(f'Stand-in login number {KiteLoginRecord.login_count}')
        self._settings = {
            'api_key': 'kite-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 KiteLoginRecord.access_token is None:
            return None
        return {
            'access_token': KiteLoginRecord.access_token,
        }


class LoggingInAgainOnceExample:
    """Asks a session to log in again twice with the same failed token.

    Attributes:
        session (ZerodhaSession): 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.zerodha')
        api_module.ZerodhaAPI = StandInZerodhaAPI
        sys.modules['stock_brokers.api.zerodha'] = api_module
        self.session = ZerodhaSession(logging.getLogger('zerodha.session'))

    def run(self):
        """Refuses the same token twice and prints the token in force after each request.

        Returns:
            None: This method returns nothing.
        """
        failed_token = self.session.credentials()[1]
        print(f'A socket was refused with {failed_token}')
        self.session.log_in_again(failed_token)
        print(f'Token in force: {self.session.credentials()[1]}')
        print(f'A second socket was refused with {failed_token}')
        self.session.log_in_again(failed_token)
        print(f'Token in force: {self.session.credentials()[1]}')
        print(f'Logins made: {KiteLoginRecord.login_count}')


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

It prints:

Stand-in login number 1
A socket was refused with kite-token-1
WARNING Logging in to Zerodha again.
Stand-in login number 2
Token in force: kite-token-2
A second socket was refused with kite-token-1
Token in force: kite-token-2
Logins made: 2

Methods:

Name Description
credentials

The api key and the access token in force now, which may be one another process has just obtained.

log_in_again

Logs in again, unless another socket or process already replaced the token that failed.

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

    Args:
        logger (logging.Logger): Where logins are reported.

    Returns:
        None: This method returns nothing.

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

    self._api_class = ZerodhaAPI
    self._logger = logger
    self._lock = threading.Lock()
    self._zerodha = ZerodhaAPI()

credentials

credentials()

The api key and the access token in force now, which may be one another process has just obtained.

Returns:

Type Description
tuple

The api key and the access token, either of which may be None.

Example usage

From example_1_reading_fresh_credentials.py
api_key, access_token = self.session.credentials()
From example_2_logging_in_again_once.py
failed_token = self.session.credentials()[1]
From example_2_logging_in_again_once.py
print(f'Token in force: {self.session.credentials()[1]}')
Source code in stock_brokers/websockets/zerodha.py
def credentials(self):
    """
    The api key and the access token in force now, which may be one another process has just obtained.

    Returns:
        tuple: The api key and the access token, either of which may be None.
    """
    login = self._zerodha._current_login() or {}
    return self._zerodha._settings.get("api_key"), login.get("access_token")

log_in_again

log_in_again(stale_token)

Logs in again, unless another socket or process already replaced the token that failed.

Kite issues one token per session and a login invalidates the one before, so two sockets logging in at once would each knock out the other's token.

Parameters:

Name Type Description Default
stale_token str | None

The access token the failing connection used.

required

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever ZerodhaAPI raises when it cannot log in.

Example usage

From example_2_logging_in_again_once.py
self.session.log_in_again(failed_token)
Source code in stock_brokers/websockets/zerodha.py
def log_in_again(self, stale_token):
    """
    Logs in again, unless another socket or process already replaced the token that failed.

    Kite issues one token per session and a login invalidates the one before, so two sockets logging in at once would each knock out the other's token.

    Args:
        stale_token (str | None): The access token the failing connection used.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `ZerodhaAPI` raises when it cannot log in.
    """
    with self._lock:
        if self.credentials()[1] != stale_token:
            return
        self._logger.warning("Logging in to Zerodha again.")
        self._zerodha = self._api_class()

ZerodhaQuotesSocket

ZerodhaQuotesSocket(name, tokens, names, session, on_ticks, logger)

Bases: BrokerWebsocket

One Kite ticker websocket carrying the market quotes of a batch of instruments.

Sets up a socket for one batch of instruments.

Parameters:

Name Type Description Default
name str

The connection's name, for the log.

required
tokens list[int]

The batch of Kite instrument tokens.

required
names dict[str, str]

Each token, as text, to the instrument's name, which becomes a tick's id.

required
session ZerodhaSession

The shared login.

required
on_ticks callable

Called with each frame's list of ticks, on this socket's thread.

required
logger Logger

Where the connection reports.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/zerodha/ZerodhaQuotesSocket/example_1_decoding_a_full_quote.py
"""Streams one Kite ticker frame through a quotes socket and prints the ticks it decodes.

A `ZerodhaQuotesSocket` opens `wss://ws.kite.trade`, subscribes its batch of instrument tokens in full mode, and turns each binary frame into normalized ticks that it hands to the function it was given. This program builds a frame the way Kite does, with one 184 byte full packet for NSE RELIANCE (token 738561) and one 8 byte last price packet for NSE INFY (token 408065), and lets the socket's own reconnect loop receive it.

No real socket is opened. The socket imports the `websocket` package when it connects, so the program registers a small stand-in under that name, which plays the scripted frame to the socket's handlers and prints every message the socket sends. The session is a stand-in with the one method the socket calls, `credentials`, so no Kite login happens. When the scripted connection ends, the stand-in closes the socket, which ends `run_forever`.

Notice in the output that the socket sends two messages when the connection opens, the subscription and the switch to full mode, and that Kite's integer prices come out in rupees, divided by 100. The full packet carries open interest and five levels of depth on each side, while the last price packet fills only `last_price`.

Run it from the project root:

    python examples/stock_brokers/websockets/zerodha/ZerodhaQuotesSocket/example_1_decoding_a_full_quote.py
"""

import logging
import struct
import sys

from stock_brokers.websockets.zerodha import (
    ZerodhaQuotesSocket,
)


class StandInSession:
    """A stand-in for `ZerodhaSession` that answers with fixed credentials and never logs in."""

    def credentials(self):
        """The api key and access token the socket puts in the feed URL.

        Returns:
            tuple: The api key and the access token.
        """
        return (
            'kite-api-key',
            'kite-access-token',
        )

    def log_in_again(self, stale_token):
        """Reports a login request, which this program never expects.

        Args:
            stale_token (str | None): The access token the failing connection used.

        Returns:
            None: This method returns nothing.
        """
        print(f'Asked to log in again after {stale_token}')


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that plays scripted frames to the socket's handlers.

    Attributes:
        url (str): The URL the socket asked for.
        frames (list): The frames to deliver after the connection opens.
        handlers (dict): The socket's callbacks, by name.
        when_finished (callable): Called after the last frame, to close the socket.
    """

    def __init__(self, url, frames, handlers, when_finished):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            frames (list): The frames to deliver after the connection opens.
            handlers (dict): The socket's callbacks, by name.
            when_finished (callable): Called after the last frame, to close the socket.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.frames = frames
        self.handlers = handlers
        self.when_finished = when_finished

    def run_forever(self, **options):
        """Opens, delivers every frame, then closes, as a real connection would.

        Args:
            **options (dict): The ping settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url} with {options}')
        self.handlers['on_open'](self)
        for frame in self.frames:
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')
        self.when_finished()

    def send(self, data):
        """Prints a message the socket sends to Kite.

        Args:
            data (str): The message.

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

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

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


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out one scripted connection.

    Attributes:
        frames (list): The frames the connection delivers.
        when_finished (callable | None): Called when the connection has delivered everything.
    """

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

        Args:
            frames (list): The frames the connection delivers.

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

    def WebSocketApp(self, url, **handlers):
        """Builds the scripted connection, with the same signature as the real class.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        return ScriptedConnection(url, self.frames, handlers, self.when_finished)


class KiteFrames:
    """Builds Kite ticker frames byte by byte, as `wss://ws.kite.trade` sends them."""

    def integers(self, values):
        """Packs unsigned big endian four byte integers.

        Args:
            values (list): The integers.

        Returns:
            bytes: The packed integers.
        """
        packed = b''
        for value in values:
            packed = packed + struct.pack('>I', value)
        return packed

    def full_packet(self, token):
        """A 184 byte full packet with a quote, open interest and ten depth levels.

        Args:
            token (int): The instrument token.

        Returns:
            bytes: The packet.
        """
        quote = [
            token,
            295050,
            25,
            294812,
            1204500,
            54000,
            61000,
            293000,
            296000,
            292500,
            291000,
            1790311528,
            0,
            0,
            0,
            1790311529,
        ]
        packet = self.integers(quote)
        for level in range(10):
            packet = packet + struct.pack('>IIHH', 100 + level, 295000 + level * 5, 3 + level, 0)
        return packet

    def last_price_packet(self, token, price):
        """An 8 byte last price packet.

        Args:
            token (int): The instrument token.
            price (int): The price in paise.

        Returns:
            bytes: The packet.
        """
        values = [
            token,
            price,
        ]
        return self.integers(values)

    def frame(self, packets):
        """A binary frame: the packet count, then each packet after its two byte length.

        Args:
            packets (list): The packets.

        Returns:
            bytes: The frame.
        """
        frame = struct.pack('>H', len(packets))
        for packet in packets:
            frame = frame + struct.pack('>H', len(packet)) + packet
        return frame


class DecodingAFullQuoteExample:
    """Runs a quotes socket against one scripted frame and prints the ticks.

    Attributes:
        websocket_module (ScriptedWebsocketModule): The stand-in `websocket` package.
        socket (ZerodhaQuotesSocket): The socket being shown.
    """

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

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        kite_frames = KiteFrames()
        packets = [
            kite_frames.full_packet(738561),
            kite_frames.last_price_packet(408065, 150025),
        ]
        frames = [
            kite_frames.frame(packets),
        ]
        self.websocket_module = ScriptedWebsocketModule(frames)
        sys.modules['websocket'] = self.websocket_module
        tokens = [
            738561,
            408065,
        ]
        names = {
            '738561': 'NSE:RELIANCE',
            '408065': 'NSE:INFY',
        }
        self.socket = ZerodhaQuotesSocket(
            'socket_0',
            tokens,
            names,
            StandInSession(),
            self.print_ticks,
            logging.getLogger('zerodha.quotes'),
        )
        self.websocket_module.when_finished = self.socket.close

    def print_ticks(self, ticks):
        """Prints the main fields of each tick the socket decoded.

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

        Returns:
            None: This method returns nothing.
        """
        for tick in ticks:
            print(f"Tick {tick['id']} on {tick['exchange']} in {tick['mode']} mode")
            change = tick['change']
            if change is not None:
                change = round(change, 4)
            print(f"  last_price={tick['last_price']} volume={tick['volume']} change={change}")
            print(f"  ohlc={tick['ohlc']}")
            print(f"  oi={tick['oi']} last_trade_time={tick['last_trade_time']}")
            if tick['depth']['buy']:
                print(f"  best bid={tick['depth']['buy'][0]} best offer={tick['depth']['sell'][0]}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connection ends.

        Returns:
            None: This method returns nothing.
        """
        self.socket.run_forever()
        print(f'Gave up: {self.socket.gave_up}')


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

It prints:

Connecting to wss://ws.kite.trade/?api_key=kite-api-key&access_token=kite-access-token with {'ping_interval': 30, 'ping_timeout': 10}
Sent: {"a": "subscribe", "v": [738561, 408065]}
Sent: {"a": "mode", "v": ["full", [738561, 408065]]}
INFO socket_0 opened and subscribed to 2 instrument(s).
Tick NSE:RELIANCE on nse in full mode
  last_price=2950.5 volume=1204500 change=1.3918
  ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0}
  oi=0 last_trade_time=1790311528
  best bid={'quantity': 100, 'price': 2950.0, 'orders': 3} best offer={'quantity': 105, 'price': 2950.25, 'orders': 8}
Tick NSE:INFY on nse in ltp mode
  last_price=1500.25 volume=None change=None
  ohlc={'open': None, 'high': None, 'low': None, 'close': None}
  oi=None last_trade_time=None
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
examples/stock_brokers/websockets/zerodha/ZerodhaQuotesSocket/example_2_refused_token_logs_in_again.py
"""Shows a quotes socket logging in again after Kite refuses its token, then receiving an index quote.

When Kite refuses a dead access token it fails the websocket handshake with HTTP 403. `ZerodhaQuotesSocket` notes the refusal, its reconnect loop asks the shared `ZerodhaSession` to log in again, and the next connection uses the new token. This program scripts two connections: the first is refused with 403, and the second opens and delivers one 28 byte index quote for NIFTY 50 (token 256265, whose low byte 9 marks the indices segment).

No real socket is opened and no real login happens. The program registers a stand-in `websocket` package that hands out the two scripted connections and prints the URL of each, and a stand-in `stock_brokers.api.zerodha` module whose `ZerodhaAPI` counts its constructions as logins, so the real `ZerodhaSession` runs unchanged on top of it. The socket's backoff is set to zero seconds so the reconnect happens at once.

Notice that the second URL carries the new token, and that an index tick has no quantities and gets its percentage `change` from the last price and the close.

Run it from the project root:

    python examples/stock_brokers/websockets/zerodha/ZerodhaQuotesSocket/example_2_refused_token_logs_in_again.py
"""

import logging
import struct
import sys
import types

from stock_brokers.websockets.zerodha import (
    ZerodhaQuotesSocket,
    ZerodhaSession,
)


class KiteLoginRecord:
    """The 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 StandInZerodhaAPI:
    """A stand-in for `ZerodhaAPI` 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.
        """
        KiteLoginRecord.login_count = KiteLoginRecord.login_count + 1
        KiteLoginRecord.access_token = f'kite-token-{KiteLoginRecord.login_count}'
        self._settings = {
            'api_key': 'kite-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 KiteLoginRecord.access_token is None:
            return None
        return {
            'access_token': KiteLoginRecord.access_token,
        }


class HandshakeRefused(Exception):
    """A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.

    Attributes:
        status_code (int): The HTTP status Kite answered with.
    """

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

        Args:
            status_code (int): The HTTP status Kite answered with.

        Returns:
            None: This method returns nothing.
        """
        super().__init__(f'Handshake status {status_code} Forbidden')
        self.status_code = status_code


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames.

    Attributes:
        url (str): The URL the socket asked for.
        plan (dict): Either a `refuse_with` status or a list of `frames`.
        handlers (dict): The socket's callbacks, by name.
    """

    def __init__(self, url, plan, handlers):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            plan (dict): Either a `refuse_with` status or a list of `frames`.
            handlers (dict): The socket's callbacks, by name.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.plan = plan
        self.handlers = handlers

    def run_forever(self, **options):
        """Plays the plan and returns when the pretend connection closes.

        Args:
            **options (dict): The ping settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url}')
        if 'refuse_with' in self.plan:
            self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
            self.handlers['on_close'](self, None, None)
            return
        self.handlers['on_open'](self)
        for frame in self.plan['frames']:
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data):
        """Prints a message the socket sends to Kite.

        Args:
            data (str): The message.

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

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

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


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

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection has been handed out and has run.
    """

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

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class RefusedTokenLogsInAgainExample:
    """Runs a quotes socket through a refused handshake and a good connection.

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

    def __init__(self):
        """Registers the stand-ins and builds 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.zerodha')
        api_module.ZerodhaAPI = StandInZerodhaAPI
        sys.modules['stock_brokers.api.zerodha'] = api_module
        index_values = [
            256265,
            2515065,
            2521000,
            2498000,
            2500000,
            2490000,
            150000,
        ]
        index_packet = b''
        for value in index_values:
            index_packet = index_packet + struct.pack('>I', value)
        frame = struct.pack('>H', 1) + struct.pack('>H', len(index_packet)) + index_packet
        plans = [
            {
                'refuse_with': 403,
            },
            {
                'frames': [
                    frame,
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('zerodha.quotes')
        session = ZerodhaSession(logger)
        tokens = [
            256265,
        ]
        names = {
            '256265': 'NSE:NIFTY 50',
        }
        self.socket = ZerodhaQuotesSocket('socket_0', tokens, names, session, self.print_ticks, logger)
        self.socket.MIN_BACKOFF_SECONDS = 0
        websocket_module.when_finished = self.socket.close

    def print_ticks(self, ticks):
        """Prints each index tick the socket decoded.

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

        Returns:
            None: This method returns nothing.
        """
        for tick in ticks:
            print(f"Tick {tick['id']} on {tick['exchange']} in {tick['mode']} mode")
            print(f"  last_price={tick['last_price']} volume={tick['volume']} change={round(tick['change'], 4)}")
            print(f"  ohlc={tick['ohlc']}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connections run out.

        Returns:
            None: This method returns nothing.
        """
        self.socket.run_forever()
        print(f'Gave up: {self.socket.gave_up}')
        print(f'Logins made: {KiteLoginRecord.login_count}')


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

It prints:

Connecting to wss://ws.kite.trade/?api_key=kite-api-key&access_token=kite-token-1
ERROR socket_0 error: Handshake status 403 Forbidden
INFO socket_0 closed with status None: None
WARNING Logging in to Zerodha again.
WARNING socket_0 disconnected. Reconnecting in 0 seconds.
Connecting to wss://ws.kite.trade/?api_key=kite-api-key&access_token=kite-token-2
Sent: {"a": "subscribe", "v": [256265]}
Sent: {"a": "mode", "v": ["full", [256265]]}
INFO socket_0 opened and subscribed to 1 instrument(s).
Tick NSE:NIFTY 50 on indices in quote mode
  last_price=25150.65 volume=None change=1.0066
  ohlc={'high': 25210.0, 'low': 24980.0, 'open': 25000.0, 'close': 24900.0}
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
Logins made: 2

Methods:

Name Description
close

Closes the connection and stops reconnecting.

run_forever

Connects, and reconnects with backoff, until closed or unable to recover.

Source code in stock_brokers/websockets/zerodha.py
def __init__(self, name, tokens, names, session, on_ticks, logger):
    """
    Sets up a socket for one batch of instruments.

    Args:
        name (str): The connection's name, for the log.
        tokens (list[int]): The batch of Kite instrument tokens.
        names (dict[str, str]): Each token, as text, to the instrument's name, which becomes a tick's `id`.
        session (ZerodhaSession): The shared login.
        on_ticks (callable): Called with each frame's list of ticks, on this socket's thread.
        logger (logging.Logger): Where the connection reports.

    Returns:
        None: This method returns nothing.
    """
    super().__init__(name, logger)
    self._tokens = tokens
    self._names = names
    self._session = session
    self._on_ticks = on_ticks
    self._token = None

_connect

_connect()

Opens the websocket with the token in force now and blocks until it closes.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _connect(self):
    """
    Opens the websocket with the token in force now and blocks until it closes.

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

    api_key, self._token = self._session.credentials()
    url = f"{FEED_URL}/?api_key={api_key}&access_token={self._token}"
    self._websocket_application = websocket.WebSocketApp(
        url,
        on_open=self._on_open,
        on_message=self._on_message,
        on_error=self._on_error,
        on_close=self._on_close,
    )
    self._websocket_application.run_forever(
        ping_interval=PING_INTERVAL_SECONDS,
        ping_timeout=PING_TIMEOUT_SECONDS,
    )

_log_in_again

_log_in_again()

Logs in again through the shared session, which skips the login when the token was already replaced.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _log_in_again(self):
    """
    Logs in again through the shared session, which skips the login when the token was already replaced.

    Returns:
        None: This method returns nothing.
    """
    self._session.log_in_again(self._token)

_on_open

_on_open(websocket_connection)

Subscribes the batch and puts it in full mode.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that opened.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _on_open(self, websocket_connection):
    """
    Subscribes the batch and puts it in full mode.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that opened.

    Returns:
        None: This method returns nothing.
    """
    self._opened = True
    websocket_connection.send(json.dumps({"a": "subscribe", "v": self._tokens}))
    websocket_connection.send(json.dumps({"a": "mode", "v": ["full", self._tokens]}))
    self._logger.info(f"{self.name} opened and subscribed to {len(self._tokens)} instrument(s).")

_on_error

_on_error(websocket_connection, error)

Notes a refused token and reports the error.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that failed.

required
error Exception

The error, which carries status_code when the handshake was refused.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _on_error(self, websocket_connection, error):
    """
    Notes a refused token and reports the error.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that failed.
        error (Exception): The error, which carries `status_code` when the handshake was refused.

    Returns:
        None: This method returns nothing.
    """
    if getattr(error, "status_code", None) in AUTHENTICATION_STATUSES:
        self._authentication_rejected = True
    self._logger.error(f"{self.name} error: {error}")

_on_message

_on_message(websocket_connection, message)

Decodes a binary frame into ticks and hands them on, and logs Kite's text errors.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection the frame arrived on.

required
message bytes | str

The frame.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _on_message(self, websocket_connection, message):
    """
    Decodes a binary frame into ticks and hands them on, and logs Kite's text errors.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection the frame arrived on.
        message (bytes | str): The frame.

    Returns:
        None: This method returns nothing.
    """
    if not isinstance(message, bytes):
        try:
            payload = json.loads(message)
        except (TypeError, json.JSONDecodeError):
            return
        if isinstance(payload, dict) and payload.get("type") == "error":
            self._logger.error(f"{self.name}: Kite reported an error: {payload.get('data')}")
        return
    ticks = []
    for packet in self._split_packets(message):
        tick = self._parse_packet(packet)
        if tick:
            ticks.append(tick)
    if ticks:
        self._on_ticks(ticks)

_split_packets

_split_packets(frame)

Splits a frame into its packets.

Parameters:

Name Type Description Default
frame bytes

The binary frame: a two byte count, then each packet after its two byte length.

required

Returns:

Type Description
list[bytes]

The packets, stopping early at a frame that ends before its count says.

Source code in stock_brokers/websockets/zerodha.py
def _split_packets(self, frame):
    """
    Splits a frame into its packets.

    Args:
        frame (bytes): The binary frame: a two byte count, then each packet after its two byte length.

    Returns:
        list[bytes]: The packets, stopping early at a frame that ends before its count says.
    """
    if len(frame) < 2:
        return []
    packets = []
    offset = 2
    for _ in range(self._unpack(frame, 0, 2, "H")):
        if offset + 2 > len(frame):
            break
        length = self._unpack(frame, offset, offset + 2, "H")
        packets.append(frame[offset + 2:offset + 2 + length])
        offset = offset + 2 + length
    return packets

_parse_packet

_parse_packet(packet)

Builds a normalized tick from one packet.

Parameters:

Name Type Description Default
packet bytes

The binary packet.

required

Returns:

Type Description
dict | None

The tick, or None for a packet that carries no tick.

Source code in stock_brokers/websockets/zerodha.py
def _parse_packet(self, packet):
    """
    Builds a normalized tick from one packet.

    Args:
        packet (bytes): The binary packet.

    Returns:
        dict | None: The tick, or None for a packet that carries no tick.
    """
    length = len(packet)
    if length < 8:
        return None
    token = self._unpack(packet, 0, 4)
    exchange = SEGMENTS.get(token & 0xff)
    divisor = self._price_divisor(exchange)

    if length == 8:
        tick = self._empty_tick(token, exchange, "ltp")
        tick["last_price"] = self._unpack(packet, 4, 8) / divisor
        return tick

    if exchange == "indices" and length in (28, 32):
        mode = "full"
        if length == 28:
            mode = "quote"
        tick = self._empty_tick(token, exchange, mode)
        tick["last_price"] = self._unpack(packet, 4, 8) / divisor
        tick["ohlc"] = {
            "high": self._unpack(packet, 8, 12) / divisor,
            "low": self._unpack(packet, 12, 16) / divisor,
            "open": self._unpack(packet, 16, 20) / divisor,
            "close": self._unpack(packet, 20, 24) / divisor,
        }
        self._apply_change(tick)
        if length == 32:
            tick["exchange_timestamp"] = self._unpack(packet, 28, 32)
        return tick

    if length not in (44, 184):
        self._logger.debug(f"{self.name} skipped a packet of unexpected length {length}.")
        return None

    mode = "full"
    if length == 44:
        mode = "quote"
    tick = self._empty_tick(token, exchange, mode)
    tick["last_price"] = self._unpack(packet, 4, 8) / divisor
    tick["last_quantity"] = self._unpack(packet, 8, 12)
    tick["average_price"] = self._unpack(packet, 12, 16) / divisor
    tick["volume"] = self._unpack(packet, 16, 20)
    tick["buy_quantity"] = self._unpack(packet, 20, 24)
    tick["sell_quantity"] = self._unpack(packet, 24, 28)
    tick["ohlc"] = {
        "open": self._unpack(packet, 28, 32) / divisor,
        "high": self._unpack(packet, 32, 36) / divisor,
        "low": self._unpack(packet, 36, 40) / divisor,
        "close": self._unpack(packet, 40, 44) / divisor,
    }
    self._apply_change(tick)

    if length == 184:
        tick["last_trade_time"] = self._unpack(packet, 44, 48)
        tick["oi"] = self._unpack(packet, 48, 52)
        tick["oi_day_high"] = self._unpack(packet, 52, 56)
        tick["oi_day_low"] = self._unpack(packet, 56, 60)
        tick["exchange_timestamp"] = self._unpack(packet, 60, 64)
        level = 0
        for offset in range(64, 184, 12):
            side = "buy"
            if level >= 5:
                side = "sell"
            tick["depth"][side].append({
                "quantity": self._unpack(packet, offset, offset + 4),
                "price": self._unpack(packet, offset + 4, offset + 8) / divisor,
                "orders": self._unpack(packet, offset + 8, offset + 10, "H"),
            })
            level = level + 1
    return tick

_empty_tick

_empty_tick(token, exchange, mode)

A tick with every field unset.

Parameters:

Name Type Description Default
token int

The Kite instrument token.

required
exchange str | None

The segment its low byte names.

required
mode str

ltp, quote or full.

required

Returns:

Type Description
dict

The tick, named from the batch's names or by its token.

Source code in stock_brokers/websockets/zerodha.py
def _empty_tick(self, token, exchange, mode):
    """
    A tick with every field unset.

    Args:
        token (int): The Kite instrument token.
        exchange (str | None): The segment its low byte names.
        mode (str): `ltp`, `quote` or `full`.

    Returns:
        dict: The tick, named from the batch's names or by its token.
    """
    return {
        "id": self._names.get(str(token)) or str(token),
        "broker": "zerodha",
        "instrument_token": token,
        "exchange": exchange,
        "mode": mode,
        "last_price": None,
        "last_quantity": None,
        "average_price": None,
        "volume": None,
        "buy_quantity": None,
        "sell_quantity": None,
        "ohlc": {
            "open": None,
            "high": None,
            "low": None,
            "close": None,
        },
        "change": None,
        "oi": None,
        "oi_day_high": None,
        "oi_day_low": None,
        "last_trade_time": None,
        "exchange_timestamp": None,
        "depth": {
            "buy": [],
            "sell": [],
        },
        "received_at": time.time(),
    }

_unpack

_unpack(data, start, end, byte_format='I')

A big endian value from a slice of a packet.

Parameters:

Name Type Description Default
data bytes

The packet or frame.

required
start int

Where the slice starts.

required
end int

Where the slice ends.

required
byte_format str

The struct format character, I for four bytes and H for two.

'I'

Returns:

Type Description
int

The value.

Source code in stock_brokers/websockets/zerodha.py
def _unpack(self, data, start, end, byte_format="I"):
    """
    A big endian value from a slice of a packet.

    Args:
        data (bytes): The packet or frame.
        start (int): Where the slice starts.
        end (int): Where the slice ends.
        byte_format (str): The `struct` format character, `I` for four bytes and `H` for two.

    Returns:
        int: The value.
    """
    return struct.unpack(">" + byte_format, data[start:end])[0]

_price_divisor

_price_divisor(exchange)

What Kite's integer prices are divided by to give rupees.

Parameters:

Name Type Description Default
exchange str | None

The segment the token's low byte names.

required

Returns:

Type Description
float

The divisor.

Source code in stock_brokers/websockets/zerodha.py
def _price_divisor(self, exchange):
    """
    What Kite's integer prices are divided by to give rupees.

    Args:
        exchange (str | None): The segment the token's low byte names.

    Returns:
        float: The divisor.
    """
    if exchange == "cds":
        return 10000000.0
    if exchange in ("bcd", "bsecds"):
        return 10000.0
    return 100.0

_apply_change

_apply_change(tick)

Sets the percentage change of the last price against the close, when there is a close.

Parameters:

Name Type Description Default
tick dict

The tick to adjust.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _apply_change(self, tick):
    """
    Sets the percentage change of the last price against the close, when there is a close.

    Args:
        tick (dict): The tick to adjust.

    Returns:
        None: This method returns nothing.
    """
    close = tick["ohlc"]["close"]
    if close:
        tick["change"] = (tick["last_price"] - close) * 100 / close
    else:
        tick["change"] = None

close

close()

Closes the connection and stops reconnecting.

Returns:

Type Description
None

This method returns nothing.

Example usage

From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.close()
Source code in stock_brokers/websockets/base.py
def close(self):
    """
    Closes the connection and stops reconnecting.

    Returns:
        None: This method returns nothing.
    """
    self._stop.set()
    if self._websocket_application is not None:
        self._websocket_application.close()

run_forever

run_forever()

Connects, and reconnects with backoff, until closed or unable to recover.

Returns:

Type Description
None

This method returns nothing. gave_up says whether the socket stopped because it could not recover.

Example usage

From example_1_reconnecting_after_a_drop.py
self.socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.refused_socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.run_forever()
Source code in stock_brokers/websockets/base.py
def run_forever(self):
    """
    Connects, and reconnects with backoff, until closed or unable to recover.

    Returns:
        None: This method returns nothing. `gave_up` says whether the socket stopped because it could not recover.
    """
    backoff = self.MIN_BACKOFF_SECONDS
    failed_connects = 0
    logged_in_again = False
    while not self._stop.is_set():
        self._opened = False
        self._authentication_rejected = False
        try:
            self._connect()
        except Exception as exception:
            self._logger.error(f"{self.name} connection failed: {type(exception).__name__}: {exception}")
        if self._stop.is_set():
            break

        if self._opened and not self._authentication_rejected:
            failed_connects = 0
            logged_in_again = False
            backoff = self.MIN_BACKOFF_SECONDS
        else:
            failed_connects = failed_connects + 1

        if self._authentication_rejected or failed_connects >= self.MAX_FAILED_CONNECTS:
            if logged_in_again and self._gives_up_after_logging_in_again(failed_connects):
                self.gave_up = True
                self._logger.error(f"{self.name} still cannot connect after logging in again. Giving up.")
                break
            try:
                self._log_in_again()
            except Exception as exception:
                self.gave_up = True
                self._logger.error(f"{self.name} could not log in again: {type(exception).__name__}: {exception}")
                break
            failed_connects = 0
            logged_in_again = True

        self._logger.warning(f"{self.name} disconnected. Reconnecting in {backoff} seconds.")
        self._stop.wait(backoff)
        backoff = min(backoff * 2, self.MAX_BACKOFF_SECONDS)
    self._logger.info(f"{self.name} stopped.")

_gives_up_after_logging_in_again

_gives_up_after_logging_in_again(failed_connects)

Whether a socket that has already logged in again gives up on this failure.

The loop asks only when the connection was refused or has failed six times in a row. By default the answer is yes, so a refusal straight after logging in again ends the socket. A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.

Parameters:

Name Type Description Default
failed_connects int

How many connects in a row have failed since the last login.

required

Returns:

Type Description
bool

True to give up now.

Source code in stock_brokers/websockets/base.py
def _gives_up_after_logging_in_again(self, failed_connects):
    """
    Whether a socket that has already logged in again gives up on this failure.

    The loop asks only when the connection was refused or has failed six times in a row.
    By default the answer is yes, so a refusal straight after logging in again ends the socket.
    A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.

    Args:
        failed_connects (int): How many connects in a row have failed since the last login.

    Returns:
        bool: True to give up now.
    """
    return True

_on_close

_on_close(websocket_connection, status_code, message)

Reports that the connection closed.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that closed.

required
status_code int | None

The close status the broker sent.

required
message str | None

The close reason the broker sent.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/base.py
def _on_close(self, websocket_connection, status_code, message):
    """
    Reports that the connection closed.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that closed.
        status_code (int | None): The close status the broker sent.
        message (str | None): The close reason the broker sent.

    Returns:
        None: This method returns nothing.
    """
    self._logger.info(f"{self.name} closed with status {status_code}: {message}")

ZerodhaOrderUpdatesSocket

ZerodhaOrderUpdatesSocket(session, on_updates, logger)

Bases: BrokerWebsocket

A Kite ticker websocket subscribed to nothing, carrying the account's order updates.

Sets up the order updates socket.

Parameters:

Name Type Description Default
session ZerodhaSession

The login.

required
on_updates callable

Called on this socket's thread with each message's Kite orders, as a list of dictionaries exactly as Kite sent them, and the datetime the message was received.

required
logger Logger

Where the connection reports.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/zerodha/ZerodhaOrderUpdatesSocket/example_1_receiving_an_order_update.py
"""Receives one Kite order update on the order updates socket and prints the order it hands on.

Kite has no separate order endpoint. `ZerodhaOrderUpdatesSocket` connects to the same ticker as the quotes socket but subscribes to nothing, so the only messages it receives are `{"type": "order", "data": {...}}` updates, whose data is the same object as an order book row. The socket hands each message's orders, exactly as Kite sent them, to the function it was given, together with the moment the message arrived.

No real socket is opened. The program registers a stand-in `websocket` package whose one connection opens, delivers a single order update for a completed INFY buy, and closes; the stand-in then closes the socket, which ends `run_forever`. The session is a stand-in with fixed credentials, so no Kite login happens. The arrival time is not printed, because it is the wall clock.

Notice that the socket sends nothing when it opens and that the order reaches the callback unchanged.

Run it from the project root:

    python examples/stock_brokers/websockets/zerodha/ZerodhaOrderUpdatesSocket/example_1_receiving_an_order_update.py
"""

import json
import logging
import sys

from stock_brokers.websockets.zerodha import (
    ZerodhaOrderUpdatesSocket,
)


class StandInSession:
    """A stand-in for `ZerodhaSession` that answers with fixed credentials and never logs in."""

    def credentials(self):
        """The api key and access token the socket puts in the feed URL.

        Returns:
            tuple: The api key and the access token.
        """
        return (
            'kite-api-key',
            'kite-access-token',
        )

    def log_in_again(self, stale_token):
        """Reports a login request, which this program never expects.

        Args:
            stale_token (str | None): The access token the failing connection used.

        Returns:
            None: This method returns nothing.
        """
        print(f'Asked to log in again after {stale_token}')


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that plays scripted messages to the socket's handlers.

    Attributes:
        url (str): The URL the socket asked for.
        messages (list): The messages to deliver after the connection opens.
        handlers (dict): The socket's callbacks, by name.
        when_finished (callable): Called after the last message, to close the socket.
    """

    def __init__(self, url, messages, handlers, when_finished):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            messages (list): The messages to deliver after the connection opens.
            handlers (dict): The socket's callbacks, by name.
            when_finished (callable): Called after the last message, to close the socket.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.messages = messages
        self.handlers = handlers
        self.when_finished = when_finished

    def run_forever(self, **options):
        """Opens, delivers every message, then closes, as a real connection would.

        Args:
            **options (dict): The ping settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url}')
        self.handlers['on_open'](self)
        for message in self.messages:
            self.handlers['on_message'](self, message)
        self.handlers['on_close'](self, 1000, 'normal closure')
        self.when_finished()

    def send(self, data):
        """Prints a message the socket sends to Kite.

        Args:
            data (str): The message.

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

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

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


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out one scripted connection.

    Attributes:
        messages (list): The messages the connection delivers.
        when_finished (callable | None): Called when the connection has delivered everything.
    """

    def __init__(self, messages):
        """Keeps the messages to deliver.

        Args:
            messages (list): The messages the connection delivers.

        Returns:
            None: This method returns nothing.
        """
        self.messages = messages
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the scripted connection, with the same signature as the real class.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        return ScriptedConnection(url, self.messages, handlers, self.when_finished)


class ReceivingAnOrderUpdateExample:
    """Runs the order updates socket against one scripted order update.

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

    def __init__(self):
        """Builds the order update, the stand-in package and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        order = {
            'order_id': '260925000000001',
            'exchange_order_id': '1100000012345678',
            'status': 'COMPLETE',
            'tradingsymbol': 'INFY',
            'exchange': 'NSE',
            'transaction_type': 'BUY',
            'order_type': 'LIMIT',
            'product': 'CNC',
            'quantity': 10,
            'filled_quantity': 10,
            'price': 1500.0,
            'average_price': 1499.5,
            'order_timestamp': '2026-09-25 10:15:30',
        }
        update = {
            'type': 'order',
            'data': order,
        }
        messages = [
            json.dumps(update),
        ]
        websocket_module = ScriptedWebsocketModule(messages)
        sys.modules['websocket'] = websocket_module
        self.socket = ZerodhaOrderUpdatesSocket(StandInSession(), self.print_orders, logging.getLogger('zerodha.orders'))
        websocket_module.when_finished = self.socket.close

    def print_orders(self, orders, received_at):
        """Prints each order the socket handed on.

        Args:
            orders (list): Kite's orders from one message, exactly as sent.
            received_at (datetime.datetime): When the message arrived, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        for order in orders:
            print(f"Order {order['order_id']} {order['status']}: {order['transaction_type']} {order['filled_quantity']} {order['tradingsymbol']} at {order['average_price']}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connection ends.

        Returns:
            None: This method returns nothing.
        """
        print(f'Socket name: {self.socket.name}')
        self.socket.run_forever()
        print(f'Gave up: {self.socket.gave_up}')


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

It prints:

Socket name: Order updates socket
Connecting to wss://ws.kite.trade/?api_key=kite-api-key&access_token=kite-access-token
INFO Order updates socket opened. Waiting for order updates.
Order 260925000000001 COMPLETE: BUY 10 INFY at 1499.5
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
examples/stock_brokers/websockets/zerodha/ZerodhaOrderUpdatesSocket/example_2_errors_and_other_messages.py
"""Sends the order updates socket a mix of messages and shows which ones become orders.

Kite can batch several messages into one frame as a JSON list, and not every message is an order update. `ZerodhaOrderUpdatesSocket` hands on only the `order` entries of a frame, logs Kite's `error` messages as errors, logs any other message type as ignored, drops binary frames silently because they are market data it never subscribed to, and notes a frame that is not JSON at debug level.

No real socket is opened. The program registers a stand-in `websocket` package whose one connection delivers four frames: a JSON list holding an order update, an error and a `message` entry; a binary frame; a frame that is not JSON; and a second order update on its own. The session is a stand-in with fixed credentials, so no Kite login happens. The logger shows debug lines so the ignored frame appears.

Notice that the callback runs twice, once per frame that held an order, and that the binary frame leaves no trace at all.

Run it from the project root:

    python examples/stock_brokers/websockets/zerodha/ZerodhaOrderUpdatesSocket/example_2_errors_and_other_messages.py
"""

import json
import logging
import sys

from stock_brokers.websockets.zerodha import (
    ZerodhaOrderUpdatesSocket,
)


class StandInSession:
    """A stand-in for `ZerodhaSession` that answers with fixed credentials and never logs in."""

    def credentials(self):
        """The api key and access token the socket puts in the feed URL.

        Returns:
            tuple: The api key and the access token.
        """
        return (
            'kite-api-key',
            'kite-access-token',
        )

    def log_in_again(self, stale_token):
        """Reports a login request, which this program never expects.

        Args:
            stale_token (str | None): The access token the failing connection used.

        Returns:
            None: This method returns nothing.
        """
        print(f'Asked to log in again after {stale_token}')


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that plays scripted messages to the socket's handlers.

    Attributes:
        url (str): The URL the socket asked for.
        messages (list): The messages to deliver after the connection opens.
        handlers (dict): The socket's callbacks, by name.
        when_finished (callable): Called after the last message, to close the socket.
    """

    def __init__(self, url, messages, handlers, when_finished):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            messages (list): The messages to deliver after the connection opens.
            handlers (dict): The socket's callbacks, by name.
            when_finished (callable): Called after the last message, to close the socket.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.messages = messages
        self.handlers = handlers
        self.when_finished = when_finished

    def run_forever(self, **options):
        """Opens, delivers every message, then closes, as a real connection would.

        Args:
            **options (dict): The ping settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url}')
        self.handlers['on_open'](self)
        for message in self.messages:
            self.handlers['on_message'](self, message)
        self.handlers['on_close'](self, 1000, 'normal closure')
        self.when_finished()

    def send(self, data):
        """Prints a message the socket sends to Kite.

        Args:
            data (str): The message.

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

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

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


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out one scripted connection.

    Attributes:
        messages (list): The messages the connection delivers.
        when_finished (callable | None): Called when the connection has delivered everything.
    """

    def __init__(self, messages):
        """Keeps the messages to deliver.

        Args:
            messages (list): The messages the connection delivers.

        Returns:
            None: This method returns nothing.
        """
        self.messages = messages
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the scripted connection, with the same signature as the real class.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        return ScriptedConnection(url, self.messages, handlers, self.when_finished)


class ErrorsAndOtherMessagesExample:
    """Runs the order updates socket against a mix of scripted frames.

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

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

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.DEBUG)
        batch = [
            {
                'type': 'order',
                'data': self.order('260925000000002', 'OPEN', 0),
            },
            {
                'type': 'error',
                'data': 'Too many requests',
            },
            {
                'type': 'message',
                'data': 'Welcome',
            },
        ]
        single = {
            'type': 'order',
            'data': self.order('260925000000002', 'COMPLETE', 5),
        }
        messages = [
            json.dumps(batch),
            b'\x00\x01\x00\x08binary',
            'not json',
            json.dumps(single),
        ]
        websocket_module = ScriptedWebsocketModule(messages)
        sys.modules['websocket'] = websocket_module
        self.socket = ZerodhaOrderUpdatesSocket(StandInSession(), self.print_orders, logging.getLogger('zerodha.orders'))
        websocket_module.when_finished = self.socket.close

    def order(self, order_id, status, filled_quantity):
        """A Kite order postback for a TCS sell.

        Args:
            order_id (str): Kite's order id.
            status (str): Kite's order status.
            filled_quantity (int): How much has filled.

        Returns:
            dict: The order.
        """
        return {
            'order_id': order_id,
            'status': status,
            'tradingsymbol': 'TCS',
            'exchange': 'NSE',
            'transaction_type': 'SELL',
            'quantity': 5,
            'filled_quantity': filled_quantity,
            'average_price': 4120.0,
        }

    def print_orders(self, orders, received_at):
        """Prints each order the socket handed on.

        Args:
            orders (list): Kite's orders from one message, exactly as sent.
            received_at (datetime.datetime): When the message arrived, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        print(f'Callback received {len(orders)} order(s)')
        for order in orders:
            print(f"Order {order['order_id']} {order['status']}: {order['transaction_type']} {order['filled_quantity']} {order['tradingsymbol']} at {order['average_price']}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connection ends.

        Returns:
            None: This method returns nothing.
        """
        self.socket.run_forever()
        print(f'Gave up: {self.socket.gave_up}')


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

It prints:

Connecting to wss://ws.kite.trade/?api_key=kite-api-key&access_token=kite-access-token
INFO Order updates socket opened. Waiting for order updates.
ERROR Kite reported an error: Too many requests
INFO Ignoring a message of type 'message': {"type": "message", "data": "Welcome"}
Callback received 1 order(s)
Order 260925000000002 OPEN: SELL 0 TCS at 4120.0
DEBUG Ignoring a frame that is not JSON: not json
Callback received 1 order(s)
Order 260925000000002 COMPLETE: SELL 5 TCS at 4120.0
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False

Methods:

Name Description
close

Closes the connection and stops reconnecting.

run_forever

Connects, and reconnects with backoff, until closed or unable to recover.

Source code in stock_brokers/websockets/zerodha.py
def __init__(self, session, on_updates, logger):
    """
    Sets up the order updates socket.

    Args:
        session (ZerodhaSession): The login.
        on_updates (callable): Called on this socket's thread with each message's Kite orders, as a list of dictionaries exactly as Kite sent them, and the `datetime` the message was received.
        logger (logging.Logger): Where the connection reports.

    Returns:
        None: This method returns nothing.
    """
    super().__init__("Order updates socket", logger)
    self._session = session
    self._on_updates = on_updates
    self._token = None

_connect

_connect()

Opens the websocket with the token in force now and blocks until it closes.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _connect(self):
    """
    Opens the websocket with the token in force now and blocks until it closes.

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

    api_key, self._token = self._session.credentials()
    url = f"{FEED_URL}/?api_key={api_key}&access_token={self._token}"
    self._websocket_application = websocket.WebSocketApp(
        url,
        on_open=self._on_open,
        on_message=self._on_message,
        on_error=self._on_error,
        on_close=self._on_close,
    )
    self._websocket_application.run_forever(
        ping_interval=PING_INTERVAL_SECONDS,
        ping_timeout=PING_TIMEOUT_SECONDS,
    )

_log_in_again

_log_in_again()

Logs in again through the session, which skips the login when the token was already replaced.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _log_in_again(self):
    """
    Logs in again through the session, which skips the login when the token was already replaced.

    Returns:
        None: This method returns nothing.
    """
    self._session.log_in_again(self._token)

_on_open

_on_open(websocket_connection)

Subscribes to nothing: without a subscription Kite pushes order updates and no market data.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that opened.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _on_open(self, websocket_connection):
    """
    Subscribes to nothing: without a subscription Kite pushes order updates and no market data.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that opened.

    Returns:
        None: This method returns nothing.
    """
    self._opened = True
    self._logger.info(f"{self.name} opened. Waiting for order updates.")

_on_error

_on_error(websocket_connection, error)

Notes a refused token and reports the error.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that failed.

required
error Exception

The error, which carries status_code when the handshake was refused.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _on_error(self, websocket_connection, error):
    """
    Notes a refused token and reports the error.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that failed.
        error (Exception): The error, which carries `status_code` when the handshake was refused.

    Returns:
        None: This method returns nothing.
    """
    if getattr(error, "status_code", None) in AUTHENTICATION_STATUSES:
        self._authentication_rejected = True
    self._logger.error(f"Order updates error: {error}")

_on_message

_on_message(websocket_connection, message)

Hands a message's order updates on, logs Kite's errors, and ignores everything else.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection the message arrived on.

required
message bytes | str

The frame: one JSON message, or a JSON list of them.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/zerodha.py
def _on_message(self, websocket_connection, message):
    """
    Hands a message's order updates on, logs Kite's errors, and ignores everything else.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection the message arrived on.
        message (bytes | str): The frame: one JSON message, or a JSON list of them.

    Returns:
        None: This method returns nothing.
    """
    if isinstance(message, (bytes, bytearray)):
        return

    try:
        payload = json.loads(message)
    except (TypeError, json.JSONDecodeError):
        self._logger.debug(f"Ignoring a frame that is not JSON: {str(message)[:160]}")
        return

    received_at = datetime.now()
    entries = payload
    if not isinstance(payload, list):
        entries = [payload]
    orders = []
    for entry in entries:
        if not isinstance(entry, dict):
            continue
        if entry.get("type") == ORDER:
            orders.append(entry.get("data"))
        elif entry.get("type") == "error":
            self._logger.error(f"Kite reported an error: {entry.get('data')}")
        else:
            self._logger.info(f"Ignoring a message of type {entry.get('type')!r}: {json.dumps(entry)[:200]}")
    if orders:
        self._on_updates(orders, received_at)

close

close()

Closes the connection and stops reconnecting.

Returns:

Type Description
None

This method returns nothing.

Example usage

From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.close()
Source code in stock_brokers/websockets/base.py
def close(self):
    """
    Closes the connection and stops reconnecting.

    Returns:
        None: This method returns nothing.
    """
    self._stop.set()
    if self._websocket_application is not None:
        self._websocket_application.close()

run_forever

run_forever()

Connects, and reconnects with backoff, until closed or unable to recover.

Returns:

Type Description
None

This method returns nothing. gave_up says whether the socket stopped because it could not recover.

Example usage

From example_1_reconnecting_after_a_drop.py
self.socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.refused_socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.run_forever()
Source code in stock_brokers/websockets/base.py
def run_forever(self):
    """
    Connects, and reconnects with backoff, until closed or unable to recover.

    Returns:
        None: This method returns nothing. `gave_up` says whether the socket stopped because it could not recover.
    """
    backoff = self.MIN_BACKOFF_SECONDS
    failed_connects = 0
    logged_in_again = False
    while not self._stop.is_set():
        self._opened = False
        self._authentication_rejected = False
        try:
            self._connect()
        except Exception as exception:
            self._logger.error(f"{self.name} connection failed: {type(exception).__name__}: {exception}")
        if self._stop.is_set():
            break

        if self._opened and not self._authentication_rejected:
            failed_connects = 0
            logged_in_again = False
            backoff = self.MIN_BACKOFF_SECONDS
        else:
            failed_connects = failed_connects + 1

        if self._authentication_rejected or failed_connects >= self.MAX_FAILED_CONNECTS:
            if logged_in_again and self._gives_up_after_logging_in_again(failed_connects):
                self.gave_up = True
                self._logger.error(f"{self.name} still cannot connect after logging in again. Giving up.")
                break
            try:
                self._log_in_again()
            except Exception as exception:
                self.gave_up = True
                self._logger.error(f"{self.name} could not log in again: {type(exception).__name__}: {exception}")
                break
            failed_connects = 0
            logged_in_again = True

        self._logger.warning(f"{self.name} disconnected. Reconnecting in {backoff} seconds.")
        self._stop.wait(backoff)
        backoff = min(backoff * 2, self.MAX_BACKOFF_SECONDS)
    self._logger.info(f"{self.name} stopped.")

_gives_up_after_logging_in_again

_gives_up_after_logging_in_again(failed_connects)

Whether a socket that has already logged in again gives up on this failure.

The loop asks only when the connection was refused or has failed six times in a row. By default the answer is yes, so a refusal straight after logging in again ends the socket. A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.

Parameters:

Name Type Description Default
failed_connects int

How many connects in a row have failed since the last login.

required

Returns:

Type Description
bool

True to give up now.

Source code in stock_brokers/websockets/base.py
def _gives_up_after_logging_in_again(self, failed_connects):
    """
    Whether a socket that has already logged in again gives up on this failure.

    The loop asks only when the connection was refused or has failed six times in a row.
    By default the answer is yes, so a refusal straight after logging in again ends the socket.
    A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.

    Args:
        failed_connects (int): How many connects in a row have failed since the last login.

    Returns:
        bool: True to give up now.
    """
    return True

_on_close

_on_close(websocket_connection, status_code, message)

Reports that the connection closed.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that closed.

required
status_code int | None

The close status the broker sent.

required
message str | None

The close reason the broker sent.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/base.py
def _on_close(self, websocket_connection, status_code, message):
    """
    Reports that the connection closed.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that closed.
        status_code (int | None): The close status the broker sent.
        message (str | None): The close reason the broker sent.

    Returns:
        None: This method returns nothing.
    """
    self._logger.info(f"{self.name} closed with status {status_code}: {message}")