Skip to content

contract_sizes

Decide, once per mapping date, how many quotation units one lot of every currency and commodity derivative is.

python -m stock_brokers.instruments.mapping.utilities.contract_sizes
python -m stock_brokers.instruments.mapping.utilities.contract_sizes --date 2026-09-15

The brokers' own lot_size figures cannot be compared on these markets, because each broker counts a lot in its own unit: on MCX, GOLD's lot is 1 at Zerodha (one lot), 1 at Kotak (one kilogram) and 100 at Groww (quotation units of 10 grams). Several brokers' instrument files also carry the exchange's contract size fields, and those are read here as independent sources, each in quotation units: Wisdom Capital's multiplier, Kotak's lot size times its general numerator over its general denominator (or its multiplier on the currency segment), Groww's commodity lot size, Shoonya's lot size times its multiplier on NSE currencies, and Stoxkart's lot size on the currency and NCDEX segments.

A contract is confirmed when at least two sources give a size and every one gives the same size. It is single_source when exactly one does, conflict when sources disagree or one source gives two different sizes for it, and no_source when none does. A source's file can list a contract twice, as Stoxkart's did for 34 nse currency options on 2026-09-28 with lots of 1000 and 2000; its sizes are then recorded together, such as 1000|2000, rather than one of them being kept by the order the rows happen to arrive in. tradeable is true for a confirmed contract, and for a single-source contract only on BSE currencies and NCDEX, where Stoxkart is the only broker that lists them.

On MCX a newly listed far-month contract often has one source only, because some brokers list new expiries later than others. Such a contract is sibling_confirmed and tradeable when every confirmed contract of the same underlying in the same segment on the date has one and the same size, and that size is the single source's figure. An underlying whose confirmed contracts already come in more than one size, as when the exchange has revised a lot size, gives no such answer, and its single-source contracts stay untradeable. The decision and every source's figure are written to unified.contract_sizes for the date, so an order reads a stored decision rather than making one.

Classes:

Name Description
ContractSizeSource

One broker instrument file field, or combination of fields, that states a contract's size in quotation units.

WisdomCapitalMultiplierSource

Wisdom Capital's XTS multiplier, which is the contract size in quotation units on MCX and NSE commodities.

KotakContractSizeSource

Kotak's lot size in its trading unit, converted to quotation units with its general numerator and denominator, and scaled by its multiplier where it gives one, as on currencies.

GrowwLotSizeSource

Groww's lot size on its commodity segment, which Groww gives in quotation units.

ShoonyaCurrencySource

Shoonya's lot size times its multiplier on NSE currencies, where it gives a lot size of 1 and a multiplier of the contract size.

StoxkartLotSizeSource

Stoxkart's lot size on the NSE and BSE currency segments and NCDEX.

ContractSizeDecision

The rule that turns the sources' figures for one contract into a stored decision.

ContractSizeResolver

Reads every source for a mapping date, decides each contract's size and writes the decisions.

ContractSizeCommand

The command line: decide a date's contract sizes and print how many contracts each segment has in each status.

ContractSizeSource

One broker instrument file field, or combination of fields, that states a contract's size in quotation units.

A subclass sets the class attributes and units_expression. The source is read by joining the date's broker mappings to that broker's raw snapshot on the broker token, restricted to the raw exchange labels the source is trusted on, because a token is not unique across a broker's exchanges.

Attributes:

Name Type Description
NAME str

The source's name, as it is recorded in unified.contract_sizes.sources.

BROKER str

The broker whose snapshot is read.

TOKEN_COLUMN str

The snapshot column holding the broker token.

EXCHANGE_COLUMN str

The snapshot column holding the exchange label.

EXCHANGES list

The exchange labels the source is read on.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeSource/example_1_writing_a_new_source.py
"""Writes a new contract size source by subclassing `ContractSizeSource` and prints the query it builds.

A source names one broker, the snapshot columns holding its token and exchange label, the exchange labels it is trusted on, and an SQL expression for the size in quotation units. This program invents a source that reads Dhan's `lot_size` column on MCX, which is not one of the project's sources, to show how little a subclass has to say.

`numeric` wraps a snapshot column in a guard that turns anything that is not a number into null, so a broker file holding text in a numeric column cannot break the query. `statement` then joins the date's broker mappings to the broker's raw snapshot on the token and the download date, and keeps only live contracts in the requested segments on the trusted exchange labels. Nothing is executed here, so no database is needed.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeSource/example_1_writing_a_new_source.py
"""

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeSource,
)


class DhanLotSizeSource(ContractSizeSource):
    """An illustrative source reading Dhan's `lot_size` column on MCX."""

    NAME = 'dhan_lot_size'
    BROKER = 'dhan'
    TOKEN_COLUMN = 'security_id'
    EXCHANGE_COLUMN = 'exch_id'
    EXCHANGES = [
        'MCX',
    ]

    def units_expression(self):
        """The `lot_size` column, read as a number.

        Returns:
            str: The SQL expression.
        """
        return self.numeric('lot_size')


class WritingANewSourceExample:
    """Builds the illustrative source and prints its settings, its expressions and its query.

    Attributes:
        source (DhanLotSizeSource): The source being shown.
    """

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

        Returns:
            None: This method returns nothing.
        """
        self.source = DhanLotSizeSource()

    def run(self):
        """Prints the source's settings, the numeric guard, the size expression and the full query.

        Returns:
            None: This method returns nothing.
        """
        print(f'Name: {self.source.NAME}')
        print(f'Broker: {self.source.BROKER}')
        print(f'Trusted exchange labels: {self.source.EXCHANGES}')
        print(f'Numeric guard on multiplier: {self.source.numeric("multiplier")}')
        print(f'Size expression: {self.source.units_expression()}')
        print('Query:')
        print(str(self.source.statement()))


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

It prints:

Name: dhan_lot_size
Broker: dhan
Trusted exchange labels: ['MCX']
Numeric guard on multiplier: (CASE WHEN raw."multiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."multiplier"::text::numeric END)
Size expression: (CASE WHEN raw."lot_size"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lot_size"::text::numeric END)
Query:
SELECT m.instrument_id::text AS instrument_id, (CASE WHEN raw."lot_size"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lot_size"::text::numeric END) AS units FROM unified.broker_mappings m JOIN unified.instruments i ON i.instrument_id = m.instrument_id JOIN dhan.instruments raw ON raw."security_id"::text = m.broker_token AND raw.download_date = m.mapping_date WHERE m.broker = :broker AND m.mapping_date = :mapping_date AND i.segment = ANY(:segments) AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) AND raw."exch_id"::text = ANY(:exchanges)
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeSource/example_2_reading_rows_that_disagree.py
"""Reads a source's rows and shows which sizes it keeps, and what the bare base class does.

`read` runs the source's query on an open connection and gathers, for each contract, the set of distinct sizes its rows give. A row whose size is missing, zero or negative is left out, and a contract listed twice with two different sizes keeps both, which is how the resolver later notices a source contradicting itself.

The connection here is a small stand-in that returns five made-up rows and remembers the parameters it was given, so the program needs no database. The rows copy a case seen in Stoxkart's file on 2026-09-28, where one USDINR option was listed with lots of 1000 and 2000.

The base class names no expression of its own, so calling `units_expression` on it raises `NotImplementedError`; the program catches that to show it.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeSource/example_2_reading_rows_that_disagree.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeSource,
    StoxkartLotSizeSource,
)


class StandInConnection:
    """A stand-in for a database connection that returns fixed rows and records the parameters of each query.

    Attributes:
        rows (list): The rows every query returns.
        parameters (dict | None): The parameters of the last query, or None before any.
    """

    def __init__(self, rows):
        """Holds the rows to return.

        Args:
            rows (list): The rows every query returns.

        Returns:
            None: This method returns nothing.
        """
        self.rows = rows
        self.parameters = None

    def execute(self, statement, parameters):
        """Records the parameters and returns the fixed rows.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The query, which is not run.
            parameters (dict): The query's parameters.

        Returns:
            list: The fixed rows.
        """
        self.parameters = parameters
        return self.rows


class ReadingRowsThatDisagreeExample:
    """Reads five stand-in rows through Stoxkart's source and tries the base class's expression.

    Attributes:
        source (StoxkartLotSizeSource): The source being read.
        connection (StandInConnection): The stand-in connection.
    """

    def __init__(self):
        """Builds the source and a connection holding five rows.

        Returns:
            None: This method returns nothing.
        """
        self.source = StoxkartLotSizeSource()
        self.connection = StandInConnection([
            types.SimpleNamespace(
                instrument_id='usdinr-option',
                units='1000',
            ),
            types.SimpleNamespace(
                instrument_id='usdinr-option',
                units='2000',
            ),
            types.SimpleNamespace(
                instrument_id='eurinr-future',
                units='1000.00',
            ),
            types.SimpleNamespace(
                instrument_id='gbpinr-future',
                units=None,
            ),
            types.SimpleNamespace(
                instrument_id='jpyinr-future',
                units='0',
            ),
        ])

    def run(self):
        """Prints the sizes read, the query parameters and the base class's refusal.

        Returns:
            None: This method returns nothing.
        """
        sizes = self.source.read(
            self.connection,
            datetime.date(2026, 9, 28),
            [
                'nse_currency_options',
                'nse_currency_futures',
            ],
        )
        for instrument_id in sorted(sizes):
            texts = []
            for size in sorted(sizes[instrument_id]):
                texts.append(format(size, 'f'))
            print(f'{instrument_id}: {texts}')
        print(f'Broker queried: {self.connection.parameters["broker"]}')
        print(f'Exchange labels queried: {self.connection.parameters["exchanges"]}')
        base_source = ContractSizeSource()
        try:
            base_source.units_expression()
        except NotImplementedError:
            print('The base class has no size expression of its own.')


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

It prints:

eurinr-future: ['1000']
usdinr-option: ['1000', '2000']
Broker queried: stoxkart
Exchange labels queried: ['NSECD', 'BSECD', 'NCDEX']
The base class has no size expression of its own.

Methods:

Name Description
numeric

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

units_expression

The SQL expression giving the contract's size in quotation units.

statement

The query reading this source for every live contract in the given segments on a date.

read

Reads this source's size for every live contract it lists.

numeric

numeric(column)

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

Parameters:

Name Type Description Default
column str

The snapshot column.

