Skip to content

zerodha

Zerodha (Kite) ticks.

A Kite instrument token carries its segment in the low byte, so the token alone says which instruments it can be - no prefix to parse. The segment codes are the ones stock_brokers/instruments/historical/zerodha.py resolves candle series with, repeated here rather than imported because that module pulls in the Kite API client; the offline suite checks the two tables agree.

What Kite's values mean, as measured against the stored ticks:

  • close is the previous session's close at every hour: RELIANCE read 1274.0 in every row from the evening of 2026-09-12 through the Sunday reconnect, while Dhan read that Friday's own close.
  • On MCX every quantity is in lots. At 22:46:57 on 2026-09-11 CRUDEOIL SEP read volume 67958, open interest 18095, last quantity 2 and a best bid of 1 at both Zerodha and Dhan - a day's volume of 67958 barrels would be absurd, 67958 lots of 100 barrels is not.
  • On NSE and BSE every quantity is taken to be in shares or contract units, which a live session checks.
  • exchange_timestamp is when the exchange sent the packet, not when the instrument last traded, and last_trade_time is the trade. Both are true epochs.

Classes:

Name Description
ZerodhaTickNormalizer

Normalizes Zerodha ticks.

ZerodhaTickNormalizer

ZerodhaTickNormalizer()

Bases: TickNormalizer

Normalizes Zerodha ticks.

Build the lookup from each Zerodha segment code to the canonical segments it can hold.

A segment code is the low byte of a Kite instrument token. The table is worked out once here from SEGMENT_CODES, so feed_key only has to look a tick's segment code up rather than recompute its segments on every call.

Example programs

examples/stock_brokers/instruments/ticks/zerodha/ZerodhaTickNormalizer/example_1_placing_feed_tokens.py
"""Places Kite instrument tokens on the segments their instruments can be in.

A Kite instrument token is an integer whose low byte names the segment: 1 is NSE cash, 7 is MCX and 9 is the indices of both exchanges. The normalizer reads that byte, so 738561 (RELIANCE) is searched among NSE cash segments, 256265 (NIFTY 50) among every index segment of NSE and BSE, and 5720583 among MCX derivatives. A token given as a string of digits works too, but text that is not a number, or a number whose low byte is a code Kite does not use, such as 264 (code 8), cannot be placed.

The normalizer only works on the text of each token, so the program needs no Redis, database, network or stand-in of any kind. Each output line shows a token as the feed spells it, then the broker token that `unified.broker_mappings` is searched with, the order symbol when the broker names instruments by symbol, and how many canonical segments the search is limited to, with the first of them. A token the normalizer cannot place prints `None`. A Kite subscription set stores tokens as strings, while ticks carry integers, so `tick_spelling` turns each string into the integer that plans are filed under.

Run it from the project root:

    python examples/stock_brokers/instruments/ticks/zerodha/ZerodhaTickNormalizer/example_1_placing_feed_tokens.py
"""

from stock_brokers.instruments.ticks.zerodha import (
    ZerodhaTickNormalizer,
)


class PlacingFeedTokensExample:
    """Asks the Zerodha normalizer where each of a few feed tokens belongs.

    Attributes:
        normalizer (ZerodhaTickNormalizer): The normalizer being shown.
        tokens (list): The tokens to place, spelled as the feed spells them.
        subscription_tokens (list): Tokens as the broker's subscription set stores them.
    """

    def __init__(self):
        """Builds the normalizer and the tokens to place.

        Returns:
            None: This method returns nothing.
        """
        self.normalizer = ZerodhaTickNormalizer()
        self.tokens = [
            738561,
            256265,
            5720583,
            '408065',
            'NSE:INFY',
            264,
        ]
        self.subscription_tokens = [
            '738561',
            '256265',
            'NSE:INFY',
        ]

    def describe(self, feed_key):
        """Describes a feed key in one line.

        Args:
            feed_key (FeedKey | None): The key the normalizer returned.

        Returns:
            str: The description.
        """
        if feed_key is None:
            return 'None'
        segment_count = len(feed_key.segments)
        first_segment = feed_key.segments[0]
        return f'broker_token={feed_key.broker_token!r} order_symbol={feed_key.order_symbol!r} segments={segment_count} starting with {first_segment}'

    def run(self):
        """Prints where each token belongs.

        Returns:
            None: This method returns nothing.
        """
        print(f'Broker: {ZerodhaTickNormalizer.BROKER_NAME}')
        for token in self.tokens:
            feed_key = self.normalizer.feed_key(token)
            print(f'{token!r} -> {self.describe(feed_key)}')
        print('Subscription tokens as ticks spell them:')
        for token in self.subscription_tokens:
            spelling = self.normalizer.tick_spelling(token)
            print(f'{token!r} -> {spelling!r}')


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

