Skip to content

base

Normalizing one broker's ticks into values comparable with every other broker's.

Every market feed already emits the same twenty keys, but the same key does not mean the same thing across brokers. The token and exchange are in the broker's own vocabulary; MCX quantities are lots at one broker and units at another; close is the previous session's close at one broker and today's close after the bell at another; some timestamps are missing or on a shifted clock; float32 prices carry noise in the last digits. A TickNormalizer subclass states those facts about one broker, and this base class applies them.

The work is split so that the per-tick cost is arithmetic only:

  • feed_key turns a tick's token into the broker token and canonical segments that unified.broker_mappings is searched with. It runs once per token per mapping date.
  • InstrumentPlan is compiled from that lookup - the instrument id, its identity, and every per-field multiplier and flag - once per token per mapping date.
  • normalize applies a plan to a tick, and runs on every tick.

Nothing here touches Redis or the database, so the same code serves the live service and is tested offline.

Classes:

Name Description
InstrumentPlan

Everything needed to normalize one broker token's ticks, decided once rather than per tick.

TickNormalizer

Base class for one broker's tick normalization.

Functions:

Name Description
family_segments

The canonical segments for a family of instruments on some exchanges.

identity_json

The identity part of a unified quote, serialized once for an instrument.

InstrumentPlan

InstrumentPlan(**values)

Everything needed to normalize one broker token's ticks, decided once rather than per tick.

Attributes:

Name Type Description
instrument_id str

The unified.instruments id.

broker str

The broker the token belongs to.

broker_token str

The token as stored in unified.broker_mappings.

exchange str

Canonical exchange.

segment str

Exchange-prefixed canonical segment.

shape str

"security", "future" or "option".

session Session

The window its ticks are accepted in.

lot_size int | None

Underlying units per lot, or None when unknown.

multipliers dict

Quantity field to the multiplier turning the broker's figure into units, or None when that field cannot be converted.

price_decimals int

Places prices are rounded to.

negative_prices bool

Whether a negative price is real (commodity futures have traded below zero).

close_policy str

One of CLOSE_ALWAYS, CLOSE_BEFORE_SESSION_END, CLOSE_NEVER.

has_open_interest bool

False for securities, whose open interest is meaningless.

trusts_last_trade_time bool

Whether the broker's last trade time is used.

trusts_exchange_time bool

Whether the broker's exchange timestamp is used.

identity_json str

The identity fields serialized once, without the enclosing braces, for splicing into every quote.

Build a plan from every attribute given by name.

Parameters:

Name Type Description Default
**values dict

One keyword argument per name in __slots__, each holding the value that attribute takes, as described in the class's Attributes: section.

{}

Raises:

Type Description
KeyError

A name in __slots__ was not given.

Example programs