required

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_writing_a_new_source.py
print(f'Numeric guard on multiplier: {self.source.numeric("multiplier")}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def numeric(self, column):
    """
    An SQL expression reading a snapshot column as a number, or null when it does not hold one.

    Args:
        column (str): The snapshot column.

    Returns:
        str: The SQL expression.
    """
    return (
        f"(CASE WHEN raw.\"{column}\"::text ~ '^-?[0-9]+(\\.[0-9]+)?$' "
        f"THEN raw.\"{column}\"::text::numeric END)"
    )

units_expression

units_expression()

The SQL expression giving the contract's size in quotation units.

Returns:

Type Description
str

The SQL expression.

Raises:

Type Description
NotImplementedError

Always, in the base class.

Example usage

From example_1_writing_a_new_source.py
print(f'Size expression: {self.source.units_expression()}')
From example_2_reading_rows_that_disagree.py
base_source.units_expression()
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def units_expression(self):
    """
    The SQL expression giving the contract's size in quotation units.

    Returns:
        str: The SQL expression.

    Raises:
        NotImplementedError: Always, in the base class.
    """
    raise NotImplementedError

statement

statement()

The query reading this source for every live contract in the given segments on a date.

Returns:

Type Description
TextClause

The query, taking mapping_date, segments and exchanges.

Example usage

From example_1_writing_a_new_source.py
print(str(self.source.statement()))
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def statement(self):
    """
    The query reading this source for every live contract in the given segments on a date.

    Returns:
        sqlalchemy.sql.elements.TextClause: The query, taking `mapping_date`, `segments` and `exchanges`.
    """
    return text(
        f"SELECT m.instrument_id::text AS instrument_id, {self.units_expression()} AS units "
        f"FROM {tables.BROKER_MAPPINGS} m "
        f"JOIN {tables.MASTER} i ON i.instrument_id = m.instrument_id "
        f"JOIN {self.BROKER}.instruments raw "
        f"ON raw.\"{self.TOKEN_COLUMN}\"::text = m.broker_token "
        "AND raw.download_date = m.mapping_date "
        "WHERE m.broker = :broker "
        "AND m.mapping_date = :mapping_date "
        "AND i.segment = ANY(:segments) "
        "AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) "
        f"AND raw.\"{self.EXCHANGE_COLUMN}\"::text = ANY(:exchanges)"
    )

read

read(connection, mapping_date, segments)

Reads this source's size for every live contract it lists.

Parameters:

Name Type Description Default
connection Connection

An open connection.

required
mapping_date date

The mapping date.

required
segments list

The exchange-prefixed segments to read.

required

Returns:

Type Description
dict

Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.

Example usage

From example_2_reading_rows_that_disagree.py
sizes = self.source.read(
    self.connection,
    datetime.date(2026, 9, 28),
    [
        'nse_currency_options',
        'nse_currency_futures',
    ],
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def read(self, connection, mapping_date, segments):
    """
    Reads this source's size for every live contract it lists.

    Args:
        connection (sqlalchemy.engine.Connection): An open connection.
        mapping_date (datetime.date): The mapping date.
        segments (list): The exchange-prefixed segments to read.

    Returns:
        dict: Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.
    """
    rows = connection.execute(
        self.statement(),
        {
            "broker": self.BROKER,
            "mapping_date": mapping_date,
            "segments": segments,
            "exchanges": self.EXCHANGES,
        },
    )
    sizes = {}
    for row in rows:
        if row.units is None:
            continue
        units = decimal.Decimal(row.units)
        if units <= 0:
            continue
        sizes.setdefault(row.instrument_id, set()).add(units.normalize())
    return sizes

WisdomCapitalMultiplierSource

Bases: ContractSizeSource

Wisdom Capital's XTS multiplier, which is the contract size in quotation units on MCX and NSE commodities.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/WisdomCapitalMultiplierSource/example_1_the_size_expression.py
"""Prints the SQL expression Wisdom Capital's XTS `multiplier` column is read with, and the query around it.

On MCX and NSE commodities that column is already the contract size in quotation units, so the expression is only the numeric guard around it.

The program only builds the expression and the query text, so it needs no database. The query joins the date's broker mappings to the Wisdom Capital snapshot on the broker token and the download date, and keeps only the exchange labels this source is trusted on.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/WisdomCapitalMultiplierSource/example_1_the_size_expression.py
"""

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    WisdomCapitalMultiplierSource,
)


class ReadingTheExpressionExample:
    """Prints the source's settings, its size expression and its query.

    Attributes:
        source (WisdomCapitalMultiplierSource): The source being shown.
    """

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

        Returns:
            None: This method returns nothing.
        """
        self.source = WisdomCapitalMultiplierSource()

    def run(self):
        """Prints the name, the broker, the trusted exchange labels, the expression and the query.

        Returns:
            None: This method returns nothing.
        """
        print(f'Name: {self.source.NAME}')
        print(f'Broker: {self.source.BROKER}')
        print(f'Token column: {self.source.TOKEN_COLUMN}')
        print(f'Exchange column: {self.source.EXCHANGE_COLUMN}')
        print(f'Trusted exchange labels: {self.source.EXCHANGES}')
        print(f'Size expression: {self.source.units_expression()}')
        print('Query:')
        print(str(self.source.statement()))


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

It prints:

Name: wisdom_capital_multiplier
Broker: wisdom_capital
Token column: exchangeinstrumentid
Exchange column: exchangesegment
Trusted exchange labels: ['MCXFO', 'NSECO']
Size expression: (CASE WHEN raw."multiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."multiplier"::text::numeric END)
Query:
SELECT m.instrument_id::text AS instrument_id, (CASE WHEN raw."multiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."multiplier"::text::numeric END) AS units FROM unified.broker_mappings m JOIN unified.instruments i ON i.instrument_id = m.instrument_id JOIN wisdom_capital.instruments raw ON raw."exchangeinstrumentid"::text = m.broker_token AND raw.download_date = m.mapping_date WHERE m.broker = :broker AND m.mapping_date = :mapping_date AND i.segment = ANY(:segments) AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) AND raw."exchangesegment"::text = ANY(:exchanges)
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/WisdomCapitalMultiplierSource/example_2_reading_sizes.py
"""Reads Wisdom Capital's XTS `multiplier` column for a few contracts through a stand-in connection.

`read` runs the source's query and returns, for each contract, the set of distinct sizes its rows give, as decimals with trailing zeros removed. The rows copy real MCX sizes: GOLD at 100 units of 10 grams, CRUDEOIL at 100 barrels and NATURALGAS at 1250 mmBtu. One option row has an empty multiplier, and the source leaves it out rather than guessing.

The connection is a small stand-in that returns the rows it was built with and records the parameters of the query, so the program needs no database. The parameters show that the query is limited to Wisdom Capital's rows on the trusted exchange labels.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/WisdomCapitalMultiplierSource/example_2_reading_sizes.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    WisdomCapitalMultiplierSource,
)


class StandInConnection:
    """A stand-in for a database connection that returns fixed rows and records the parameters of each query.

    Attributes:
        rows (list): The rows every query returns.
        parameters (dict | None): The parameters of the last query, or None before any.
    """

    def __init__(self, rows):
        """Holds the rows to return.

        Args:
            rows (list): The rows every query returns.

        Returns:
            None: This method returns nothing.
        """
        self.rows = rows
        self.parameters = None

    def execute(self, statement, parameters):
        """Records the parameters and returns the fixed rows.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The query, which is not run.
            parameters (dict): The query's parameters.

        Returns:
            list: The fixed rows.
        """
        self.parameters = parameters
        return self.rows


class ReadingSizesExample:
    """Reads the stand-in rows through the source and prints each contract's sizes.

    Attributes:
        source (WisdomCapitalMultiplierSource): The source being read.
        connection (StandInConnection): The stand-in connection.
    """

    def __init__(self):
        """Builds the source and a connection holding the rows.

        Returns:
            None: This method returns nothing.
        """
        self.source = WisdomCapitalMultiplierSource()
        self.connection = StandInConnection([
            types.SimpleNamespace(
                instrument_id='gold-future-oct',
                units='100',
            ),
            types.SimpleNamespace(
                instrument_id='gold-future-dec',
                units='100',
            ),
            types.SimpleNamespace(
                instrument_id='crudeoil-future-oct',
                units='100',
            ),
            types.SimpleNamespace(
                instrument_id='naturalgas-future-oct',
                units='1250',
            ),
            types.SimpleNamespace(
                instrument_id='silver-option',
                units=None,
            ),
        ])

    def run(self):
        """Prints the sizes read and the parameters the query was given.

        Returns:
            None: This method returns nothing.
        """
        sizes = self.source.read(
            self.connection,
            datetime.date(2026, 9, 28),
            [
                'mcx_commodity_futures',
                'mcx_commodity_options',
            ],
        )
        for instrument_id in sorted(sizes):
            texts = []
            for size in sorted(sizes[instrument_id]):
                texts.append(format(size, 'f'))
            print(f'{instrument_id}: {texts}')
        print(f'Broker queried: {self.connection.parameters["broker"]}')
        print(f'Mapping date: {self.connection.parameters["mapping_date"]}')
        print(f'Segments: {self.connection.parameters["segments"]}')
        print(f'Exchange labels: {self.connection.parameters["exchanges"]}')


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

It prints:

crudeoil-future-oct: ['100']
gold-future-dec: ['100']
gold-future-oct: ['100']
naturalgas-future-oct: ['1250']
Broker queried: wisdom_capital
Mapping date: 2026-09-28
Segments: ['mcx_commodity_futures', 'mcx_commodity_options']
Exchange labels: ['MCXFO', 'NSECO']

Methods:

Name Description
units_expression

The multiplier column.

numeric

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

statement

The query reading this source for every live contract in the given segments on a date.

read

Reads this source's size for every live contract it lists.

units_expression

units_expression()