It prints:

Broker: zerodha
738561 -> broker_token='738561' order_symbol=None segments=6 starting with nse_equities
256265 -> broker_token='256265' order_symbol=None segments=8 starting with nse_equity_indices
5720583 -> broker_token='5720583' order_symbol=None segments=16 starting with mcx_fixed_income_futures
'408065' -> broker_token='408065' order_symbol=None segments=6 starting with nse_equities
'NSE:INFY' -> None
264 -> None
Subscription tokens as ticks spell them:
'738561' -> 738561
'256265' -> 256265
'NSE:INFY' -> None
examples/stock_brokers/instruments/ticks/zerodha/ZerodhaTickNormalizer/example_2_mcx_quantities_in_lots.py
"""Normalizes one Zerodha tick for an MCX crude oil future, whose quantities arrive in lots.

Kite reports every MCX quantity - last quantity, volume, the order book and open interest - in lots, so the plan multiplies each by the contract's 100-barrel lot. Its `close` is always the previous session's close, so `reported_close` is kept both before and after the session ends.

The plan that `normalize` applies is normally compiled by `TickResolver` from the mapping cache. This program builds it by hand with the same rules, asking the normalizer how it reports each quantity field and when its close may be trusted, so it needs no database. The tick is shaped like the ones the broker's market feed publishes, with fixed times, so the output is the same on every run. The program normalizes the tick twice, once as received before the session's close and once as received after it, which only changes whether `reported_close` is kept. Order book levels are printed as (price, quantity, orders).

Run it from the project root:

    python examples/stock_brokers/instruments/ticks/zerodha/ZerodhaTickNormalizer/example_2_mcx_quantities_in_lots.py
"""

import datetime
import zoneinfo

from stock_brokers.instruments.ticks import base
from stock_brokers.instruments.ticks.base import (
    InstrumentPlan,
)
from stock_brokers.instruments.ticks.zerodha import (
    ZerodhaTickNormalizer,
)
from stock_brokers.instruments.ticks.utilities import sessions

INDIA = zoneinfo.ZoneInfo('Asia/Kolkata')