examples/stock_brokers/instruments/ticks/base/InstrumentPlan/example_1_a_plan_for_one_future.py
"""Builds the plan for one MCX crude oil future and applies it to a tick.

An `InstrumentPlan` holds every decision about one broker token that does not change from tick to tick: which instrument it is, the window its ticks are accepted in, the multiplier that turns each quantity into units, how many places prices are rounded to, and which of the broker's fields are trusted. The live service compiles one per token through `TickResolver`; this program fills one in by hand, so every attribute is visible, and then hands it to `ZerodhaTickNormalizer.normalize` to show what the plan is for.

The identity part of a unified quote is serialized once into the plan's `identity_json`, so the per-tick path only splices it in. The tick has a fixed receipt time, so the output is identical on every run. Nothing here needs Redis, a database or a broker.

Run it from the project root:

    python examples/stock_brokers/instruments/ticks/base/InstrumentPlan/example_1_a_plan_for_one_future.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.utilities import sessions
from stock_brokers.instruments.ticks.zerodha import (
    ZerodhaTickNormalizer,
)

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


class FuturePlanExample:
    """Builds a plan for a crude oil future, prints it and normalizes one tick with it.

    Attributes:
        plan (InstrumentPlan): The plan being shown.
        received_at (float): When the tick was received, in epoch seconds.
    """

    def __init__(self):
        """Builds the plan with a 100-barrel lot and quantities reported in lots.

        Returns:
            None: This method returns nothing.
        """
        identity = {
            'exchange': 'mcx',
            'segment': 'mcx_commodity_futures',
            'shape': 'future',
            'symbol': 'CRUDEOIL26OCTFUT',
            'underlying_symbol': 'CRUDEOIL',
            'expiry_date': datetime.date(2026, 10, 19),
            'strike_price': None,
            'option_type': None,
        }
        multipliers = {}
        for field in base.QUANTITY_FIELDS:
            multipliers[field] = 100
        self.plan = InstrumentPlan(
            instrument_id='11111111-1111-5111-8111-000000000101',
            broker='zerodha',
            broker_token='5720583',
            exchange='mcx',
            segment='mcx_commodity_futures',
            shape='future',
            session=sessions.session_for('mcx_commodity_futures'),
            lot_size=100,
            multipliers=multipliers,
            price_decimals=2,
            negative_prices=True,
            close_policy=base.CLOSE_ALWAYS,
            has_open_interest=True,
            trusts_last_trade_time=True,
            trusts_exchange_time=True,
            identity_json=base.identity_json(
                '11111111-1111-5111-8111-000000000101',
                'zerodha',
                '5720583',
                identity,
                100,
            ),
        )
        received = datetime.datetime(2026, 9, 15, 22, 46, tzinfo=INDIA)
        self.received_at = received.timestamp()

    def run(self):
        """Prints every attribute of the plan, then a tick normalized with it.

        Returns:
            None: This method returns nothing.
        """
        plan = self.plan
        print(f'instrument_id: {plan.instrument_id}')
        print(f'broker: {plan.broker}')
        print(f'broker_token: {plan.broker_token}')
        print(f'exchange: {plan.exchange}')
        print(f'segment: {plan.segment}')
        print(f'shape: {plan.shape}')
        print(f'session: {plan.session}')
        print(f'lot_size: {plan.lot_size}')
        print(f'multipliers: {plan.multipliers}')
        print(f'price_decimals: {plan.price_decimals}')
        print(f'negative_prices: {plan.negative_prices}')
        print(f'close_policy: {plan.close_policy}')
        print(f'has_open_interest: {plan.has_open_interest}')
        print(f'trusts_last_trade_time: {plan.trusts_last_trade_time}')
        print(f'trusts_exchange_time: {plan.trusts_exchange_time}')
        print(f'identity_json: {plan.identity_json}')
        tick = {
            'instrument_token': 5720583,
            'last_price': 9731.0,
            'last_quantity': 2,
            'volume': 67958,
            'ohlc': {
                'open': 9702.0,
                'high': 9745.0,
                'low': 9688.0,
                'close': 9722.0,
            },
            'oi': 18095,
            'received_at': self.received_at,
        }
        values = ZerodhaTickNormalizer().normalize(tick, self.plan, True)
        print('A tick normalized with this plan:')
        print(f"  last_price {values['last_price']}, last_quantity {values['last_quantity']}, volume {values['volume']}, oi {values['oi']}, reported_close {values['reported_close']}")


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

It prints:

instrument_id: 11111111-1111-5111-8111-000000000101
broker: zerodha
broker_token: 5720583
exchange: mcx
segment: mcx_commodity_futures
shape: future
session: Session(calendar='commodity', opens=32400, evening_opens=61200, trading_close=86100, window_close=86399)
lot_size: 100
multipliers: {'last_quantity': 100, 'volume': 100, 'buy_quantity': 100, 'sell_quantity': 100, 'depth_quantity': 100, 'oi': 100, 'oi_day_high': 100, 'oi_day_low': 100}
price_decimals: 2
negative_prices: True
close_policy: always
has_open_interest: True
trusts_last_trade_time: True
trusts_exchange_time: True
identity_json: "instrument_id":"11111111-1111-5111-8111-000000000101","broker":"zerodha","broker_token":"5720583","exchange":"mcx","segment":"mcx_commodity_futures","shape":"future","symbol":"CRUDEOIL26OCTFUT","underlying_symbol":"CRUDEOIL","expiry_date":"2026-10-19","strike_price":null,"option_type":null,"lot_size":100
A tick normalized with this plan:
  last_price 9731.0, last_quantity 200, volume 6795800, oi 1809500, reported_close 9722.0
examples/stock_brokers/instruments/ticks/base/InstrumentPlan/example_2_a_missing_attribute.py
"""Shows that a plan must be given every attribute, and cannot take one it does not declare.