The multiplier column.

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_the_size_expression.py
print(f'Size expression: {self.source.units_expression()}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def units_expression(self):
    """
    The `multiplier` column.

    Returns:
        str: The SQL expression.
    """
    return self.numeric("multiplier")

numeric

numeric(column)

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

Parameters:

Name Type Description Default
column str

The snapshot column.

required

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_writing_a_new_source.py
print(f'Numeric guard on multiplier: {self.source.numeric("multiplier")}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def numeric(self, column):
    """
    An SQL expression reading a snapshot column as a number, or null when it does not hold one.

    Args:
        column (str): The snapshot column.

    Returns:
        str: The SQL expression.
    """
    return (
        f"(CASE WHEN raw.\"{column}\"::text ~ '^-?[0-9]+(\\.[0-9]+)?$' "
        f"THEN raw.\"{column}\"::text::numeric END)"
    )

statement

statement()

The query reading this source for every live contract in the given segments on a date.

Returns:

Type Description
TextClause

The query, taking mapping_date, segments and exchanges.

Example usage

From example_1_writing_a_new_source.py
print(str(self.source.statement()))
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def statement(self):
    """
    The query reading this source for every live contract in the given segments on a date.

    Returns:
        sqlalchemy.sql.elements.TextClause: The query, taking `mapping_date`, `segments` and `exchanges`.
    """
    return text(
        f"SELECT m.instrument_id::text AS instrument_id, {self.units_expression()} AS units "
        f"FROM {tables.BROKER_MAPPINGS} m "
        f"JOIN {tables.MASTER} i ON i.instrument_id = m.instrument_id "
        f"JOIN {self.BROKER}.instruments raw "
        f"ON raw.\"{self.TOKEN_COLUMN}\"::text = m.broker_token "
        "AND raw.download_date = m.mapping_date "
        "WHERE m.broker = :broker "
        "AND m.mapping_date = :mapping_date "
        "AND i.segment = ANY(:segments) "
        "AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) "
        f"AND raw.\"{self.EXCHANGE_COLUMN}\"::text = ANY(:exchanges)"
    )

read

read(connection, mapping_date, segments)

Reads this source's size for every live contract it lists.

Parameters:

Name Type Description Default
connection Connection

An open connection.

required
mapping_date date

The mapping date.

required
segments list

The exchange-prefixed segments to read.

required

Returns:

Type Description
dict

Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.

Example usage

From example_2_reading_rows_that_disagree.py
sizes = self.source.read(
    self.connection,
    datetime.date(2026, 9, 28),
    [
        'nse_currency_options',
        'nse_currency_futures',
    ],
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def read(self, connection, mapping_date, segments):
    """
    Reads this source's size for every live contract it lists.

    Args:
        connection (sqlalchemy.engine.Connection): An open connection.
        mapping_date (datetime.date): The mapping date.
        segments (list): The exchange-prefixed segments to read.

    Returns:
        dict: Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.
    """
    rows = connection.execute(
        self.statement(),
        {
            "broker": self.BROKER,
            "mapping_date": mapping_date,
            "segments": segments,
            "exchanges": self.EXCHANGES,
        },
    )
    sizes = {}
    for row in rows:
        if row.units is None:
            continue
        units = decimal.Decimal(row.units)
        if units <= 0:
            continue
        sizes.setdefault(row.instrument_id, set()).add(units.normalize())
    return sizes

KotakContractSizeSource

Bases: ContractSizeSource

Kotak's lot size in its trading unit, converted to quotation units with its general numerator and denominator, and scaled by its multiplier where it gives one, as on currencies.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/KotakContractSizeSource/example_1_the_size_expression.py
"""Prints the SQL expression Kotak's lot size converted to quotation units is read with, and the query around it.

Kotak counts GOLD in kilograms, so its lot size of 1 is multiplied by its general numerator and divided by its general denominator to reach the 100 units of 10 grams the exchange quotes in; on currencies it gives a multiplier as well, which scales the lot size when it is positive.

The program only builds the expression and the query text, so it needs no database. The query joins the date's broker mappings to the Kotak snapshot on the broker token and the download date, and keeps only the exchange labels this source is trusted on.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/KotakContractSizeSource/example_1_the_size_expression.py
"""

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    KotakContractSizeSource,
)


class ReadingTheExpressionExample:
    """Prints the source's settings, its size expression and its query.

    Attributes:
        source (KotakContractSizeSource): The source being shown.
    """

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

        Returns:
            None: This method returns nothing.
        """
        self.source = KotakContractSizeSource()

    def run(self):
        """Prints the name, the broker, the trusted exchange labels, the expression and the query.

        Returns:
            None: This method returns nothing.
        """
        print(f'Name: {self.source.NAME}')
        print(f'Broker: {self.source.BROKER}')
        print(f'Token column: {self.source.TOKEN_COLUMN}')
        print(f'Exchange column: {self.source.EXCHANGE_COLUMN}')
        print(f'Trusted exchange labels: {self.source.EXCHANGES}')
        print(f'Size expression: {self.source.units_expression()}')
        print('Query:')
        print(str(self.source.statement()))


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

It prints:

Name: kotak_contract_size
Broker: kotak
Token column: psymbol
Exchange column: pexchseg
Trusted exchange labels: ['mcx_fo', 'nse_com', 'cde_fo']
Size expression: ((CASE WHEN raw."llotsize"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."llotsize"::text::numeric END) * (CASE WHEN (CASE WHEN raw."lmultiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lmultiplier"::text::numeric END) > 0 THEN (CASE WHEN raw."lmultiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lmultiplier"::text::numeric END) ELSE 1 END) * (CASE WHEN raw."dgennum"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."dgennum"::text::numeric END) / NULLIF((CASE WHEN raw."dgenden"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."dgenden"::text::numeric END), 0))
Query:
SELECT m.instrument_id::text AS instrument_id, ((CASE WHEN raw."llotsize"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."llotsize"::text::numeric END) * (CASE WHEN (CASE WHEN raw."lmultiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lmultiplier"::text::numeric END) > 0 THEN (CASE WHEN raw."lmultiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lmultiplier"::text::numeric END) ELSE 1 END) * (CASE WHEN raw."dgennum"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."dgennum"::text::numeric END) / NULLIF((CASE WHEN raw."dgenden"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."dgenden"::text::numeric END), 0)) AS units FROM unified.broker_mappings m JOIN unified.instruments i ON i.instrument_id = m.instrument_id JOIN kotak.instruments raw ON raw."psymbol"::text = m.broker_token AND raw.download_date = m.mapping_date WHERE m.broker = :broker AND m.mapping_date = :mapping_date AND i.segment = ANY(:segments) AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) AND raw."pexchseg"::text = ANY(:exchanges)
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/KotakContractSizeSource/example_2_reading_sizes.py
"""Reads Kotak's lot size converted to quotation units for a few contracts through a stand-in connection.

`read` runs the source's query and returns, for each contract, the set of distinct sizes its rows give, as decimals with trailing zeros removed. The stand-in rows hold the size as the database would compute it from that expression, including trailing zeros, which `read` normalises away. A negative figure, which a broken denominator could produce, is left out.

The connection is a small stand-in that returns the rows it was built with and records the parameters of the query, so the program needs no database. The parameters show that the query is limited to Kotak's rows on the trusted exchange labels.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/KotakContractSizeSource/example_2_reading_sizes.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    KotakContractSizeSource,
)


class StandInConnection:
    """A stand-in for a database connection that returns fixed rows and records the parameters of each query.

    Attributes:
        rows (list): The rows every query returns.
        parameters (dict | None): The parameters of the last query, or None before any.
    """

    def __init__(self, rows):
        """Holds the rows to return.

        Args:
            rows (list): The rows every query returns.

        Returns:
            None: This method returns nothing.
        """
        self.rows = rows
        self.parameters = None

    def execute(self, statement, parameters):
        """Records the parameters and returns the fixed rows.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The query, which is not run.
            parameters (dict): The query's parameters.

        Returns:
            list: The fixed rows.
        """
        self.parameters = parameters
        return self.rows


class ReadingSizesExample:
    """Reads the stand-in rows through the source and prints each contract's sizes.

    Attributes:
        source (KotakContractSizeSource): The source being read.
        connection (StandInConnection): The stand-in connection.
    """

    def __init__(self):
        """Builds the source and a connection holding the rows.

        Returns:
            None: This method returns nothing.
        """
        self.source = KotakContractSizeSource()
        self.connection = StandInConnection([
            types.SimpleNamespace(
                instrument_id='gold-future-oct',
                units='100.0000',
            ),
            types.SimpleNamespace(
                instrument_id='usdinr-future-oct',
                units='1000.00',
            ),
            types.SimpleNamespace(
                instrument_id='silver-future-dec',
                units='30.000',
            ),
            types.SimpleNamespace(
                instrument_id='copper-future-oct',
                units='-1',
            ),
        ])

    def run(self):
        """Prints the sizes read and the parameters the query was given.

        Returns:
            None: This method returns nothing.
        """
        sizes = self.source.read(
            self.connection,
            datetime.date(2026, 9, 28),
            [
                'mcx_commodity_futures',
                'nse_currency_futures',
            ],
        )
        for instrument_id in sorted(sizes):
            texts = []
            for size in sorted(sizes[instrument_id]):
                texts.append(format(size, 'f'))
            print(f'{instrument_id}: {texts}')
        print(f'Broker queried: {self.connection.parameters["broker"]}')
        print(f'Mapping date: {self.connection.parameters["mapping_date"]}')
        print(f'Segments: {self.connection.parameters["segments"]}')
        print(f'Exchange labels: {self.connection.parameters["exchanges"]}')


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

It prints:

gold-future-oct: ['100']
silver-future-dec: ['30']
usdinr-future-oct: ['1000']
Broker queried: kotak
Mapping date: 2026-09-28
Segments: ['mcx_commodity_futures', 'nse_currency_futures']
Exchange labels: ['mcx_fo', 'nse_com', 'cde_fo']

Methods:

Name Description
units_expression

llotsize times lmultiplier when it is positive, times dgennum, over dgenden.

numeric

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

statement

The query reading this source for every live contract in the given segments on a date.

read

Reads this source's size for every live contract it lists.

units_expression

units_expression()

llotsize times lmultiplier when it is positive, times dgennum, over dgenden.

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_the_size_expression.py
print(f'Size expression: {self.source.units_expression()}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def units_expression(self):
    """
    `llotsize` times `lmultiplier` when it is positive, times `dgennum`, over `dgenden`.

    Returns:
        str: The SQL expression.
    """
    lot_size = self.numeric("llotsize")
    multiplier = self.numeric("lmultiplier")
    numerator = self.numeric("dgennum")
    denominator = self.numeric("dgenden")
    return (
        f"({lot_size} "
        f"* (CASE WHEN {multiplier} > 0 THEN {multiplier} ELSE 1 END) "
        f"* {numerator} / NULLIF({denominator}, 0))"
    )

numeric

numeric(column)

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

Parameters:

Name Type Description Default
column str

The snapshot column.

required

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_writing_a_new_source.py
print(f'Numeric guard on multiplier: {self.source.numeric("multiplier")}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def numeric(self, column):
    """
    An SQL expression reading a snapshot column as a number, or null when it does not hold one.

    Args:
        column (str): The snapshot column.

    Returns:
        str: The SQL expression.
    """
    return (
        f"(CASE WHEN raw.\"{column}\"::text ~ '^-?[0-9]+(\\.[0-9]+)?$' "
        f"THEN raw.\"{column}\"::text::numeric END)"
    )

statement

statement()

The query reading this source for every live contract in the given segments on a date.

Returns:

Type Description
TextClause

The query, taking mapping_date, segments and exchanges.

Example usage

From example_1_writing_a_new_source.py
print(str(self.source.statement()))
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def statement(self):
    """
    The query reading this source for every live contract in the given segments on a date.

    Returns:
        sqlalchemy.sql.elements.TextClause: The query, taking `mapping_date`, `segments` and `exchanges`.
    """
    return text(
        f"SELECT m.instrument_id::text AS instrument_id, {self.units_expression()} AS units "
        f"FROM {tables.BROKER_MAPPINGS} m "
        f"JOIN {tables.MASTER} i ON i.instrument_id = m.instrument_id "
        f"JOIN {self.BROKER}.instruments raw "
        f"ON raw.\"{self.TOKEN_COLUMN}\"::text = m.broker_token "
        "AND raw.download_date = m.mapping_date "
        "WHERE m.broker = :broker "
        "AND m.mapping_date = :mapping_date "
        "AND i.segment = ANY(:segments) "
        "AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) "
        f"AND raw.\"{self.EXCHANGE_COLUMN}\"::text = ANY(:exchanges)"
    )

read

read(connection, mapping_date, segments)

Reads this source's size for every live contract it lists.

Parameters:

Name Type Description Default
connection Connection

An open connection.

required
mapping_date date

The mapping date.

required
segments list

The exchange-prefixed segments to read.

required

Returns:

Type Description
dict

Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.

Example usage

From example_2_reading_rows_that_disagree.py
sizes = self.source.read(
    self.connection,
    datetime.date(2026, 9, 28),
    [
        'nse_currency_options',
        'nse_currency_futures',
    ],
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def read(self, connection, mapping_date, segments):
    """
    Reads this source's size for every live contract it lists.

    Args:
        connection (sqlalchemy.engine.Connection): An open connection.
        mapping_date (datetime.date): The mapping date.
        segments (list): The exchange-prefixed segments to read.

    Returns:
        dict: Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.
    """
    rows = connection.execute(
        self.statement(),
        {
            "broker": self.BROKER,
            "mapping_date": mapping_date,
            "segments": segments,
            "exchanges": self.EXCHANGES,
        },
    )
    sizes = {}
    for row in rows:
        if row.units is None:
            continue
        units = decimal.Decimal(row.units)
        if units <= 0:
            continue
        sizes.setdefault(row.instrument_id, set()).add(units.normalize())
    return sizes

GrowwLotSizeSource

Bases: ContractSizeSource

Groww's lot size on its commodity segment, which Groww gives in quotation units.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/GrowwLotSizeSource/example_1_the_size_expression.py
"""Prints the SQL expression Groww's `lot_size` on its COMMODITY segment is read with, and the query around it.

Groww already states a commodity lot in quotation units, which is why its GOLD lot reads 100 where Zerodha says 1.

The program only builds the expression and the query text, so it needs no database. The query joins the date's broker mappings to the Groww snapshot on the broker token and the download date, and keeps only the exchange labels this source is trusted on.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/GrowwLotSizeSource/example_1_the_size_expression.py
"""

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    GrowwLotSizeSource,
)


class ReadingTheExpressionExample:
    """Prints the source's settings, its size expression and its query.

    Attributes:
        source (GrowwLotSizeSource): The source being shown.
    """

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

        Returns:
            None: This method returns nothing.
        """
        self.source = GrowwLotSizeSource()

    def run(self):
        """Prints the name, the broker, the trusted exchange labels, the expression and the query.

        Returns:
            None: This method returns nothing.
        """
        print(f'Name: {self.source.NAME}')
        print(f'Broker: {self.source.BROKER}')
        print(f'Token column: {self.source.TOKEN_COLUMN}')
        print(f'Exchange column: {self.source.EXCHANGE_COLUMN}')
        print(f'Trusted exchange labels: {self.source.EXCHANGES}')
        print(f'Size expression: {self.source.units_expression()}')
        print('Query:')
        print(str(self.source.statement()))


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

It prints:

Name: groww_lot_size
Broker: groww
Token column: exchange_token
Exchange column: segment
Trusted exchange labels: ['COMMODITY']
Size expression: (CASE WHEN raw."lot_size"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lot_size"::text::numeric END)
Query:
SELECT m.instrument_id::text AS instrument_id, (CASE WHEN raw."lot_size"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lot_size"::text::numeric END) AS units FROM unified.broker_mappings m JOIN unified.instruments i ON i.instrument_id = m.instrument_id JOIN groww.instruments raw ON raw."exchange_token"::text = m.broker_token AND raw.download_date = m.mapping_date WHERE m.broker = :broker AND m.mapping_date = :mapping_date AND i.segment = ANY(:segments) AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) AND raw."segment"::text = ANY(:exchanges)
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/GrowwLotSizeSource/example_2_reading_sizes.py
"""Reads Groww's `lot_size` on its COMMODITY segment for a few contracts through a stand-in connection.

`read` runs the source's query and returns, for each contract, the set of distinct sizes its rows give, as decimals with trailing zeros removed. The rows give GOLD at 100, GOLDM at 10 and a CRUDEOIL option at 100. The last row stands for a column that held text; the real query's numeric guard turns such text into null, and the stand-in returns None for it to match.

The connection is a small stand-in that returns the rows it was built with and records the parameters of the query, so the program needs no database. The parameters show that the query is limited to Groww's rows on the trusted exchange labels.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/GrowwLotSizeSource/example_2_reading_sizes.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    GrowwLotSizeSource,
)


class StandInConnection:
    """A stand-in for a database connection that returns fixed rows and records the parameters of each query.

    Attributes:
        rows (list): The rows every query returns.
        parameters (dict | None): The parameters of the last query, or None before any.
    """

    def __init__(self, rows):
        """Holds the rows to return.

        Args:
            rows (list): The rows every query returns.

        Returns:
            None: This method returns nothing.
        """
        self.rows = rows
        self.parameters = None

    def execute(self, statement, parameters):
        """Records the parameters and returns the fixed rows.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The query, which is not run.
            parameters (dict): The query's parameters.

        Returns:
            list: The fixed rows.
        """
        self.parameters = parameters
        return self.rows


class ReadingSizesExample:
    """Reads the stand-in rows through the source and prints each contract's sizes.

    Attributes:
        source (GrowwLotSizeSource): The source being read.
        connection (StandInConnection): The stand-in connection.
    """

    def __init__(self):
        """Builds the source and a connection holding the rows.

        Returns:
            None: This method returns nothing.
        """
        self.source = GrowwLotSizeSource()
        self.connection = StandInConnection([
            types.SimpleNamespace(
                instrument_id='gold-future-oct',
                units='100',
            ),
            types.SimpleNamespace(
                instrument_id='goldm-future-oct',
                units='10',
            ),
            types.SimpleNamespace(
                instrument_id='crudeoil-option',
                units='100',
            ),
            types.SimpleNamespace(
                instrument_id='zinc-future-oct',
                units=None,
            ),
        ])

    def run(self):
        """Prints the sizes read and the parameters the query was given.

        Returns:
            None: This method returns nothing.
        """
        sizes = self.source.read(
            self.connection,
            datetime.date(2026, 9, 28),
            [
                'mcx_commodity_futures',
                'mcx_commodity_options',
            ],
        )
        for instrument_id in sorted(sizes):
            texts = []
            for size in sorted(sizes[instrument_id]):
                texts.append(format(size, 'f'))
            print(f'{instrument_id}: {texts}')
        print(f'Broker queried: {self.connection.parameters["broker"]}')
        print(f'Mapping date: {self.connection.parameters["mapping_date"]}')
        print(f'Segments: {self.connection.parameters["segments"]}')
        print(f'Exchange labels: {self.connection.parameters["exchanges"]}')


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

It prints:

crudeoil-option: ['100']
gold-future-oct: ['100']
goldm-future-oct: ['10']
Broker queried: groww
Mapping date: 2026-09-28
Segments: ['mcx_commodity_futures', 'mcx_commodity_options']
Exchange labels: ['COMMODITY']

Methods:

Name Description
units_expression

The lot_size column.

numeric

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

statement

The query reading this source for every live contract in the given segments on a date.

read

Reads this source's size for every live contract it lists.

units_expression

units_expression()

The lot_size column.

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_the_size_expression.py
print(f'Size expression: {self.source.units_expression()}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def units_expression(self):
    """
    The `lot_size` column.

    Returns:
        str: The SQL expression.
    """
    return self.numeric("lot_size")

numeric

numeric(column)

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

Parameters:

Name Type Description Default
column str

The snapshot column.

required

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_writing_a_new_source.py
print(f'Numeric guard on multiplier: {self.source.numeric("multiplier")}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def numeric(self, column):
    """
    An SQL expression reading a snapshot column as a number, or null when it does not hold one.

    Args:
        column (str): The snapshot column.

    Returns:
        str: The SQL expression.
    """
    return (
        f"(CASE WHEN raw.\"{column}\"::text ~ '^-?[0-9]+(\\.[0-9]+)?$' "
        f"THEN raw.\"{column}\"::text::numeric END)"
    )

statement

statement()

The query reading this source for every live contract in the given segments on a date.

Returns:

Type Description
TextClause

The query, taking mapping_date, segments and exchanges.

Example usage

From example_1_writing_a_new_source.py
print(str(self.source.statement()))
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def statement(self):
    """
    The query reading this source for every live contract in the given segments on a date.

    Returns:
        sqlalchemy.sql.elements.TextClause: The query, taking `mapping_date`, `segments` and `exchanges`.
    """
    return text(
        f"SELECT m.instrument_id::text AS instrument_id, {self.units_expression()} AS units "
        f"FROM {tables.BROKER_MAPPINGS} m "
        f"JOIN {tables.MASTER} i ON i.instrument_id = m.instrument_id "
        f"JOIN {self.BROKER}.instruments raw "
        f"ON raw.\"{self.TOKEN_COLUMN}\"::text = m.broker_token "
        "AND raw.download_date = m.mapping_date "
        "WHERE m.broker = :broker "
        "AND m.mapping_date = :mapping_date "
        "AND i.segment = ANY(:segments) "
        "AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) "
        f"AND raw.\"{self.EXCHANGE_COLUMN}\"::text = ANY(:exchanges)"
    )

read

read(connection, mapping_date, segments)

Reads this source's size for every live contract it lists.

Parameters:

Name Type Description Default
connection Connection

An open connection.

required
mapping_date date

The mapping date.

required
segments list

The exchange-prefixed segments to read.

required

Returns:

Type Description
dict

Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.

Example usage

From example_2_reading_rows_that_disagree.py
sizes = self.source.read(
    self.connection,
    datetime.date(2026, 9, 28),
    [
        'nse_currency_options',
        'nse_currency_futures',
    ],
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def read(self, connection, mapping_date, segments):
    """
    Reads this source's size for every live contract it lists.

    Args:
        connection (sqlalchemy.engine.Connection): An open connection.
        mapping_date (datetime.date): The mapping date.
        segments (list): The exchange-prefixed segments to read.

    Returns:
        dict: Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.
    """
    rows = connection.execute(
        self.statement(),
        {
            "broker": self.BROKER,
            "mapping_date": mapping_date,
            "segments": segments,
            "exchanges": self.EXCHANGES,
        },
    )
    sizes = {}
    for row in rows:
        if row.units is None:
            continue
        units = decimal.Decimal(row.units)
        if units <= 0:
            continue
        sizes.setdefault(row.instrument_id, set()).add(units.normalize())
    return sizes

ShoonyaCurrencySource

Bases: ContractSizeSource

Shoonya's lot size times its multiplier on NSE currencies, where it gives a lot size of 1 and a multiplier of the contract size.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ShoonyaCurrencySource/example_1_the_size_expression.py
"""Prints the SQL expression Shoonya's lot size times its multiplier on NSE currencies is read with, and the query around it.

Shoonya lists a currency lot as 1 and puts the contract size in its multiplier, so the product of the two is the size in quotation units.

The program only builds the expression and the query text, so it needs no database. The query joins the date's broker mappings to the Shoonya snapshot on the broker token and the download date, and keeps only the exchange labels this source is trusted on.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ShoonyaCurrencySource/example_1_the_size_expression.py
"""

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ShoonyaCurrencySource,
)


class ReadingTheExpressionExample:
    """Prints the source's settings, its size expression and its query.

    Attributes:
        source (ShoonyaCurrencySource): The source being shown.
    """

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

        Returns:
            None: This method returns nothing.
        """
        self.source = ShoonyaCurrencySource()

    def run(self):
        """Prints the name, the broker, the trusted exchange labels, the expression and the query.

        Returns:
            None: This method returns nothing.
        """
        print(f'Name: {self.source.NAME}')
        print(f'Broker: {self.source.BROKER}')
        print(f'Token column: {self.source.TOKEN_COLUMN}')
        print(f'Exchange column: {self.source.EXCHANGE_COLUMN}')
        print(f'Trusted exchange labels: {self.source.EXCHANGES}')
        print(f'Size expression: {self.source.units_expression()}')
        print('Query:')
        print(str(self.source.statement()))


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

It prints:

Name: shoonya_lot_size_times_multiplier
Broker: shoonya
Token column: token
Exchange column: exchange
Trusted exchange labels: ['CDS']
Size expression: ((CASE WHEN raw."lotsize"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lotsize"::text::numeric END) * (CASE WHEN raw."multiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."multiplier"::text::numeric END))
Query:
SELECT m.instrument_id::text AS instrument_id, ((CASE WHEN raw."lotsize"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lotsize"::text::numeric END) * (CASE WHEN raw."multiplier"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."multiplier"::text::numeric END)) AS units FROM unified.broker_mappings m JOIN unified.instruments i ON i.instrument_id = m.instrument_id JOIN shoonya.instruments raw ON raw."token"::text = m.broker_token AND raw.download_date = m.mapping_date WHERE m.broker = :broker AND m.mapping_date = :mapping_date AND i.segment = ANY(:segments) AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) AND raw."exchange"::text = ANY(:exchanges)
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ShoonyaCurrencySource/example_2_reading_sizes.py
"""Reads Shoonya's lot size times its multiplier on NSE currencies for a few contracts through a stand-in connection.

`read` runs the source's query and returns, for each contract, the set of distinct sizes its rows give, as decimals with trailing zeros removed. The rows give USDINR and EURINR at 1000 units and JPYINR at 100000 yen, which are the NSE contract sizes.

The connection is a small stand-in that returns the rows it was built with and records the parameters of the query, so the program needs no database. The parameters show that the query is limited to Shoonya's rows on the trusted exchange labels.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ShoonyaCurrencySource/example_2_reading_sizes.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ShoonyaCurrencySource,
)


class StandInConnection:
    """A stand-in for a database connection that returns fixed rows and records the parameters of each query.

    Attributes:
        rows (list): The rows every query returns.
        parameters (dict | None): The parameters of the last query, or None before any.
    """

    def __init__(self, rows):
        """Holds the rows to return.

        Args:
            rows (list): The rows every query returns.

        Returns:
            None: This method returns nothing.
        """
        self.rows = rows
        self.parameters = None

    def execute(self, statement, parameters):
        """Records the parameters and returns the fixed rows.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The query, which is not run.
            parameters (dict): The query's parameters.

        Returns:
            list: The fixed rows.
        """
        self.parameters = parameters
        return self.rows


class ReadingSizesExample:
    """Reads the stand-in rows through the source and prints each contract's sizes.

    Attributes:
        source (ShoonyaCurrencySource): The source being read.
        connection (StandInConnection): The stand-in connection.
    """

    def __init__(self):
        """Builds the source and a connection holding the rows.

        Returns:
            None: This method returns nothing.
        """
        self.source = ShoonyaCurrencySource()
        self.connection = StandInConnection([
            types.SimpleNamespace(
                instrument_id='usdinr-future-oct',
                units='1000',
            ),
            types.SimpleNamespace(
                instrument_id='jpyinr-future-oct',
                units='100000',
            ),
            types.SimpleNamespace(
                instrument_id='eurinr-option',
                units='1000',
            ),
        ])

    def run(self):
        """Prints the sizes read and the parameters the query was given.

        Returns:
            None: This method returns nothing.
        """
        sizes = self.source.read(
            self.connection,
            datetime.date(2026, 9, 28),
            [
                'nse_currency_futures',
                'nse_currency_options',
            ],
        )
        for instrument_id in sorted(sizes):
            texts = []
            for size in sorted(sizes[instrument_id]):
                texts.append(format(size, 'f'))
            print(f'{instrument_id}: {texts}')
        print(f'Broker queried: {self.connection.parameters["broker"]}')
        print(f'Mapping date: {self.connection.parameters["mapping_date"]}')
        print(f'Segments: {self.connection.parameters["segments"]}')
        print(f'Exchange labels: {self.connection.parameters["exchanges"]}')


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

It prints:

eurinr-option: ['1000']
jpyinr-future-oct: ['100000']
usdinr-future-oct: ['1000']
Broker queried: shoonya
Mapping date: 2026-09-28
Segments: ['nse_currency_futures', 'nse_currency_options']
Exchange labels: ['CDS']

Methods:

Name Description
units_expression

lotsize times multiplier.

numeric

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

statement

The query reading this source for every live contract in the given segments on a date.

read

Reads this source's size for every live contract it lists.

units_expression

units_expression()

lotsize times multiplier.

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_the_size_expression.py
print(f'Size expression: {self.source.units_expression()}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def units_expression(self):
    """
    `lotsize` times `multiplier`.

    Returns:
        str: The SQL expression.
    """
    return f"({self.numeric('lotsize')} * {self.numeric('multiplier')})"

numeric

numeric(column)

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

Parameters:

Name Type Description Default
column str

The snapshot column.

required

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_writing_a_new_source.py
print(f'Numeric guard on multiplier: {self.source.numeric("multiplier")}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def numeric(self, column):
    """
    An SQL expression reading a snapshot column as a number, or null when it does not hold one.

    Args:
        column (str): The snapshot column.

    Returns:
        str: The SQL expression.
    """
    return (
        f"(CASE WHEN raw.\"{column}\"::text ~ '^-?[0-9]+(\\.[0-9]+)?$' "
        f"THEN raw.\"{column}\"::text::numeric END)"
    )

statement

statement()

The query reading this source for every live contract in the given segments on a date.

Returns:

Type Description
TextClause

The query, taking mapping_date, segments and exchanges.

Example usage

From example_1_writing_a_new_source.py
print(str(self.source.statement()))
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def statement(self):
    """
    The query reading this source for every live contract in the given segments on a date.

    Returns:
        sqlalchemy.sql.elements.TextClause: The query, taking `mapping_date`, `segments` and `exchanges`.
    """
    return text(
        f"SELECT m.instrument_id::text AS instrument_id, {self.units_expression()} AS units "
        f"FROM {tables.BROKER_MAPPINGS} m "
        f"JOIN {tables.MASTER} i ON i.instrument_id = m.instrument_id "
        f"JOIN {self.BROKER}.instruments raw "
        f"ON raw.\"{self.TOKEN_COLUMN}\"::text = m.broker_token "
        "AND raw.download_date = m.mapping_date "
        "WHERE m.broker = :broker "
        "AND m.mapping_date = :mapping_date "
        "AND i.segment = ANY(:segments) "
        "AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) "
        f"AND raw.\"{self.EXCHANGE_COLUMN}\"::text = ANY(:exchanges)"
    )

read

read(connection, mapping_date, segments)

Reads this source's size for every live contract it lists.

Parameters:

Name Type Description Default
connection Connection

An open connection.

required
mapping_date date

The mapping date.

required
segments list

The exchange-prefixed segments to read.

required

Returns:

Type Description
dict

Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.

Example usage

From example_2_reading_rows_that_disagree.py
sizes = self.source.read(
    self.connection,
    datetime.date(2026, 9, 28),
    [
        'nse_currency_options',
        'nse_currency_futures',
    ],
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def read(self, connection, mapping_date, segments):
    """
    Reads this source's size for every live contract it lists.

    Args:
        connection (sqlalchemy.engine.Connection): An open connection.
        mapping_date (datetime.date): The mapping date.
        segments (list): The exchange-prefixed segments to read.

    Returns:
        dict: Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.
    """
    rows = connection.execute(
        self.statement(),
        {
            "broker": self.BROKER,
            "mapping_date": mapping_date,
            "segments": segments,
            "exchanges": self.EXCHANGES,
        },
    )
    sizes = {}
    for row in rows:
        if row.units is None:
            continue
        units = decimal.Decimal(row.units)
        if units <= 0:
            continue
        sizes.setdefault(row.instrument_id, set()).add(units.normalize())
    return sizes

StoxkartLotSizeSource

Bases: ContractSizeSource

Stoxkart's lot size on the NSE and BSE currency segments and NCDEX.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/StoxkartLotSizeSource/example_1_the_size_expression.py
"""Prints the SQL expression Stoxkart's `lot_size` on NSE and BSE currencies and NCDEX is read with, and the query around it.

Stoxkart is the only broker that lists BSE currencies and NCDEX, so on those markets its figure alone is enough for the contract to be traded.

The program only builds the expression and the query text, so it needs no database. The query joins the date's broker mappings to the Stoxkart snapshot on the broker token and the download date, and keeps only the exchange labels this source is trusted on.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/StoxkartLotSizeSource/example_1_the_size_expression.py
"""

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    StoxkartLotSizeSource,
)


class ReadingTheExpressionExample:
    """Prints the source's settings, its size expression and its query.

    Attributes:
        source (StoxkartLotSizeSource): The source being shown.
    """

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

        Returns:
            None: This method returns nothing.
        """
        self.source = StoxkartLotSizeSource()

    def run(self):
        """Prints the name, the broker, the trusted exchange labels, the expression and the query.

        Returns:
            None: This method returns nothing.
        """
        print(f'Name: {self.source.NAME}')
        print(f'Broker: {self.source.BROKER}')
        print(f'Token column: {self.source.TOKEN_COLUMN}')
        print(f'Exchange column: {self.source.EXCHANGE_COLUMN}')
        print(f'Trusted exchange labels: {self.source.EXCHANGES}')
        print(f'Size expression: {self.source.units_expression()}')
        print('Query:')
        print(str(self.source.statement()))


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

It prints:

Name: stoxkart_lot_size
Broker: stoxkart
Token column: token
Exchange column: exchange
Trusted exchange labels: ['NSECD', 'BSECD', 'NCDEX']
Size expression: (CASE WHEN raw."lot_size"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lot_size"::text::numeric END)
Query:
SELECT m.instrument_id::text AS instrument_id, (CASE WHEN raw."lot_size"::text ~ '^-?[0-9]+(\.[0-9]+)?$' THEN raw."lot_size"::text::numeric END) AS units FROM unified.broker_mappings m JOIN unified.instruments i ON i.instrument_id = m.instrument_id JOIN stoxkart.instruments raw ON raw."token"::text = m.broker_token AND raw.download_date = m.mapping_date WHERE m.broker = :broker AND m.mapping_date = :mapping_date AND i.segment = ANY(:segments) AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) AND raw."exchange"::text = ANY(:exchanges)
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/StoxkartLotSizeSource/example_2_reading_sizes.py
"""Reads Stoxkart's `lot_size` on NSE and BSE currencies and NCDEX for a few contracts through a stand-in connection.

`read` runs the source's query and returns, for each contract, the set of distinct sizes its rows give, as decimals with trailing zeros removed. The rows give a BSE USDINR future at 1000 units and two NCDEX futures, DHANIYA and JEERA, at 5 and 3 tonnes.

The connection is a small stand-in that returns the rows it was built with and records the parameters of the query, so the program needs no database. The parameters show that the query is limited to Stoxkart's rows on the trusted exchange labels.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/StoxkartLotSizeSource/example_2_reading_sizes.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    StoxkartLotSizeSource,
)


class StandInConnection:
    """A stand-in for a database connection that returns fixed rows and records the parameters of each query.

    Attributes:
        rows (list): The rows every query returns.
        parameters (dict | None): The parameters of the last query, or None before any.
    """

    def __init__(self, rows):
        """Holds the rows to return.

        Args:
            rows (list): The rows every query returns.

        Returns:
            None: This method returns nothing.
        """
        self.rows = rows
        self.parameters = None

    def execute(self, statement, parameters):
        """Records the parameters and returns the fixed rows.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The query, which is not run.
            parameters (dict): The query's parameters.

        Returns:
            list: The fixed rows.
        """
        self.parameters = parameters
        return self.rows


class ReadingSizesExample:
    """Reads the stand-in rows through the source and prints each contract's sizes.

    Attributes:
        source (StoxkartLotSizeSource): The source being read.
        connection (StandInConnection): The stand-in connection.
    """

    def __init__(self):
        """Builds the source and a connection holding the rows.

        Returns:
            None: This method returns nothing.
        """
        self.source = StoxkartLotSizeSource()
        self.connection = StandInConnection([
            types.SimpleNamespace(
                instrument_id='bse-usdinr-future-oct',
                units='1000',
            ),
            types.SimpleNamespace(
                instrument_id='ncdex-dhaniya-future-nov',
                units='5',
            ),
            types.SimpleNamespace(
                instrument_id='ncdex-jeera-future-nov',
                units='3',
            ),
        ])

    def run(self):
        """Prints the sizes read and the parameters the query was given.

        Returns:
            None: This method returns nothing.
        """
        sizes = self.source.read(
            self.connection,
            datetime.date(2026, 9, 28),
            [
                'bse_currency_futures',
                'ncdex_commodity_futures',
            ],
        )
        for instrument_id in sorted(sizes):
            texts = []
            for size in sorted(sizes[instrument_id]):
                texts.append(format(size, 'f'))
            print(f'{instrument_id}: {texts}')
        print(f'Broker queried: {self.connection.parameters["broker"]}')
        print(f'Mapping date: {self.connection.parameters["mapping_date"]}')
        print(f'Segments: {self.connection.parameters["segments"]}')
        print(f'Exchange labels: {self.connection.parameters["exchanges"]}')


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

It prints:

bse-usdinr-future-oct: ['1000']
ncdex-dhaniya-future-nov: ['5']
ncdex-jeera-future-nov: ['3']
Broker queried: stoxkart
Mapping date: 2026-09-28
Segments: ['bse_currency_futures', 'ncdex_commodity_futures']
Exchange labels: ['NSECD', 'BSECD', 'NCDEX']

Methods:

Name Description
units_expression

The lot_size column.

numeric

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

statement

The query reading this source for every live contract in the given segments on a date.

read

Reads this source's size for every live contract it lists.

units_expression

units_expression()

The lot_size column.

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_the_size_expression.py
print(f'Size expression: {self.source.units_expression()}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def units_expression(self):
    """
    The `lot_size` column.

    Returns:
        str: The SQL expression.
    """
    return self.numeric("lot_size")

numeric

numeric(column)

An SQL expression reading a snapshot column as a number, or null when it does not hold one.

Parameters:

Name Type Description Default
column str

The snapshot column.

required

Returns:

Type Description
str

The SQL expression.

Example usage

From example_1_writing_a_new_source.py
print(f'Numeric guard on multiplier: {self.source.numeric("multiplier")}')
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def numeric(self, column):
    """
    An SQL expression reading a snapshot column as a number, or null when it does not hold one.

    Args:
        column (str): The snapshot column.

    Returns:
        str: The SQL expression.
    """
    return (
        f"(CASE WHEN raw.\"{column}\"::text ~ '^-?[0-9]+(\\.[0-9]+)?$' "
        f"THEN raw.\"{column}\"::text::numeric END)"
    )

statement

statement()

The query reading this source for every live contract in the given segments on a date.

Returns:

Type Description
TextClause

The query, taking mapping_date, segments and exchanges.

Example usage

From example_1_writing_a_new_source.py
print(str(self.source.statement()))
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def statement(self):
    """
    The query reading this source for every live contract in the given segments on a date.

    Returns:
        sqlalchemy.sql.elements.TextClause: The query, taking `mapping_date`, `segments` and `exchanges`.
    """
    return text(
        f"SELECT m.instrument_id::text AS instrument_id, {self.units_expression()} AS units "
        f"FROM {tables.BROKER_MAPPINGS} m "
        f"JOIN {tables.MASTER} i ON i.instrument_id = m.instrument_id "
        f"JOIN {self.BROKER}.instruments raw "
        f"ON raw.\"{self.TOKEN_COLUMN}\"::text = m.broker_token "
        "AND raw.download_date = m.mapping_date "
        "WHERE m.broker = :broker "
        "AND m.mapping_date = :mapping_date "
        "AND i.segment = ANY(:segments) "
        "AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date) "
        f"AND raw.\"{self.EXCHANGE_COLUMN}\"::text = ANY(:exchanges)"
    )

read

read(connection, mapping_date, segments)

Reads this source's size for every live contract it lists.

Parameters:

Name Type Description Default
connection Connection

An open connection.

required
mapping_date date

The mapping date.

required
segments list

The exchange-prefixed segments to read.

required

Returns:

Type Description
dict

Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.

Example usage

From example_2_reading_rows_that_disagree.py
sizes = self.source.read(
    self.connection,
    datetime.date(2026, 9, 28),
    [
        'nse_currency_options',
        'nse_currency_futures',
    ],
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def read(self, connection, mapping_date, segments):
    """
    Reads this source's size for every live contract it lists.

    Args:
        connection (sqlalchemy.engine.Connection): An open connection.
        mapping_date (datetime.date): The mapping date.
        segments (list): The exchange-prefixed segments to read.

    Returns:
        dict: Instrument ids to the set of distinct sizes, as decimals, that the source's rows give for the contract, leaving out rows whose size is missing, zero or negative. A set holds more than one size when the source's file lists the contract twice with different sizes.
    """
    rows = connection.execute(
        self.statement(),
        {
            "broker": self.BROKER,
            "mapping_date": mapping_date,
            "segments": segments,
            "exchanges": self.EXCHANGES,
        },
    )
    sizes = {}
    for row in rows:
        if row.units is None:
            continue
        units = decimal.Decimal(row.units)
        if units <= 0:
            continue
        sizes.setdefault(row.instrument_id, set()).add(units.normalize())
    return sizes

ContractSizeDecision

The rule that turns the sources' figures for one contract into a stored decision.

Attributes:

Name Type Description
SINGLE_SOURCE_MARKETS list

The (exchange, asset class) markets where one source is enough to trade, because only one broker lists them.

SIBLING_MARKETS list

The (exchange, asset class) markets where one source is enough to trade when the underlying's confirmed contracts in the segment all have that size.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeDecision/example_1_deciding_one_contract.py
"""Decides the contract size of five contracts from what each source says about them.

`decide` looks only at the figures it is given. Two or more sources that agree make a contract `confirmed`; one source makes it `single_source`, which is tradeable only on BSE currencies and NCDEX, where Stoxkart is the only broker listing them; any disagreement, including one source giving two sizes for the same contract, makes it a `conflict`; and no figure at all makes it `no_source`.

The figures are made up but follow real contract sizes: 100 units for MCX GOLD, 1000 for a USDINR contract and 5 tonnes for an NCDEX DHANIYA future. `market` is printed beside each decision because the tradeable flag of a single-source contract depends on it. No database is needed.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeDecision/example_1_deciding_one_contract.py
"""

import decimal

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeDecision,
)


class DecidingOneContractExample:
    """Runs the decision rule over five contracts and prints each answer.

    Attributes:
        decision (ContractSizeDecision): The rule being shown.
        cases (list): Tuples of a label, a segment, the figures and the contradicting sources.
    """

    def __init__(self):
        """Builds the rule and the five cases.

        Returns:
            None: This method returns nothing.
        """
        self.decision = ContractSizeDecision()
        self.cases = [
            (
                'MCX GOLD, three sources agree',
                'mcx_commodity_futures',
                {
                    'wisdom_capital_multiplier': decimal.Decimal('100'),
                    'kotak_contract_size': decimal.Decimal('100'),
                    'groww_lot_size': decimal.Decimal('100'),
                },
                None,
            ),
            (
                'NSE USDINR, one source disagrees',
                'nse_currency_options',
                {
                    'kotak_contract_size': decimal.Decimal('1000'),
                    'shoonya_lot_size_times_multiplier': decimal.Decimal('1000'),
                    'stoxkart_lot_size': decimal.Decimal('2000'),
                },
                None,
            ),
            (
                'NSE USDINR, Stoxkart lists it twice',
                'nse_currency_options',
                {
                    'kotak_contract_size': decimal.Decimal('1000'),
                    'shoonya_lot_size_times_multiplier': decimal.Decimal('1000'),
                },
                {
                    'stoxkart_lot_size',
                },
            ),
            (
                'NCDEX DHANIYA, Stoxkart only',
                'ncdex_commodity_futures',
                {
                    'stoxkart_lot_size': decimal.Decimal('5'),
                },
                None,
            ),
            (
                'MCX ZINC, no source',
                'mcx_commodity_futures',
                {},
                None,
            ),
        ]

    def run(self):
        """Prints the market and the decision for each case.

        Returns:
            None: This method returns nothing.
        """
        for label, segment, figures, contradicting_sources in self.cases:
            market = self.decision.market(segment)
            units_per_lot, status, tradeable = self.decision.decide(
                segment,
                figures,
                contradicting_sources,
            )
            print(f'{label}')
            print(f'  market {market}: size {units_per_lot}, {status}, tradeable {tradeable}')


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

It prints:

MCX GOLD, three sources agree
  market ('mcx', 'commodity'): size 100, confirmed, tradeable True
NSE USDINR, one source disagrees
  market ('nse', 'currency'): size None, conflict, tradeable False
NSE USDINR, Stoxkart lists it twice
  market ('nse', 'currency'): size None, conflict, tradeable False
NCDEX DHANIYA, Stoxkart only
  market ('ncdex', 'commodity'): size 5, single_source, tradeable True
MCX ZINC, no source
  market ('mcx', 'commodity'): size None, no_source, tradeable False
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeDecision/example_2_settling_by_siblings.py
"""Upgrades a newly listed MCX contract that only one broker lists yet, by checking its already confirmed siblings.

Some brokers add a new far-month expiry later than others, so on MCX a new NATURALGAS option can have a single source for a few days. `decide` calls it `single_source` and untradeable. `settle_by_siblings` then looks at the sizes of the confirmed contracts of the same underlying in the same segment: when they all have one size and it is the single source's figure, the contract becomes `sibling_confirmed` and tradeable.

The program runs four such checks. The first is upgraded. The second is not, because the siblings come in two sizes, as happens after the exchange revises a lot. The third is not, because the single figure disagrees with the siblings. The fourth is on NSE commodities, where the sibling rule does not apply at all. The figures are made up; no database is needed.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeDecision/example_2_settling_by_siblings.py
"""

import decimal

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeDecision,
)


class SettlingBySiblingsExample:
    """Decides four single-source contracts and then settles each against its siblings.

    Attributes:
        decision (ContractSizeDecision): The rule being shown.
        cases (list): Tuples of a label, a segment, the single source's figure and the siblings' confirmed sizes.
    """

    def __init__(self):
        """Builds the rule and the four cases.

        Returns:
            None: This method returns nothing.
        """
        self.decision = ContractSizeDecision()
        self.cases = [
            (
                'MCX NATURALGAS far-month option',
                'mcx_commodity_options',
                decimal.Decimal('1250'),
                {
                    decimal.Decimal('1250'),
                },
            ),
            (
                'MCX index option after a lot revision',
                'mcx_commodity_index_options',
                decimal.Decimal('15'),
                {
                    decimal.Decimal('15'),
                    decimal.Decimal('30'),
                },
            ),
            (
                'MCX option disagreeing with its siblings',
                'mcx_commodity_options',
                decimal.Decimal('1250'),
                {
                    decimal.Decimal('250'),
                },
            ),
            (
                'NSE commodity future',
                'nse_commodity_futures',
                decimal.Decimal('1'),
                {
                    decimal.Decimal('1'),
                },
            ),
        ]

    def run(self):
        """Prints the first decision and the settled status for each case.

        Returns:
            None: This method returns nothing.
        """
        print(f'Markets where one source is enough: {ContractSizeDecision.SINGLE_SOURCE_MARKETS}')
        print(f'Markets where siblings can confirm: {ContractSizeDecision.SIBLING_MARKETS}')
        for label, segment, figure, sibling_sizes in self.cases:
            units_per_lot, status, tradeable = self.decision.decide(
                segment,
                {
                    'kotak_contract_size': figure,
                },
            )
            settled_status, settled_tradeable = self.decision.settle_by_siblings(
                segment,
                units_per_lot,
                status,
                tradeable,
                sibling_sizes,
            )
            print(f'{label}: {status} {tradeable} -> {settled_status} {settled_tradeable}')


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

It prints:

Markets where one source is enough: [('bse', 'currency'), ('ncdex', 'commodity')]
Markets where siblings can confirm: [('mcx', 'commodity')]
MCX NATURALGAS far-month option: single_source False -> sibling_confirmed True
MCX index option after a lot revision: single_source False -> single_source False
MCX option disagreeing with its siblings: single_source False -> single_source False
NSE commodity future: single_source False -> single_source False

Methods:

Name Description
market

The exchange and asset class of an exchange-prefixed segment.

decide

Decides one contract's size from what each source says.

settle_by_siblings

Upgrades an untradeable single-source decision when the underlying's confirmed contracts all agree with it.

market

market(segment)

The exchange and asset class of an exchange-prefixed segment.

Parameters:

Name Type Description Default
segment str

The segment, such as mcx_commodity_futures.

required

Returns:

Type Description
tuple

(exchange, asset class), where the asset class is currency or commodity.

Example usage

From example_1_deciding_one_contract.py
market = self.decision.market(segment)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def market(self, segment):
    """
    The exchange and asset class of an exchange-prefixed segment.

    Args:
        segment (str): The segment, such as `mcx_commodity_futures`.

    Returns:
        tuple: `(exchange, asset class)`, where the asset class is `currency` or `commodity`.
    """
    exchange, _, bare_segment = segment.partition("_")
    if bare_segment.startswith("currency"):
        return (exchange, "currency")
    return (exchange, "commodity")

decide

decide(segment, figures, contradicting_sources=None)

Decides one contract's size from what each source says.

A source that gives two different sizes for the same contract contradicts itself, and makes the contract a conflict just as two disagreeing sources do. Keeping either of its sizes would make the decision depend on the order the database happens to return the rows in.

Parameters:

Name Type Description Default
segment str

The contract's exchange-prefixed segment.

required
figures dict

Source names to sizes as decimals, for the sources that give exactly one.

required
contradicting_sources set | None

The names of the sources that give more than one size for the contract, or None for none.

None

Returns:

Type Description
tuple

(units_per_lot, status, tradeable), where units_per_lot is a decimal or None, status one of confirmed, single_source, conflict and no_source, and tradeable a bool.

Example usage

From example_1_deciding_one_contract.py
units_per_lot, status, tradeable = self.decision.decide(
    segment,
    figures,
    contradicting_sources,
)
From example_2_settling_by_siblings.py
units_per_lot, status, tradeable = self.decision.decide(
    segment,
    {
        'kotak_contract_size': figure,
    },
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def decide(self, segment, figures, contradicting_sources=None):
    """
    Decides one contract's size from what each source says.

    A source that gives two different sizes for the same contract contradicts itself, and makes the contract a conflict just as two disagreeing sources do. Keeping either of its sizes would make the decision depend on the order the database happens to return the rows in.

    Args:
        segment (str): The contract's exchange-prefixed segment.
        figures (dict): Source names to sizes as decimals, for the sources that give exactly one.
        contradicting_sources (set | None): The names of the sources that give more than one size for the contract, or None for none.

    Returns:
        tuple: `(units_per_lot, status, tradeable)`, where `units_per_lot` is a decimal or None, `status` one of `confirmed`, `single_source`, `conflict` and `no_source`, and `tradeable` a bool.
    """
    if contradicting_sources:
        return None, "conflict", False
    distinct_sizes = set(figures.values())
    if not figures:
        return None, "no_source", False
    if len(distinct_sizes) > 1:
        return None, "conflict", False
    units_per_lot = next(iter(distinct_sizes))
    if len(figures) == 1:
        tradeable = self.market(segment) in self.SINGLE_SOURCE_MARKETS
        return units_per_lot, "single_source", tradeable
    return units_per_lot, "confirmed", True

settle_by_siblings

settle_by_siblings(segment, units_per_lot, status, tradeable, sibling_sizes)

Upgrades an untradeable single-source decision when the underlying's confirmed contracts all agree with it.

Parameters:

Name Type Description Default
segment str

The contract's exchange-prefixed segment.

required
units_per_lot Decimal | None

The size decide gave.

required
status str

The status decide gave.

required
tradeable bool

Whether decide made the contract tradeable.

required
sibling_sizes set

The distinct sizes of the confirmed contracts of the same underlying in the same segment on the date.

required

Returns:

Type Description
tuple

(status, tradeable), which is ("sibling_confirmed", True) when the decision is upgraded and the given status and tradeable otherwise.

Example usage

From example_2_settling_by_siblings.py
settled_status, settled_tradeable = self.decision.settle_by_siblings(
    segment,
    units_per_lot,
    status,
    tradeable,
    sibling_sizes,
)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def settle_by_siblings(self, segment, units_per_lot, status, tradeable, sibling_sizes):
    """
    Upgrades an untradeable single-source decision when the underlying's confirmed contracts all agree with it.

    Args:
        segment (str): The contract's exchange-prefixed segment.
        units_per_lot (decimal.Decimal | None): The size `decide` gave.
        status (str): The status `decide` gave.
        tradeable (bool): Whether `decide` made the contract tradeable.
        sibling_sizes (set): The distinct sizes of the confirmed contracts of the same underlying in the same segment on the date.

    Returns:
        tuple: `(status, tradeable)`, which is `("sibling_confirmed", True)` when the decision is upgraded and the given status and tradeable otherwise.
    """
    if status != "single_source" or tradeable:
        return status, tradeable
    if self.market(segment) not in self.SIBLING_MARKETS:
        return status, tradeable
    if sibling_sizes != {units_per_lot}:
        return status, tradeable
    return "sibling_confirmed", True

ContractSizeResolver

ContractSizeResolver(engine=None)

Reads every source for a mapping date, decides each contract's size and writes the decisions.

Attributes:

Name Type Description
WRITE_BATCH_ROWS int

How many rows are inserted per statement.

SOURCES list

The source classes read, in the order their figures are recorded.

engine Engine

The engine over TimescaleDB.

decision ContractSizeDecision

The rule applied to each contract.

Builds the resolver.

Parameters:

Name Type Description Default
engine Engine | None

The engine to use. None builds one from the environment.

None

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeResolver/example_1_deciding_and_writing_a_date.py
"""Decides and writes one mapping date's contract sizes through a stand-in database.

`run` reads the date's live currency and commodity contracts, reads every source's figure for them, decides each contract, writes the decisions to `unified.contract_sizes` in one transaction and returns a count per segment, status and tradeable flag.

The engine here is a stand-in that answers the resolver's queries from a few rows held in memory and records what the resolver writes, so no PostgreSQL is needed. The contracts are made up but realistic: three MCX GOLD futures, of which the newest far month is listed by Groww only; an NCDEX DHANIYA future that only Stoxkart lists; and an MCX ZINC future no source gives a size for.

Notice that the far-month GOLD future becomes `sibling_confirmed`, because the two nearer GOLD futures are confirmed at the same 100 units, and that DHANIYA is tradeable on a single source because NCDEX is one of the single-source markets. Each written row keeps every source's figure, so the reason for the decision can be read back later.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeResolver/example_1_deciding_and_writing_a_date.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeResolver,
)


class StandInConnection:
    """A stand-in database connection that answers the resolver's queries from rows in memory.

    Attributes:
        database (StandInDatabase): The rows and the record of writes.
    """

    def __init__(self, database):
        """Holds the stand-in database.

        Args:
            database (StandInDatabase): The rows and the record of writes.

        Returns:
            None: This method returns nothing.
        """
        self.database = database

    def __enter__(self):
        """Opens the connection.

        Returns:
            StandInConnection: This connection.
        """
        return self

    def __exit__(self, error_type, error, traceback):
        """Closes the connection.

        Args:
            error_type (type | None): The exception type raised inside the block, if any.
            error (BaseException | None): The exception raised inside the block, if any.
            traceback (types.TracebackType | None): The traceback, if any.

        Returns:
            bool: False, so an exception is never swallowed.
        """
        return False

    def execute(self, statement, parameters):
        """Answers one query from the rows in memory, or records one write.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The statement.
            parameters (dict | list): The statement's parameters, or a list of them for a batch insert.

        Returns:
            list: The rows the query returns, empty for a write.
        """
        sql = str(statement)
        if sql.startswith('DELETE'):
            self.database.deleted_dates.append(parameters['mapping_date'])
            return []
        if sql.startswith('INSERT'):
            for row in parameters:
                self.database.written_rows.append(row)
            return []
        if 'AS units' in sql:
            return self.database.source_rows.get(parameters['broker'], [])
        return self.database.contract_rows


class StandInDatabase:
    """The rows the stand-in answers with, and the writes it has received.

    Attributes:
        contract_rows (list): The live contracts, as rows with `instrument_id`, `segment` and `underlying_symbol`.
        source_rows (dict): Broker name to the rows of `instrument_id` and `units` its source returns.
        deleted_dates (list): The mapping dates whose decisions were deleted.
        written_rows (list): The decision rows inserted.
    """

    def __init__(self, contract_rows, source_rows):
        """Holds the rows.

        Args:
            contract_rows (list): The live contracts.
            source_rows (dict): Broker name to its source's rows.

        Returns:
            None: This method returns nothing.
        """
        self.contract_rows = contract_rows
        self.source_rows = source_rows
        self.deleted_dates = []
        self.written_rows = []


class StandInEngine:
    """A stand-in SQLAlchemy engine whose connections all share one stand-in database.

    Attributes:
        database (StandInDatabase): The shared rows.
    """

    def __init__(self, database):
        """Holds the database.

        Args:
            database (StandInDatabase): The shared rows.

        Returns:
            None: This method returns nothing.
        """
        self.database = database

    def connect(self):
        """Opens a connection for reading.

        Returns:
            StandInConnection: A connection over the shared rows.
        """
        return StandInConnection(self.database)

    def begin(self):
        """Opens a connection inside a transaction.

        Returns:
            StandInConnection: A connection over the shared rows.
        """
        return StandInConnection(self.database)


class DecidingAndWritingADateExample:
    """Runs the resolver for 2026-09-28 over five contracts and prints the summary and the written rows.

    Attributes:
        database (StandInDatabase): The stand-in rows.
        resolver (ContractSizeResolver): The resolver being shown.
    """

    def __init__(self):
        """Builds the stand-in database and the resolver over it.

        Returns:
            None: This method returns nothing.
        """
        self.database = StandInDatabase(
            [
                self.contract('gold-2026-10', 'mcx_commodity_futures', 'GOLD'),
                self.contract('gold-2026-12', 'mcx_commodity_futures', 'GOLD'),
                self.contract('gold-2027-02', 'mcx_commodity_futures', 'GOLD'),
                self.contract('dhaniya-2026-11', 'ncdex_commodity_futures', 'DHANIYA'),
                self.contract('zinc-2026-10', 'mcx_commodity_futures', 'ZINC'),
            ],
            {
                'wisdom_capital': [
                    self.figure('gold-2026-10', '100'),
                    self.figure('gold-2026-12', '100'),
                ],
                'kotak': [
                    self.figure('gold-2026-10', '100.000'),
                ],
                'groww': [
                    self.figure('gold-2026-10', '100'),
                    self.figure('gold-2026-12', '100'),
                    self.figure('gold-2027-02', '100'),
                ],
                'stoxkart': [
                    self.figure('dhaniya-2026-11', '5'),
                ],
            },
        )
        self.resolver = ContractSizeResolver(StandInEngine(self.database))

    def contract(self, instrument_id, segment, underlying_symbol):
        """Builds one live contract row.

        Args:
            instrument_id (str): The contract's instrument id.
            segment (str): Its exchange-prefixed segment.
            underlying_symbol (str): Its underlying's symbol.

        Returns:
            types.SimpleNamespace: The row.
        """
        return types.SimpleNamespace(
            instrument_id=instrument_id,
            segment=segment,
            underlying_symbol=underlying_symbol,
        )

    def figure(self, instrument_id, units):
        """Builds one source row.

        Args:
            instrument_id (str): The contract's instrument id.
            units (str): The size the source gives.

        Returns:
            types.SimpleNamespace: The row.
        """
        return types.SimpleNamespace(
            instrument_id=instrument_id,
            units=units,
        )

    def run(self):
        """Runs the resolver and prints the summary and every written row.

        Returns:
            None: This method returns nothing.
        """
        summary = self.resolver.run(datetime.date(2026, 9, 28))
        print('Summary:')
        for segment, status, tradeable in sorted(summary):
            count = summary[(segment, status, tradeable)]
            print(f'  {segment} {status} tradeable={tradeable}: {count}')
        for deleted_date in self.database.deleted_dates:
            print(f'Decisions deleted first for: {deleted_date.isoformat()}')
        print('Rows written:')
        for row in self.database.written_rows:
            units_text = 'none'
            if row['units_per_lot'] is not None:
                units_text = format(row['units_per_lot'], 'f')
            print(f'  {row["instrument_id"]}: {units_text} {row["status"]} tradeable={row["tradeable"]} sources={row["sources"]}')


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

It prints:

Summary:
  mcx_commodity_futures confirmed tradeable=True: 2
  mcx_commodity_futures no_source tradeable=False: 1
  mcx_commodity_futures sibling_confirmed tradeable=True: 1
  ncdex_commodity_futures single_source tradeable=True: 1
Decisions deleted first for: 2026-09-28
Rows written:
  gold-2026-10: 100 confirmed tradeable=True sources={"groww_lot_size": "100", "kotak_contract_size": "100", "wisdom_capital_multiplier": "100"}
  gold-2026-12: 100 confirmed tradeable=True sources={"groww_lot_size": "100", "wisdom_capital_multiplier": "100"}
  gold-2027-02: 100 sibling_confirmed tradeable=True sources={"groww_lot_size": "100"}
  dhaniya-2026-11: 5 single_source tradeable=True sources={"stoxkart_lot_size": "5"}
  zinc-2026-10: none no_source tradeable=False sources={}
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeResolver/example_2_inspecting_conflicts_before_writing.py
"""Decides a date's contract sizes without writing them, to inspect the conflicts first, and then writes them.

`segments` lists every exchange-prefixed currency and commodity futures and options segment the resolver reads. `live_contracts` reads the contracts mapped on the date in those segments, and `decisions` reads every source and decides each contract without writing anything, which is useful to look at the conflicts before `write` replaces the date's stored decisions.

The engine is the same kind of in-memory stand-in as in the first program, so no PostgreSQL is needed. Two troubles are staged here. Stoxkart's file lists one USDINR option twice, with lots of 1000 and 2000, so the contract is a conflict and its recorded figure reads `1000|2000`, even though Kotak and Shoonya agree on 1000. And the MCX SILVERMIC contracts come in two confirmed sizes, as after a lot revision, so the far-month contract that only Groww lists stays `single_source` and untradeable instead of being settled by its siblings.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeResolver/example_2_inspecting_conflicts_before_writing.py
"""

import datetime
import types

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeResolver,
)


class StandInConnection:
    """A stand-in database connection that answers the resolver's queries from rows in memory.

    Attributes:
        database (StandInDatabase): The rows and the record of writes.
    """

    def __init__(self, database):
        """Holds the stand-in database.

        Args:
            database (StandInDatabase): The rows and the record of writes.

        Returns:
            None: This method returns nothing.
        """
        self.database = database

    def __enter__(self):
        """Opens the connection.

        Returns:
            StandInConnection: This connection.
        """
        return self

    def __exit__(self, error_type, error, traceback):
        """Closes the connection.

        Args:
            error_type (type | None): The exception type raised inside the block, if any.
            error (BaseException | None): The exception raised inside the block, if any.
            traceback (types.TracebackType | None): The traceback, if any.

        Returns:
            bool: False, so an exception is never swallowed.
        """
        return False

    def execute(self, statement, parameters):
        """Answers one query from the rows in memory, or records one write.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The statement.
            parameters (dict | list): The statement's parameters, or a list of them for a batch insert.

        Returns:
            list: The rows the query returns, empty for a write.
        """
        sql = str(statement)
        if sql.startswith('DELETE'):
            self.database.deleted_dates.append(parameters['mapping_date'])
            return []
        if sql.startswith('INSERT'):
            for row in parameters:
                self.database.written_rows.append(row)
            return []
        if 'AS units' in sql:
            return self.database.source_rows.get(parameters['broker'], [])
        return self.database.contract_rows


class StandInDatabase:
    """The rows the stand-in answers with, and the writes it has received.

    Attributes:
        contract_rows (list): The live contracts, as rows with `instrument_id`, `segment` and `underlying_symbol`.
        source_rows (dict): Broker name to the rows of `instrument_id` and `units` its source returns.
        deleted_dates (list): The mapping dates whose decisions were deleted.
        written_rows (list): The decision rows inserted.
    """

    def __init__(self, contract_rows, source_rows):
        """Holds the rows.

        Args:
            contract_rows (list): The live contracts.
            source_rows (dict): Broker name to its source's rows.

        Returns:
            None: This method returns nothing.
        """
        self.contract_rows = contract_rows
        self.source_rows = source_rows
        self.deleted_dates = []
        self.written_rows = []


class StandInEngine:
    """A stand-in SQLAlchemy engine whose connections all share one stand-in database.

    Attributes:
        database (StandInDatabase): The shared rows.
    """

    def __init__(self, database):
        """Holds the database.

        Args:
            database (StandInDatabase): The shared rows.

        Returns:
            None: This method returns nothing.
        """
        self.database = database

    def connect(self):
        """Opens a connection for reading.

        Returns:
            StandInConnection: A connection over the shared rows.
        """
        return StandInConnection(self.database)

    def begin(self):
        """Opens a connection inside a transaction.

        Returns:
            StandInConnection: A connection over the shared rows.
        """
        return StandInConnection(self.database)


class InspectingConflictsBeforeWritingExample:
    """Lists the segments, reads the live contracts, decides them and then writes the decisions.

    Attributes:
        database (StandInDatabase): The stand-in rows.
        engine (StandInEngine): The stand-in engine.
        resolver (ContractSizeResolver): The resolver being shown.
        mapping_date (datetime.date): The date being decided.
    """

    def __init__(self):
        """Builds the stand-in database and the resolver over it.

        Returns:
            None: This method returns nothing.
        """
        self.database = StandInDatabase(
            [
                self.contract('usdinr-option-83', 'nse_currency_options', 'USDINR'),
                self.contract('silvermic-2026-11', 'mcx_commodity_futures', 'SILVERMIC'),
                self.contract('silvermic-2027-02', 'mcx_commodity_futures', 'SILVERMIC'),
                self.contract('silvermic-2027-04', 'mcx_commodity_futures', 'SILVERMIC'),
            ],
            {
                'kotak': [
                    self.figure('usdinr-option-83', '1000'),
                    self.figure('silvermic-2026-11', '1'),
                    self.figure('silvermic-2027-02', '5'),
                ],
                'shoonya': [
                    self.figure('usdinr-option-83', '1000'),
                ],
                'stoxkart': [
                    self.figure('usdinr-option-83', '1000'),
                    self.figure('usdinr-option-83', '2000'),
                ],
                'groww': [
                    self.figure('silvermic-2026-11', '1'),
                    self.figure('silvermic-2027-02', '5'),
                    self.figure('silvermic-2027-04', '5'),
                ],
            },
        )
        self.engine = StandInEngine(self.database)
        self.resolver = ContractSizeResolver(self.engine)
        self.mapping_date = datetime.date(2026, 9, 28)

    def contract(self, instrument_id, segment, underlying_symbol):
        """Builds one live contract row.

        Args:
            instrument_id (str): The contract's instrument id.
            segment (str): Its exchange-prefixed segment.
            underlying_symbol (str): Its underlying's symbol.

        Returns:
            types.SimpleNamespace: The row.
        """
        return types.SimpleNamespace(
            instrument_id=instrument_id,
            segment=segment,
            underlying_symbol=underlying_symbol,
        )

    def figure(self, instrument_id, units):
        """Builds one source row.

        Args:
            instrument_id (str): The contract's instrument id.
            units (str): The size the source gives.

        Returns:
            types.SimpleNamespace: The row.
        """
        return types.SimpleNamespace(
            instrument_id=instrument_id,
            units=units,
        )

    def run(self):
        """Prints the segments, the live contracts and the decisions, then writes them.

        Returns:
            None: This method returns nothing.
        """
        segments = self.resolver.segments()
        print(f'{len(segments)} segments are read, starting with {segments[:4]}')
        with self.engine.connect() as connection:
            contracts = self.resolver.live_contracts(connection, self.mapping_date, segments)
        for instrument_id in contracts:
            segment, underlying_symbol = contracts[instrument_id]
            print(f'Live: {instrument_id} in {segment} on {underlying_symbol}')
        rows = self.resolver.decisions(self.mapping_date)
        print(f'Rows written before writing: {len(self.database.written_rows)}')
        for row in rows:
            print(f'  {row["instrument_id"]}: {row["status"]} tradeable={row["tradeable"]} sources={row["sources"]}')
        written = self.resolver.write(self.mapping_date, rows)
        print(f'Rows written: {written}')


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

It prints:

32 segments are read, starting with ['nse_currency_futures', 'bse_currency_futures', 'mcx_currency_futures', 'ncdex_currency_futures']
Live: usdinr-option-83 in nse_currency_options on USDINR
Live: silvermic-2026-11 in mcx_commodity_futures on SILVERMIC
Live: silvermic-2027-02 in mcx_commodity_futures on SILVERMIC
Live: silvermic-2027-04 in mcx_commodity_futures on SILVERMIC
Rows written before writing: 0
  usdinr-option-83: conflict tradeable=False sources={"kotak_contract_size": "1000", "shoonya_lot_size_times_multiplier": "1000", "stoxkart_lot_size": "1000|2000"}
  silvermic-2026-11: confirmed tradeable=True sources={"groww_lot_size": "1", "kotak_contract_size": "1"}
  silvermic-2027-02: confirmed tradeable=True sources={"groww_lot_size": "5", "kotak_contract_size": "5"}
  silvermic-2027-04: single_source tradeable=False sources={"groww_lot_size": "5"}
Rows written: 4

Methods:

Name Description
segments

Every exchange-prefixed currency and commodity futures and options segment.

live_contracts

Every contract mapped on the date in the segments that has not expired.

decisions

Decides every live contract's size for the date.

write

Replaces the date's decisions with the given rows, in one transaction.

run

Decides and writes every live contract's size for the date.

Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def __init__(self, engine=None):
    """
    Builds the resolver.

    Args:
        engine (sqlalchemy.engine.Engine | None): The engine to use. None builds one from the environment.

    Returns:
        None: This method returns nothing.
    """
    self.engine = engine or get_postgres_engine()
    self.decision = ContractSizeDecision()

segments

segments()

Every exchange-prefixed currency and commodity futures and options segment.

Returns:

Type Description
list

The segments.

Example usage

From example_2_inspecting_conflicts_before_writing.py
segments = self.resolver.segments()
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def segments(self):
    """
    Every exchange-prefixed currency and commodity futures and options segment.

    Returns:
        list: The segments.
    """
    segments = []
    for bare_segment, shape in CANONICAL_SEGMENTS:
        if shape not in ("future", "option"):
            continue
        if not bare_segment.startswith(("currency", "commodity")):
            continue
        for exchange in CANONICAL_EXCHANGES:
            segments.append(f"{exchange}_{bare_segment}")
    return segments

live_contracts

live_contracts(connection, mapping_date, segments)

Every contract mapped on the date in the segments that has not expired.

Parameters:

Name Type Description Default
connection Connection

An open connection.

required
mapping_date date

The mapping date.

required
segments list

The exchange-prefixed segments.

required

Returns:

Type Description
dict

Instrument ids to (segment, underlying symbol) tuples, where the underlying symbol may be None.

Example usage

From example_2_inspecting_conflicts_before_writing.py
contracts = self.resolver.live_contracts(connection, self.mapping_date, segments)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def live_contracts(self, connection, mapping_date, segments):
    """
    Every contract mapped on the date in the segments that has not expired.

    Args:
        connection (sqlalchemy.engine.Connection): An open connection.
        mapping_date (datetime.date): The mapping date.
        segments (list): The exchange-prefixed segments.

    Returns:
        dict: Instrument ids to `(segment, underlying symbol)` tuples, where the underlying symbol may be None.
    """
    statement = text(
        f"SELECT DISTINCT m.instrument_id::text AS instrument_id, i.segment, i.underlying_symbol "
        f"FROM {tables.BROKER_MAPPINGS} m "
        f"JOIN {tables.MASTER} i ON i.instrument_id = m.instrument_id "
        "WHERE m.mapping_date = :mapping_date "
        "AND i.segment = ANY(:segments) "
        "AND (i.expiry_date IS NULL OR i.expiry_date >= :mapping_date)"
    )
    contracts = {}
    rows = connection.execute(
        statement,
        {
            "mapping_date": mapping_date,
            "segments": segments,
        },
    )
    for row in rows:
        contracts[row.instrument_id] = (
            row.segment,
            row.underlying_symbol,
        )
    return contracts

decisions

decisions(mapping_date)

Decides every live contract's size for the date.

Parameters:

Name Type Description Default
mapping_date date

The mapping date.

required

Returns:

Type Description
list

One dictionary per contract with instrument_id, mapping_date, segment, units_per_lot, status, tradeable and sources.

Example usage

From example_2_inspecting_conflicts_before_writing.py
rows = self.resolver.decisions(self.mapping_date)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def decisions(self, mapping_date):
    """
    Decides every live contract's size for the date.

    Args:
        mapping_date (datetime.date): The mapping date.

    Returns:
        list: One dictionary per contract with `instrument_id`, `mapping_date`, `segment`, `units_per_lot`, `status`, `tradeable` and `sources`.
    """
    segments = self.segments()
    figures_by_contract = collections.defaultdict(dict)
    contradictions_by_contract = collections.defaultdict(dict)
    with self.engine.connect() as connection:
        contracts = self.live_contracts(connection, mapping_date, segments)
        for source_class in self.SOURCES:
            source = source_class()
            sizes = source.read(connection, mapping_date, segments)
            for instrument_id, distinct_sizes in sizes.items():
                if len(distinct_sizes) == 1:
                    figures_by_contract[instrument_id][source.NAME] = next(iter(distinct_sizes))
                else:
                    contradictions_by_contract[instrument_id][source.NAME] = distinct_sizes
    decided = {}
    confirmed_sizes = collections.defaultdict(set)
    for instrument_id, (segment, underlying_symbol) in contracts.items():
        figures = figures_by_contract.get(instrument_id, {})
        contradictions = contradictions_by_contract.get(instrument_id, {})
        units_per_lot, status, tradeable = self.decision.decide(
            segment,
            figures,
            set(contradictions),
        )
        decided[instrument_id] = (units_per_lot, status, tradeable)
        if status == "confirmed" and underlying_symbol is not None:
            confirmed_sizes[(segment, underlying_symbol)].add(units_per_lot)
    rows = []
    for instrument_id, (segment, underlying_symbol) in contracts.items():
        figures = figures_by_contract.get(instrument_id, {})
        units_per_lot, status, tradeable = decided[instrument_id]
        if underlying_symbol is not None:
            status, tradeable = self.decision.settle_by_siblings(
                segment,
                units_per_lot,
                status,
                tradeable,
                confirmed_sizes.get((segment, underlying_symbol), set()),
            )
        recorded_figures = {}
        for source_name, units in figures.items():
            recorded_figures[source_name] = format(units, "f")
        for source_name, distinct_sizes in contradictions_by_contract.get(instrument_id, {}).items():
            texts = []
            for units in sorted(distinct_sizes):
                texts.append(format(units, "f"))
            recorded_figures[source_name] = "|".join(texts)
        rows.append({
            "instrument_id": instrument_id,
            "mapping_date": mapping_date,
            "segment": segment,
            "units_per_lot": units_per_lot,
            "status": status,
            "tradeable": tradeable,
            "sources": json.dumps(recorded_figures, sort_keys=True),
        })
    return rows

write

write(mapping_date, rows)

Replaces the date's decisions with the given rows, in one transaction.

Parameters:

Name Type Description Default
mapping_date date

The mapping date.

required
rows list

The decisions, as decisions returns them.

required

Returns:

Type Description
int

The number of rows written.

Example usage

From example_2_inspecting_conflicts_before_writing.py
written = self.resolver.write(self.mapping_date, rows)
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def write(self, mapping_date, rows):
    """
    Replaces the date's decisions with the given rows, in one transaction.

    Args:
        mapping_date (datetime.date): The mapping date.
        rows (list): The decisions, as `decisions` returns them.

    Returns:
        int: The number of rows written.
    """
    delete_statement = text(
        f"DELETE FROM {tables.CONTRACT_SIZES} "
        "WHERE mapping_date = :mapping_date"
    )
    insert_statement = text(
        f"INSERT INTO {tables.CONTRACT_SIZES} "
        "(instrument_id, mapping_date, segment, units_per_lot, status, tradeable, sources) "
        "VALUES (CAST(:instrument_id AS uuid), :mapping_date, :segment, :units_per_lot, :status, :tradeable, "
        "CAST(:sources AS jsonb))"
    )
    with self.engine.begin() as connection:
        connection.execute(
            delete_statement,
            {
                "mapping_date": mapping_date,
            },
        )
        for start in range(0, len(rows), self.WRITE_BATCH_ROWS):
            batch = rows[start:start + self.WRITE_BATCH_ROWS]
            connection.execute(insert_statement, batch)
    return len(rows)

run

run(mapping_date)

Decides and writes every live contract's size for the date.

Parameters:

Name Type Description Default
mapping_date date

The mapping date.

required

Returns:

Type Description
Counter

(segment, status, tradeable) to the number of contracts.

Example usage

From example_1_deciding_and_writing_a_date.py
summary = self.resolver.run(datetime.date(2026, 9, 28))
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def run(self, mapping_date):
    """
    Decides and writes every live contract's size for the date.

    Args:
        mapping_date (datetime.date): The mapping date.

    Returns:
        collections.Counter: `(segment, status, tradeable)` to the number of contracts.
    """
    rows = self.decisions(mapping_date)
    self.write(mapping_date, rows)
    summary = collections.Counter()
    for row in rows:
        summary[(row["segment"], row["status"], row["tradeable"])] += 1
    return summary

ContractSizeCommand

The command line: decide a date's contract sizes and print how many contracts each segment has in each status.

Example programs

examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeCommand/example_1_a_malformed_date.py
"""Runs the contract size command line with a date it cannot read, and shows that it stops before touching the database.

`ContractSizeCommand.run` reads its arguments from `sys.argv`, as `python -m stock_brokers.instruments.mapping.utilities.contract_sizes --date 2026-09-15` would pass them. A `--date` that is not a real `YYYY-MM-DD` date is refused with a message and exit code 2, which the project uses for a bad argument, and no resolver or database engine is built.

The program sets `sys.argv` itself to the date `2026-09-31`, a day September does not have, so it needs no database and no shell arguments.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeCommand/example_1_a_malformed_date.py
"""

import sys

from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeCommand,
)


class MalformedDateExample:
    """Runs the command with an impossible date and prints its exit code.

    Attributes:
        command (ContractSizeCommand): The command being shown.
    """

    def __init__(self):
        """Builds the command and sets the arguments it will read.

        Returns:
            None: This method returns nothing.
        """
        sys.argv = [
            'contract_sizes',
            '--date',
            '2026-09-31',
        ]
        self.command = ContractSizeCommand()

    def run(self):
        """Runs the command and prints the exit code it returns.

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


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

It prints:

--date needs a date as YYYY-MM-DD, not '2026-09-31'.
Exit code: 2
examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeCommand/example_2_running_for_a_date.py
"""Runs the contract size command line for one date, the way the daily mapping run does, against a stand-in database.

`ContractSizeCommand.run` reads `--date` from `sys.argv`, builds a `ContractSizeResolver` over the configured PostgreSQL engine, decides and writes the date's contract sizes, and prints how many contracts each segment has in each status.

The command builds its engine through `get_postgres_engine`, so this program replaces that function in the `contract_sizes` module with one returning an in-memory stand-in engine. That keeps the real command and the real resolver running while no database is reached. The stand-in holds three MCX CRUDEOIL futures, two confirmed at 100 barrels by Wisdom Capital and Kotak and one far month listed by Wisdom Capital only, and one BSE USDINR future that only Stoxkart lists.

Notice in the printed table that the far-month CRUDEOIL future is `sibling_confirmed` and that the BSE currency future is tradeable on its single source.

Run it from the project root:

    python examples/stock_brokers/instruments/mapping/utilities/contract_sizes/ContractSizeCommand/example_2_running_for_a_date.py
"""

import sys
import types

from stock_brokers.instruments.mapping.utilities import contract_sizes
from stock_brokers.instruments.mapping.utilities.contract_sizes import (
    ContractSizeCommand,
)


class StandInConnection:
    """A stand-in database connection that answers the resolver's queries from rows in memory.

    Attributes:
        database (StandInDatabase): The rows and the record of writes.
    """

    def __init__(self, database):
        """Holds the stand-in database.

        Args:
            database (StandInDatabase): The rows and the record of writes.

        Returns:
            None: This method returns nothing.
        """
        self.database = database

    def __enter__(self):
        """Opens the connection.

        Returns:
            StandInConnection: This connection.
        """
        return self

    def __exit__(self, error_type, error, traceback):
        """Closes the connection.

        Args:
            error_type (type | None): The exception type raised inside the block, if any.
            error (BaseException | None): The exception raised inside the block, if any.
            traceback (types.TracebackType | None): The traceback, if any.

        Returns:
            bool: False, so an exception is never swallowed.
        """
        return False

    def execute(self, statement, parameters):
        """Answers one query from the rows in memory, or records one write.

        Args:
            statement (sqlalchemy.sql.elements.TextClause): The statement.
            parameters (dict | list): The statement's parameters, or a list of them for a batch insert.

        Returns:
            list: The rows the query returns, empty for a write.
        """
        sql = str(statement)
        if sql.startswith('DELETE'):
            self.database.deleted_dates.append(parameters['mapping_date'])
            return []
        if sql.startswith('INSERT'):
            for row in parameters:
                self.database.written_rows.append(row)
            return []
        if 'AS units' in sql:
            return self.database.source_rows.get(parameters['broker'], [])
        return self.database.contract_rows


class StandInDatabase:
    """The rows the stand-in answers with, and the writes it has received.

    Attributes:
        contract_rows (list): The live contracts, as rows with `instrument_id`, `segment` and `underlying_symbol`.
        source_rows (dict): Broker name to the rows of `instrument_id` and `units` its source returns.
        deleted_dates (list): The mapping dates whose decisions were deleted.
        written_rows (list): The decision rows inserted.
    """

    def __init__(self, contract_rows, source_rows):
        """Holds the rows.

        Args:
            contract_rows (list): The live contracts.
            source_rows (dict): Broker name to its source's rows.

        Returns:
            None: This method returns nothing.
        """
        self.contract_rows = contract_rows
        self.source_rows = source_rows
        self.deleted_dates = []
        self.written_rows = []


class StandInEngine:
    """A stand-in SQLAlchemy engine whose connections all share one stand-in database.

    Attributes:
        database (StandInDatabase): The shared rows.
    """

    def __init__(self, database):
        """Holds the database.

        Args:
            database (StandInDatabase): The shared rows.

        Returns:
            None: This method returns nothing.
        """
        self.database = database

    def connect(self):
        """Opens a connection for reading.

        Returns:
            StandInConnection: A connection over the shared rows.
        """
        return StandInConnection(self.database)

    def begin(self):
        """Opens a connection inside a transaction.

        Returns:
            StandInConnection: A connection over the shared rows.
        """
        return StandInConnection(self.database)


class RunningForADateExample:
    """Runs the command for 2026-09-28 over a stand-in database and prints its exit code and what it wrote.

    Attributes:
        database (StandInDatabase): The stand-in rows.
        command (ContractSizeCommand): The command being shown.
    """

    def __init__(self):
        """Builds the stand-in database, points the command's engine at it and sets the arguments.

        Returns:
            None: This method returns nothing.
        """
        self.database = StandInDatabase(
            [
                self.contract('crudeoil-2026-10', 'mcx_commodity_futures', 'CRUDEOIL'),
                self.contract('crudeoil-2026-11', 'mcx_commodity_futures', 'CRUDEOIL'),
                self.contract('crudeoil-2027-03', 'mcx_commodity_futures', 'CRUDEOIL'),
                self.contract('bse-usdinr-2026-10', 'bse_currency_futures', 'USDINR'),
            ],
            {
                'wisdom_capital': [
                    self.figure('crudeoil-2026-10', '100'),
                    self.figure('crudeoil-2026-11', '100'),
                    self.figure('crudeoil-2027-03', '100'),
                ],
                'kotak': [
                    self.figure('crudeoil-2026-10', '100'),
                    self.figure('crudeoil-2026-11', '100'),
                ],
                'stoxkart': [
                    self.figure('bse-usdinr-2026-10', '1000'),
                ],
            },
        )
        contract_sizes.get_postgres_engine = self.stand_in_engine
        sys.argv = [
            'contract_sizes',
            '--date',
            '2026-09-28',
        ]
        self.command = ContractSizeCommand()

    def stand_in_engine(self):
        """Builds the stand-in engine the command uses in place of the configured one.

        Returns:
            StandInEngine: An engine over the stand-in database.
        """
        return StandInEngine(self.database)

    def contract(self, instrument_id, segment, underlying_symbol):
        """Builds one live contract row.

        Args:
            instrument_id (str): The contract's instrument id.
            segment (str): Its exchange-prefixed segment.
            underlying_symbol (str): Its underlying's symbol.

        Returns:
            types.SimpleNamespace: The row.
        """
        return types.SimpleNamespace(
            instrument_id=instrument_id,
            segment=segment,
            underlying_symbol=underlying_symbol,
        )

    def figure(self, instrument_id, units):
        """Builds one source row.

        Args:
            instrument_id (str): The contract's instrument id.
            units (str): The size the source gives.

        Returns:
            types.SimpleNamespace: The row.
        """
        return types.SimpleNamespace(
            instrument_id=instrument_id,
            units=units,
        )

    def run(self):
        """Runs the command and prints its exit code and how many rows it wrote.

        Returns:
            None: This method returns nothing.
        """
        exit_code = self.command.run()
        print(f'Exit code: {exit_code}')
        print(f'Rows written: {len(self.database.written_rows)}')


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

It prints:

contract sizes for 2026-09-28
  segment                          status         tradeable  contracts
  bse_currency_futures             single_source  True               1
  mcx_commodity_futures            confirmed      True               2
  mcx_commodity_futures            sibling_confirmed True               1
Exit code: 0
Rows written: 4

Methods:

Name Description
run

Parses the arguments, runs the resolver and prints the summary.

run

run()

Parses the arguments, runs the resolver and prints the summary.

Returns:

Type Description
int

The exit code: 0 on success, 2 for a malformed date.

Example usage

From example_1_a_malformed_date.py
exit_code = self.command.run()
From example_2_running_for_a_date.py
exit_code = self.command.run()
Source code in stock_brokers/instruments/mapping/utilities/contract_sizes.py
def run(self):
    """
    Parses the arguments, runs the resolver and prints the summary.

    Returns:
        int: The exit code: 0 on success, 2 for a malformed date.
    """
    parser = argparse.ArgumentParser(
        description="Decide the contract size of every live currency and commodity derivative for a mapping date.",
    )
    parser.add_argument(
        "--date",
        help="the mapping date, as YYYY-MM-DD. Today when omitted.",
    )
    arguments = parser.parse_args()
    mapping_date = datetime.date.today()
    if arguments.date:
        try:
            mapping_date = datetime.date.fromisoformat(arguments.date)
        except ValueError:
            print(f"--date needs a date as YYYY-MM-DD, not {arguments.date!r}.")
            return 2
    summary = ContractSizeResolver().run(mapping_date)
    print(f"contract sizes for {mapping_date}")
    print(f"  {'segment':<32} {'status':<14} {'tradeable':<10} {'contracts':>9}")
    for segment, status, tradeable in sorted(summary):
        count = summary[(segment, status, tradeable)]
        print(f"  {segment:<32} {status:<14} {str(tradeable):<10} {count:>9}")
    return 0