class CrudeOilTickExample:
    """Normalizes one Zerodha tick through a hand-built plan and prints the result.

    Attributes:
        normalizer (ZerodhaTickNormalizer): The normalizer being shown.
        received_at (float): When the tick was received, in epoch seconds.
        tick (dict): The tick as the market feed published it.
        plan (InstrumentPlan): The plan for the tick's instrument.
    """

    def __init__(self):
        """Builds the normalizer, the tick and its plan.

        Returns:
            None: This method returns nothing.
        """
        self.normalizer = ZerodhaTickNormalizer()
        received = datetime.datetime(2026, 9, 15, 22, 46, tzinfo=INDIA)
        self.received_at = received.timestamp()
        self.tick = {
            'instrument_token': 5720583,
            'last_price': 9731.0,
            'last_quantity': 2,
            'average_price': 9718.45,
            'volume': 67958,
            'buy_quantity': 4210,
            'sell_quantity': 3987,
            'ohlc': {
                'open': 9702.0,
                'high': 9745.0,
                'low': 9688.0,
                'close': 9722.0,
            },
            'oi': 18095,
            'oi_day_high': 18410,
            'oi_day_low': 17602,
            'last_trade_time': self.received_at - 2.0,
            'exchange_timestamp': self.received_at - 1.0,
            'received_at': self.received_at,
            'depth': {
                'buy': [
                    {
                        'price': 9730.0,
                        'quantity': 3,
                        'orders': 2,
                    },
                    {
                        'price': 9729.0,
                        'quantity': 11,
                        'orders': 4,
                    },
                    {
                        'price': 9728.0,
                        'quantity': 7,
                        'orders': 3,
                    },
                    {
                        'price': 9727.0,
                        'quantity': 9,
                        'orders': 5,
                    },
                    {
                        'price': 9726.0,
                        'quantity': 14,
                        'orders': 6,
                    },
                ],
                'sell': [
                    {
                        'price': 9731.0,
                        'quantity': 5,
                        'orders': 2,
                    },
                    {
                        'price': 9732.0,
                        'quantity': 8,
                        'orders': 3,
                    },
                    {
                        'price': 9733.0,
                        'quantity': 12,
                        'orders': 4,
                    },
                    {
                        'price': 9734.0,
                        'quantity': 6,
                        'orders': 2,
                    },
                    {
                        'price': 9735.0,
                        'quantity': 10,
                        'orders': 5,
                    },
                ],
            },
        }
        self.plan = self.build_plan()

    def build_plan(self):
        """Compiles the plan for the tick's instrument the way the resolver would.

        Returns:
            InstrumentPlan: The plan.
        """
        exchange = 'mcx'
        segment = 'mcx_commodity_futures'
        lot_size = 100
        broker_lot_size = 1
        multipliers = {}
        for field in base.QUANTITY_FIELDS:
            basis = self.normalizer.quantity_basis(exchange, field)
            if basis == base.BASIS_LOTS:
                multipliers[field] = lot_size
            elif basis == base.BASIS_BROKER_LOTS:
                multipliers[field] = lot_size // broker_lot_size
            else:
                multipliers[field] = 1
        identity = {
            'exchange': 'mcx',
            'segment': 'mcx_commodity_futures',
            'shape': 'future',
            'symbol': 'CRUDEOIL26OCTFUT',
            'underlying_symbol': 'CRUDEOIL',
            'expiry_date': '2026-10-19',
            'strike_price': None,
            'option_type': None,
        }
        instrument_id = '11111111-1111-5111-8111-000000000101'
        broker_token = '5720583'
        return InstrumentPlan(
            instrument_id=instrument_id,
            broker=ZerodhaTickNormalizer.BROKER_NAME,
            broker_token=broker_token,
            exchange=exchange,
            segment=segment,
            shape=identity['shape'],
            session=sessions.session_for(segment),
            lot_size=lot_size,
            multipliers=multipliers,
            price_decimals=2,
            negative_prices=True,
            close_policy=self.normalizer.close_policy(exchange),
            has_open_interest=identity['shape'] != 'security',
            trusts_last_trade_time=ZerodhaTickNormalizer.TRUSTS_LAST_TRADE_TIME,
            trusts_exchange_time=ZerodhaTickNormalizer.TRUSTS_EXCHANGE_TIME,
            identity_json=base.identity_json(
                instrument_id,
                ZerodhaTickNormalizer.BROKER_NAME,
                broker_token,
                identity,
                lot_size,
            ),
        )

    def run(self):
        """Prints the feed key, the plan's rules and the normalized values.

        Returns:
            None: This method returns nothing.
        """
        token = self.tick['instrument_token']
        feed_key = self.normalizer.feed_key(token)
        print(f'Token {token!r}: broker_token={feed_key.broker_token!r} order_symbol={feed_key.order_symbol!r}')
        volume_basis = self.normalizer.quantity_basis(self.plan.exchange, 'volume')
        print(f'Volume on {self.plan.exchange} is reported in {volume_basis}, multiplier {self.plan.multipliers["volume"]}')
        print(f'Close policy on {self.plan.exchange}: {self.normalizer.close_policy(self.plan.exchange)}')
        before_close = self.normalizer.normalize(self.tick, self.plan, True)
        print('Normalized, received before the close:')
        for name, value in before_close.items():
            print(f'  {name}: {value}')
        after_close = self.normalizer.normalize(self.tick, self.plan, False)
        print(f'reported_close when received after the close: {after_close["reported_close"]}')


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