`InstrumentPlan` declares its attributes in `__slots__` and its constructor reads each of them from the keyword arguments by name. That is deliberate: a plan is built once per token and read on every tick, so a plan missing a field would fail on the hot path, far from the mistake. Leaving one out fails at construction instead, with a `KeyError` naming the missing attribute.

Because of `__slots__`, a plan also refuses an attribute that is not declared, which catches a misspelt name. This program builds an equity plan without `identity_json` and catches the error, then builds a complete one and tries to set a misspelt attribute on it. It needs no data store or broker.

Run it from the project root:

    python examples/stock_brokers/instruments/ticks/base/InstrumentPlan/example_2_a_missing_attribute.py
"""

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


class MissingAttributeExample:
    """Builds a plan without one attribute, then a complete one, and shows both refusals.

    Attributes:
        values (dict): The attributes of an NSE equity plan, without `identity_json`.
    """

    def __init__(self):
        """Prepares the attributes of a plan for RELIANCE on NSE, leaving one out.

        Returns:
            None: This method returns nothing.
        """
        multipliers = {}
        for field in base.QUANTITY_FIELDS:
            multipliers[field] = 1
        self.values = {
            'instrument_id': '11111111-1111-5111-8111-000000000102',
            'broker': 'zerodha',
            'broker_token': '738561',
            'exchange': 'nse',
            'segment': 'nse_equities',
            'shape': 'security',
            'session': sessions.session_for('nse_equities'),
            'lot_size': 1,
            'multipliers': multipliers,
            'price_decimals': 2,
            'negative_prices': False,
            'close_policy': base.CLOSE_ALWAYS,
            'has_open_interest': False,
            'trusts_last_trade_time': True,
            'trusts_exchange_time': True,
        }

    def run(self):
        """Prints the two refusals and the plan that was built.

        Returns:
            None: This method returns nothing.
        """
        try:
            InstrumentPlan(**self.values)
        except KeyError as error:
            print(f'Refused, missing attribute: {error}')
        self.values['identity_json'] = '"instrument_id":"11111111-1111-5111-8111-000000000102"'
        plan = InstrumentPlan(**self.values)
        print(f'Built a plan for {plan.segment} with lot size {plan.lot_size} and session {plan.session}')
        try:
            plan.lotsize = 5
        except AttributeError as error:
            print(f'Refused, undeclared attribute: {error}')


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

It prints:

Refused, missing attribute: 'identity_json'
Built a plan for nse_equities with lot size 1 and session Session(calendar='equity', opens=32400, evening_opens=None, trading_close=55800, window_close=57600)
Refused, undeclared attribute: 'InstrumentPlan' object has no attribute 'lotsize' and no __dict__ for setting new attributes
Source code in stock_brokers/instruments/ticks/base.py
def __init__(self, **values):
    """
    Build a plan from every attribute given by name.

    Args:
        **values (dict): One keyword argument per name in `__slots__`, each holding the value that attribute takes, as described in the class's `Attributes:` section.

    Raises:
        KeyError: A name in `__slots__` was not given.
    """
    for name in self.__slots__:
        setattr(self, name, values[name])

TickNormalizer

Base class for one broker's tick normalization.

A subclass implements feed_key and sets the class attributes that describe the broker. The defaults are the conservative reading: quantities in units, close not trusted as the previous close, both timestamps trusted.

Attributes:

Name Type Description
BROKER_NAME str

The broker this normalizer is for.

LOT_FIELDS dict

Exchange to the set of QUANTITY_FIELDS the broker reports in lots there.

BROKER_LOT_FIELDS dict

Exchange to the set of QUANTITY_FIELDS the broker reports as lots times its own lot size there.

CLOSE_POLICY dict

Exchange to one of the CLOSE_* policies. Exchanges absent use CLOSE_NEVER.

TRUSTS_LAST_TRADE_TIME bool

Whether the broker's last_trade_time is a true instant.

TRUSTS_EXCHANGE_TIME bool

Whether the broker's exchange_timestamp is a true instant.

Example programs

examples/stock_brokers/instruments/ticks/base/TickNormalizer/example_1_a_new_broker_subclass.py
"""Writes the normalizer for a new broker by subclassing `TickNormalizer`, and asks it the questions the resolver asks.

A broker's normalizer states facts about that broker and leaves the work to the base class. It implements `feed_key`, which turns the token on a tick into the broker token and canonical segments the mappings are searched with, and sets class attributes that say which quantities arrive in lots, when `close` is the previous close and which timestamps are true instants. The subclass here is for an imaginary broker whose feed spells tokens `NSE-2885` and `MCX-440001`, reports MCX volume and open interest in lots, and trusts its close on NSE only.

The program then calls the base class's own methods on it: `quantity_basis` and `close_policy`, which the resolver asks when it compiles a plan, and `tick_spelling`, whose default keeps a subscription token as it is. Everything is pure computation, so no data store or broker is involved.

Run it from the project root:

    python examples/stock_brokers/instruments/ticks/base/TickNormalizer/example_1_a_new_broker_subclass.py
"""

from stock_brokers.instruments.ticks import base
from stock_brokers.instruments.ticks.base import (
    TickNormalizer,
)


class ImaginaryBrokerTickNormalizer(TickNormalizer):
    """Normalizes ticks from an imaginary broker whose tokens are spelled `EXCHANGE-TOKEN`."""

    BROKER_NAME = 'imaginary'
    FAMILIES = {
        'NSE': 'cash_or_index',
        'NFO': 'derivative',
        'MCX': 'derivative',
    }
    EXCHANGES = {
        'NSE': 'nse',
        'NFO': 'nse',
        'MCX': 'mcx',
    }
    LOT_FIELDS = {
        'mcx': frozenset(
            [
                'volume',
                'oi',
            ]
        ),
    }
    CLOSE_POLICY = {
        'nse': base.CLOSE_ALWAYS,
    }
    TRUSTS_EXCHANGE_TIME = False

    def feed_key(self, instrument_token):
        """Reads the broker token and segments out of an `EXCHANGE-TOKEN` token.

        Args:
            instrument_token (str): The tick's token, such as `NSE-2885`.

        Returns:
            FeedKey | None: The key, or None for a token this normalizer cannot place.
        """
        exchange, _, token = str(instrument_token).partition('-')
        if exchange not in self.FAMILIES or not token:
            return None
        segments = base.family_segments(
            [
                self.EXCHANGES[exchange],
            ],
            self.FAMILIES[exchange],
        )
        return base.FeedKey(token, segments)


class NewBrokerSubclassExample:
    """Asks the imaginary broker's normalizer everything the resolver asks.

    Attributes:
        normalizer (ImaginaryBrokerTickNormalizer): The normalizer being shown.
    """

    def __init__(self):
        """Builds the normalizer.

        Returns:
            None: This method returns nothing.
        """
        self.normalizer = ImaginaryBrokerTickNormalizer()

    def run(self):
        """Prints feed keys, tick spellings, quantity bases and close policies.

        Returns:
            None: This method returns nothing.
        """
        tokens = [
            'NSE-2885',
            'MCX-440001',
            'LSE-1',
        ]
        for token in tokens:
            feed_key = self.normalizer.feed_key(token)
            if feed_key is None:
                print(f'{token}: cannot be placed')
                continue
            print(f'{token}: broker token {feed_key.broker_token}, {len(feed_key.segments)} segments starting with {feed_key.segments[0]}')
        print(f"Tick spelling of 'NSE-2885': {self.normalizer.tick_spelling('NSE-2885')!r}")
        fields = [
            'last_quantity',
            'volume',
            'oi',
        ]
        exchanges = [
            'nse',
            'mcx',
        ]
        for exchange in exchanges:
            for field in fields:
                basis = self.normalizer.quantity_basis(exchange, field)
                print(f'{exchange} {field}: {basis}')
            print(f'{exchange} close policy: {self.normalizer.close_policy(exchange)}')
        try:
            TickNormalizer().feed_key('NSE-2885')
        except NotImplementedError:
            print('The base class alone cannot place a token: feed_key raises NotImplementedError')


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

It prints:

NSE-2885: broker token 2885, 10 segments starting with nse_equities
MCX-440001: broker token 440001, 16 segments starting with mcx_fixed_income_futures
LSE-1: cannot be placed
Tick spelling of 'NSE-2885': 'NSE-2885'
nse last_quantity: units
nse volume: units
nse oi: units
nse close policy: always
mcx last_quantity: units
mcx volume: lots
mcx oi: lots
mcx close policy: never
The base class alone cannot place a token: feed_key raises NotImplementedError
examples/stock_brokers/instruments/ticks/base/TickNormalizer/example_2_what_normalize_refuses.py
"""Shows the rules the base class's `normalize` applies to every broker's ticks, by feeding it awkward ticks.