It prints:

Token 5720583: broker_token='5720583' order_symbol=None
Volume on mcx is reported in lots, multiplier 100
Close policy on mcx: always
Normalized, received before the close:
  last_price: 9731.0
  last_quantity: 200
  average_price: 9718.45
  volume: 6795800
  buy_quantity: 421000
  sell_quantity: 398700
  open: 9702.0
  high: 9745.0
  low: 9688.0
  reported_close: 9722.0
  oi: 1809500
  oi_day_high: 1841000
  oi_day_low: 1760200
  last_trade_time: 1789492558.0
  exchange_time: 1789492559.0
  bids: [(9730.0, 300, 2), (9729.0, 1100, 4), (9728.0, 700, 3), (9727.0, 900, 5), (9726.0, 1400, 6)]
  asks: [(9731.0, 500, 2), (9732.0, 800, 3), (9733.0, 1200, 4), (9734.0, 600, 2), (9735.0, 1000, 5)]
reported_close when received after the close: 9722.0

Methods:

Name Description
feed_key

The broker token and segments for a Kite instrument token.

tick_spelling

A Kite subscription token as ticks carry it: an integer.

quantity_basis

Whether the broker reports a quantity field in lots or units on an exchange.

close_policy

When the broker's close is the previous session's close on an exchange.

normalize

Apply a plan to one tick.

Source code in stock_brokers/instruments/ticks/zerodha.py
def __init__(self):
    """
    Build the lookup from each Zerodha segment code to the canonical segments it can hold.

    A segment code is the low byte of a Kite instrument token. The table is worked out once here from `SEGMENT_CODES`, so `feed_key` only has to look a tick's segment code up rather than recompute its segments on every call.
    """
    self._segments = {code: family_segments(exchanges, family)
                      for code, (exchanges, family) in self.SEGMENT_CODES.items()}

feed_key

feed_key(instrument_token)

The broker token and segments for a Kite instrument token.

Parameters:

Name Type Description Default
instrument_token int | str

The tick's instrument_token, an integer.

required

Returns:

Type Description
FeedKey | None

The key, or None for a token that is not a number or whose segment code is unknown.

Example usage

From example_1_placing_feed_tokens.py
feed_key = self.normalizer.feed_key(token)
From example_2_mcx_quantities_in_lots.py
feed_key = self.normalizer.feed_key(token)
Source code in stock_brokers/instruments/ticks/zerodha.py
def feed_key(self, instrument_token):
    """
    The broker token and segments for a Kite instrument token.

    Args:
        instrument_token (int | str): The tick's `instrument_token`, an integer.

    Returns:
        FeedKey | None: The key, or None for a token that is not a number or whose segment code is unknown.
    """
    try:
        token = int(instrument_token)
    except (TypeError, ValueError):
        return None
    segments = self._segments.get(token & 0xff)
    if segments is None:
        return None
    return FeedKey(str(token), segments)

tick_spelling

tick_spelling(subscription_token)

A Kite subscription token as ticks carry it: an integer.

Parameters:

Name Type Description Default
subscription_token str

A member of subscriptions_zerodha, such as "738561".

required

Returns:

Type Description
int | None

The token, or None when it is not a number.

Example usage

From example_1_placing_feed_tokens.py
spelling = self.normalizer.tick_spelling(token)
Source code in stock_brokers/instruments/ticks/zerodha.py
def tick_spelling(self, subscription_token):
    """
    A Kite subscription token as ticks carry it: an integer.

    Args:
        subscription_token (str): A member of `subscriptions_zerodha`, such as "738561".

    Returns:
        int | None: The token, or None when it is not a number.
    """
    try:
        return int(subscription_token)
    except (TypeError, ValueError):
        return None

quantity_basis

quantity_basis(exchange, field)

Whether the broker reports a quantity field in lots or units on an exchange.

Parameters:

Name Type Description Default
exchange str

Canonical exchange.

required
field str

One of QUANTITY_FIELDS.

required

Returns:

Type Description
str

BASIS_UNITS, BASIS_LOTS or BASIS_BROKER_LOTS.

Example usage

From example_1_a_new_broker_subclass.py
basis = self.normalizer.quantity_basis(exchange, field)
From example_2_what_normalize_refuses.py
self.normalizer.quantity_basis(exchange, field)
Source code in stock_brokers/instruments/ticks/base.py
def quantity_basis(self, exchange, field):
    """
    Whether the broker reports a quantity field in lots or units on an exchange.

    Args:
        exchange (str): Canonical exchange.
        field (str): One of QUANTITY_FIELDS.

    Returns:
        str: BASIS_UNITS, BASIS_LOTS or BASIS_BROKER_LOTS.
    """
    if field in self.BROKER_LOT_FIELDS.get(exchange, ()):
        return BASIS_BROKER_LOTS
    if field in self.LOT_FIELDS.get(exchange, ()):
        return BASIS_LOTS
    return BASIS_UNITS

close_policy

close_policy(exchange)

When the broker's close is the previous session's close on an exchange.

Parameters:

Name Type Description Default
exchange str

Canonical exchange.

required

Returns:

Type Description
str

One of the CLOSE_* policies.

Example usage

From example_1_a_new_broker_subclass.py
print(f'{exchange} close policy: {self.normalizer.close_policy(exchange)}')
From example_2_what_normalize_refuses.py
return InstrumentPlan(
    instrument_id='11111111-1111-5111-8111-000000000102',
    broker='imaginary',
    broker_token='2885',
    exchange=exchange,
    segment=segment,
    shape=shape,
    session=sessions.session_for(segment),
    lot_size=lot_size,
    multipliers=multipliers,
    price_decimals=2,
    negative_prices=shape != 'security' and exchange == 'mcx',
    close_policy=self.normalizer.close_policy(exchange),
    has_open_interest=shape != 'security',
    trusts_last_trade_time=True,
    trusts_exchange_time=True,
    identity_json='"instrument_id":"11111111-1111-5111-8111-000000000102"',
)
Source code in stock_brokers/instruments/ticks/base.py
def close_policy(self, exchange):
    """
    When the broker's `close` is the previous session's close on an exchange.

    Args:
        exchange (str): Canonical exchange.

    Returns:
        str: One of the CLOSE_* policies.
    """
    return self.CLOSE_POLICY.get(exchange, CLOSE_NEVER)

normalize

normalize(tick, plan, before_trading_close)

Apply a plan to one tick.

Parameters:

Name Type Description Default
tick dict

A tick as the broker's market feed published it.

required
plan InstrumentPlan

The compiled plan for the tick's token.

required
before_trading_close bool

Whether the tick was received before its session's close, for the close policy.

required

Returns:

Type Description
dict | None

The normalized values, or None when the tick carries no usable last price. Keys are last_price, last_quantity, average_price, volume, buy_quantity, sell_quantity, open, high, low, reported_close, oi, oi_day_high, oi_day_low, last_trade_time, exchange_time, bids and asks, where bids and asks are lists of (price, quantity, orders) best first.

Example usage

From example_2_what_normalize_refuses.py
values = self.normalizer.normalize(tick, share, True)
From example_2_what_normalize_refuses.py
values = self.normalizer.normalize(
    {
        'last_price': -12.5,
        'volume': 400,
        'oi': 900,
        'received_at': self.received_at,
    },
    future,
    True,
)
From example_2_what_normalize_refuses.py
outcome = self.normalizer.normalize(
    {
        'last_price': last_price,
        'received_at': self.received_at,
    },
    share,
    True,
)
Source code in stock_brokers/instruments/ticks/base.py
def normalize(self, tick, plan, before_trading_close):
    """
    Apply a plan to one tick.

    Args:
        tick (dict): A tick as the broker's market feed published it.
        plan (InstrumentPlan): The compiled plan for the tick's token.
        before_trading_close (bool): Whether the tick was received before its session's close, for the close policy.

    Returns:
        dict | None: The normalized values, or None when the tick carries no usable last price. Keys are last_price, last_quantity, average_price, volume, buy_quantity, sell_quantity, open, high, low, reported_close, oi, oi_day_high, oi_day_low, last_trade_time, exchange_time, bids and asks, where bids and asks are lists of (price, quantity, orders) best first.
    """
    decimals = plan.price_decimals
    negative = plan.negative_prices

    last_price = tick.get("last_price")
    if last_price is None or last_price == 0 or (last_price < 0 and not negative):
        return None
    last_price = round(last_price, decimals)

    multipliers = plan.multipliers
    received_at = tick.get("received_at") or 0.0
    ohlc = tick.get("ohlc") or {}

    close = None
    policy = plan.close_policy
    if policy == CLOSE_ALWAYS or (policy == CLOSE_BEFORE_SESSION_END and before_trading_close):
        close = _price(ohlc.get("close"), decimals, negative)

    if plan.has_open_interest:
        oi = _quantity(tick.get("oi"), multipliers["oi"])
        oi_day_high = _quantity(tick.get("oi_day_high") or None, multipliers["oi_day_high"])
        oi_day_low = _quantity(tick.get("oi_day_low") or None, multipliers["oi_day_low"])
    else:
        oi = oi_day_high = oi_day_low = None

    depth = tick.get("depth") or {}
    depth_multiplier = multipliers["depth_quantity"]

    return {
        "last_price": last_price,
        "last_quantity": _quantity(tick.get("last_quantity"), multipliers["last_quantity"]),
        "average_price": _price(tick.get("average_price"), decimals, negative),
        "volume": _quantity(tick.get("volume"), multipliers["volume"]),
        "buy_quantity": _quantity(tick.get("buy_quantity"), multipliers["buy_quantity"]),
        "sell_quantity": _quantity(tick.get("sell_quantity"), multipliers["sell_quantity"]),
        "open": _price(ohlc.get("open"), decimals, negative),
        "high": _price(ohlc.get("high"), decimals, negative),
        "low": _price(ohlc.get("low"), decimals, negative),
        "reported_close": close,
        "oi": oi,
        "oi_day_high": oi_day_high,
        "oi_day_low": oi_day_low,
        "last_trade_time": self._instant(tick.get("last_trade_time"), received_at) if plan.trusts_last_trade_time else None,
        "exchange_time": self._instant(tick.get("exchange_timestamp"), received_at) if plan.trusts_exchange_time else None,
        "bids": _levels(depth.get("buy"), decimals, negative, depth_multiplier),
        "asks": _levels(depth.get("sell"), decimals, negative, depth_multiplier),
    }

_instant

_instant(value, received_at)

A broker timestamp as a true epoch, or None when it is missing or implausible.

Brokers that stamp times on a shifted clock have that corrected in their market feed; what is left to catch here is a clock nobody has corrected yet, which shows as an instant days away from when the tick arrived.

Parameters:

Name Type Description Default
value float | int | None

Epoch seconds as the feed published them.

required
received_at float

When the feed decoded the tick.

required

Returns:

Type Description
float | None

The instant, or None.

Source code in stock_brokers/instruments/ticks/base.py
def _instant(self, value, received_at):
    """
    A broker timestamp as a true epoch, or None when it is missing or implausible.

    Brokers that stamp times on a shifted clock have that corrected in their market feed; what is
    left to catch here is a clock nobody has corrected yet, which shows as an instant days away
    from when the tick arrived.

    Args:
        value (float | int | None): Epoch seconds as the feed published them.
        received_at (float): When the feed decoded the tick.

    Returns:
        float | None: The instant, or None.
    """
    if not value:
        return None
    try:
        instant = float(value)
    except (TypeError, ValueError):
        return None
    if received_at and not (received_at - self.MAXIMUM_TIMESTAMP_AGE_SECONDS
                            <= instant <= received_at + self.MAXIMUM_TIMESTAMP_LEAD_SECONDS):
        return None
    return instant