`TickNormalizer.normalize` is shared by every broker. Beyond multiplying quantities, it refuses values that cannot be right: a tick with no last price, or a zero or negative one where negative prices are impossible, gives `None`; a timestamp more than seven days before the tick was received, or more than five seconds after it, is dropped as a clock nobody corrected; an order book level without both a price and a quantity is dropped and the book is cut to five levels; and a quantity whose multiplier is unknown is left as `None` rather than guessed.

The program uses a tiny subclass for an imaginary broker, because `normalize` needs only a plan and the broker's class attributes. Two plans are built by hand: an NSE share, and an MCX future whose lot size is unknown. Every time is fixed, so the output is the same on every run, and nothing touches a data store or a broker.

Run it from the project root:

    python examples/stock_brokers/instruments/ticks/base/TickNormalizer/example_2_what_normalize_refuses.py
"""

import datetime
import zoneinfo

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

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


class ImaginaryBrokerTickNormalizer(TickNormalizer):
    """Normalizes ticks from an imaginary broker that reports MCX quantities in lots."""

    BROKER_NAME = 'imaginary'
    LOT_FIELDS = {
        'mcx': frozenset(base.QUANTITY_FIELDS),
    }

    def feed_key(self, instrument_token):
        """Places every token among NSE shares, which is all this example needs.

        Args:
            instrument_token (str): The tick's token.

        Returns:
            FeedKey: The key.
        """
        segments = (
            'nse_equities',
        )
        return base.FeedKey(str(instrument_token), segments)


class WhatNormalizeRefusesExample:
    """Normalizes a series of awkward ticks and prints what survives.

    Attributes:
        normalizer (ImaginaryBrokerTickNormalizer): The normalizer being shown.
        received_at (float): When every tick was received, in epoch seconds.
    """

    def __init__(self):
        """Builds the normalizer and fixes the receipt time.

        Returns:
            None: This method returns nothing.
        """
        self.normalizer = ImaginaryBrokerTickNormalizer()
        received = datetime.datetime(2026, 9, 15, 11, 5, tzinfo=INDIA)
        self.received_at = received.timestamp()

    def plan(self, exchange, segment, shape, lot_size):
        """Builds a plan the way the resolver would, using the normalizer's rules.

        Args:
            exchange (str): The canonical exchange.
            segment (str): The canonical segment.
            shape (str): "security", "future" or "option".
            lot_size (int | None): Units per lot, or None when unknown.

        Returns:
            InstrumentPlan: The plan.
        """
        multipliers = {}
        for field in base.QUANTITY_FIELDS:
            if self.normalizer.quantity_basis(exchange, field) == base.BASIS_UNITS:
                multipliers[field] = 1
            else:
                multipliers[field] = lot_size
        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"',
        )

    def run(self):
        """Prints what `normalize` makes of each awkward tick.

        Returns:
            None: This method returns nothing.
        """
        share = self.plan('nse', 'nse_equities', 'security', 1)
        impossible_prices = [
            None,
            0,
            -3.5,
        ]
        for last_price in impossible_prices:
            outcome = self.normalizer.normalize(
                {
                    'last_price': last_price,
                    'received_at': self.received_at,
                },
                share,
                True,
            )
            print(f'last_price {last_price!r} -> {outcome}')
        eight_days = 8 * 86400
        tick = {
            'last_price': 1262.70000001,
            'volume': 1885670.0,
            'last_trade_time': self.received_at - eight_days,
            'exchange_timestamp': self.received_at + 60,
            'received_at': self.received_at,
            'oi': 0,
            'depth': {
                'buy': [
                    {
                        'price': 1262.6,
                        'quantity': 120,
                        'orders': 4.0,
                    },
                    {
                        'price': 0.0,
                        'quantity': 0,
                        'orders': 0,
                    },
                    {
                        'price': 1262.4,
                        'quantity': 0,
                        'orders': 0,
                    },
                    {
                        'price': 1262.3,
                        'quantity': 50,
                        'orders': None,
                    },
                ],
            },
        }
        values = self.normalizer.normalize(tick, share, True)
        print(f"Share: last_price {values['last_price']}, volume {values['volume']}, oi {values['oi']}")
        print(f"Share: last_trade_time eight days old -> {values['last_trade_time']}, exchange_time a minute ahead -> {values['exchange_time']}")
        print(f"Share: bids {values['bids']}, asks {values['asks']}")
        print(f"Share: close policy {share.close_policy}, reported_close {values['reported_close']}")
        future = self.plan('mcx', 'mcx_commodity_futures', 'future', None)
        values = self.normalizer.normalize(
            {
                'last_price': -12.5,
                'volume': 400,
                'oi': 900,
                'received_at': self.received_at,
            },
            future,
            True,
        )
        print(f"Future with an unknown lot: last_price {values['last_price']}, volume {values['volume']}, oi {values['oi']}")


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

It prints:

last_price None -> None
last_price 0 -> None
last_price -3.5 -> None
Share: last_price 1262.7, volume 1885670, oi None
Share: last_trade_time eight days old -> None, exchange_time a minute ahead -> None
Share: bids [(1262.6, 120, 4), (1262.3, 50, None)], asks []
Share: close policy never, reported_close None
Future with an unknown lot: last_price -12.5, volume None, oi None

Methods:

Name Description
feed_key

Turn the token a feed puts on its ticks into what the broker's mappings are searched with.

tick_spelling

A subscription-set token spelled the way the feed spells it on ticks.

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.

feed_key

feed_key(instrument_token)

Turn the token a feed puts on its ticks into what the broker's mappings are searched with.

Parameters:

Name Type Description Default
instrument_token int | str

The tick's instrument_token.

required

Returns:

Type Description
FeedKey | None

The key, or None for a token this normalizer cannot place.

Example usage

From example_1_a_new_broker_subclass.py
feed_key = self.normalizer.feed_key(token)
From example_1_a_new_broker_subclass.py
TickNormalizer().feed_key('NSE-2885')
Source code in stock_brokers/instruments/ticks/base.py
def feed_key(self, instrument_token):
    """
    Turn the token a feed puts on its ticks into what the broker's mappings are searched with.

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

    Returns:
        FeedKey | None: The key, or None for a token this normalizer cannot place.
    """
    raise NotImplementedError

tick_spelling

tick_spelling(subscription_token)

A subscription-set token spelled the way the feed spells it on ticks.

Plans are filed under the tick's spelling, because that is what the per-tick path looks up. A subscription set holds strings in the spelling the broker's quote feed subscribes with, which is not always the tick's: Kite ticks carry an integer, Dhan ticks a numeric segment code.

Parameters:

Name Type Description Default
subscription_token str

A member of subscriptions_<broker>.

required

Returns:

Type Description
int | str | None

The tick's spelling, or None for a token this normalizer cannot place.

Example usage

From example_1_a_new_broker_subclass.py
print(f"Tick spelling of 'NSE-2885': {self.normalizer.tick_spelling('NSE-2885')!r}")
Source code in stock_brokers/instruments/ticks/base.py
def tick_spelling(self, subscription_token):
    """
    A subscription-set token spelled the way the feed spells it on ticks.

    Plans are filed under the tick's spelling, because that is what the per-tick path looks up.
    A subscription set holds strings in the spelling the broker's quote feed subscribes with, which is
    not always the tick's: Kite ticks carry an integer, Dhan ticks a numeric segment code.

    Args:
        subscription_token (str): A member of `subscriptions_<broker>`.

    Returns:
        int | str | None: The tick's spelling, or None for a token this normalizer cannot place.
    """
    return subscription_token

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

family_segments

family_segments(exchanges, family)

The canonical segments for a family of instruments on some exchanges.

Parameters:

Name Type Description Default
exchanges list[str]

Canonical exchanges, for example ["nse"].

required
family str

"cash", "index", "cash_or_index" or "derivative".

required

Returns:

Type Description
tuple[str]

Exchange-prefixed segment names.

Source code in stock_brokers/instruments/ticks/base.py
def family_segments(exchanges, family):
    """
    The canonical segments for a family of instruments on some exchanges.

    Args:
        exchanges (list[str]): Canonical exchanges, for example ["nse"].
        family (str): "cash", "index", "cash_or_index" or "derivative".

    Returns:
        tuple[str]: Exchange-prefixed segment names.
    """
    bare = {
        "cash": CASH_SEGMENTS,
        "index": INDEX_SEGMENTS,
        "cash_or_index": CASH_SEGMENTS + INDEX_SEGMENTS,
        "derivative": DERIVATIVE_SEGMENTS,
    }[family]
    return tuple(segments_of(list(exchanges), bare))

identity_json

identity_json(instrument_id, broker, broker_token, identity, lot_size)

The identity part of a unified quote, serialized once for an instrument.

Parameters:

Name Type Description Default
instrument_id str

The unified.instruments id.

required
broker str

The broker supplying the quote.

required
broker_token str

The broker's token for the instrument.

required
identity dict

The identity dict the mapping cache returns.

required
lot_size int | None

Underlying units per lot.

required

Returns:

Type Description
str

A JSON object's members without the enclosing braces.

Source code in stock_brokers/instruments/ticks/base.py
def identity_json(instrument_id, broker, broker_token, identity, lot_size):
    """
    The identity part of a unified quote, serialized once for an instrument.

    Args:
        instrument_id (str): The unified.instruments id.
        broker (str): The broker supplying the quote.
        broker_token (str): The broker's token for the instrument.
        identity (dict): The identity dict the mapping cache returns.
        lot_size (int | None): Underlying units per lot.

    Returns:
        str: A JSON object's members without the enclosing braces.
    """
    expiry = identity.get("expiry_date")
    strike = identity.get("strike_price")
    fields = {
        "instrument_id": instrument_id,
        "broker": broker,
        "broker_token": broker_token,
        "exchange": identity.get("exchange"),
        "segment": identity.get("segment"),
        "shape": identity.get("shape"),
        "symbol": identity.get("symbol"),
        "underlying_symbol": identity.get("underlying_symbol"),
        "expiry_date": expiry.isoformat() if hasattr(expiry, "isoformat") else expiry,
        "strike_price": float(strike) if strike is not None else None,
        "option_type": identity.get("option_type"),
        "lot_size": lot_size,
    }
    return json.dumps(fields, separators=(",", ":"))[1:-1]

_price

_price(value, decimals, negative)

A price rounded to the instrument's precision, or None for a missing, zero or impossible one.

Rounding is what removes float32 noise - 2232.6001 from a feed decoding single precision floats is 2232.60 - and fixes every broker's figure to the same number of places.

Source code in stock_brokers/instruments/ticks/base.py
def _price(value, decimals, negative):
    """
    A price rounded to the instrument's precision, or None for a missing, zero or impossible one.

    Rounding is what removes float32 noise - 2232.6001 from a feed decoding single precision floats
    is 2232.60 - and fixes every broker's figure to the same number of places.
    """
    if value is None or value == 0:
        return None
    if value < 0 and not negative:
        return None
    return round(value, decimals)

_quantity

_quantity(value, multiplier)

A quantity in whole underlying units, or None for a missing, negative or unconvertible one.

Source code in stock_brokers/instruments/ticks/base.py
def _quantity(value, multiplier):
    """
    A quantity in whole underlying units, or None for a missing, negative or unconvertible one.
    """
    if value is None or multiplier is None:
        return None
    if value.__class__ is not int:
        try:
            value = int(round(value))
        except (TypeError, ValueError):
            return None
    if value < 0:
        return None
    if multiplier.__class__ is int:
        return value * multiplier
    return int(round(value * multiplier))

_levels

_levels(levels, decimals, negative, multiplier)

Order book levels with a price and a quantity, best first, at most DEPTH_LEVELS.

Brokers pad an empty book with zero rows, some do and some do not; dropping every level without both a price and a quantity makes an empty level mean the same thing everywhere.

Source code in stock_brokers/instruments/ticks/base.py
def _levels(levels, decimals, negative, multiplier):
    """
    Order book levels with a price and a quantity, best first, at most DEPTH_LEVELS.

    Brokers pad an empty book with zero rows, some do and some do not; dropping every level without
    both a price and a quantity makes an empty level mean the same thing everywhere.
    """
    if not levels:
        return []
    kept = []
    for level in levels:
        price = level.get("price")
        quantity = level.get("quantity")
        if not price or not quantity:
            continue
        price = _price(price, decimals, negative)
        units = _quantity(quantity, multiplier)
        if price is None:
            continue
        orders = level.get("orders")
        if orders is not None and orders.__class__ is not int:
            try:
                orders = int(round(orders))
            except (TypeError, ValueError):
                orders = None
        kept.append((price, units, orders))
        if len(kept) == DEPTH_LEVELS:
            break
    return kept