Skip to content

tradingmachine.asset_baskets.member_resolver

Turns rows that name instruments into basket members with one list request to UBI.

A stored basket, a CSV file and the account's holdings all describe their instruments as plain rows, each naming an instrument by instrument_id or by exchange, segment and identity fields, beside a weight or a quantity. MemberResolver.resolve sends every row to POST /api/instruments/details at once and builds each member's instrument from its entry of the answer, so a basket of five hundred instruments costs one request rather than five hundred.

Typical usage example:

resolver = member_resolver.MemberResolver() members = resolver.resolve( [ {"exchange": "nse", "segment": "equities", "symbol": "INFY", "weight": 0.6}, {"exchange": "nse", "segment": "equities", "symbol": "TCS", "weight": 0.4}, ] )

DETAILS_PATH module-attribute

DETAILS_PATH = '/api/instruments/details'

IDENTITY_FIELDS module-attribute

IDENTITY_FIELDS = ['exchange', 'segment', 'symbol', 'underlying_symbol', 'expiry_date', 'strike_price', 'option_type']

SUCCESS_STATUS module-attribute

SUCCESS_STATUS = 200

MemberResolver

MemberResolver(unified_broker_interface=None)

A builder of basket members from rows that name their instruments.

Attributes:

Name Type Description
unified_broker_interface

The client.UnifiedBrokerInterface the details request is sent through.

Initialises the resolver with the client it sends requests through.

Parameters:

Name Type Description Default
unified_broker_interface UnifiedBrokerInterface | None

The client.UnifiedBrokerInterface to use, or None to share the one every instrument uses.

None

Raises:

Type Description
ValueError

No client was given and the shared client's base url or MongoDB credentials are not configured.

Source code in src/tradingmachine/asset_baskets/member_resolver.py
def __init__(
    self,
    unified_broker_interface: client.UnifiedBrokerInterface | None = None,
):
    """Initialises the resolver with the client it sends requests through.

    Args:
        unified_broker_interface: The client.UnifiedBrokerInterface to use, or None to share the one every instrument uses.

    Raises:
        ValueError: No client was given and the shared client's base url or MongoDB credentials are not configured.
    """
    if unified_broker_interface is None:
        unified_broker_interface = (
            instruments.Instrument.shared_unified_broker_interface()
        )
    self.unified_broker_interface = unified_broker_interface

resolve

resolve(rows)

Looks every row's instrument up in UBI and returns one member per row.

An index becomes a tradingmachine.assets.instruments.NonTradeableInstrument and anything else a tradingmachine.assets.instruments.TradeableInstrument. A row that names its instrument by instrument_id is looked up by that alone, and any other row by the identity fields it has.

Parameters:

Name Type Description Default
rows list[dict]

A list of dicts, each with instrument_id or exchange, segment and the identity fields its shape needs, and optionally weight, quantity and average_price.

required

Returns:

Type Description
list[BasketMember]

A list of basket_member.BasketMember, in the order of rows.

Raises:

Type Description
BasketMemberError

rows is empty, or UBI could not find one or more of the instruments, all of which the message lists.

UnifiedBrokerInterfaceError

The whole request was refused by, or failed on the way to, UBI.

Examples:

Build weighted members for three shares in one request:

from tradingmachine.asset_baskets import member_resolver

rows = [
    {
        "exchange": "nse",
        "segment": "equities",
        "symbol": "INFY",
        "weight": 0.5,
    },
    {
        "exchange": "nse",
        "segment": "equities",
        "symbol": "TCS",
        "weight": 0.3,
    },
    {
        "exchange": "nse",
        "segment": "equity_indices",
        "symbol": "NIFTY",
        "weight": 0.2,
    },
]
for member in member_resolver.MemberResolver().resolve(rows):
    print(member, type(member.instrument).__name__)

See every unknown symbol listed in one error:

from tradingmachine.asset_baskets import exceptions
from tradingmachine.asset_baskets import member_resolver

rows = [
    {
        "exchange": "nse",
        "segment": "equities",
        "symbol": "INFY",
    },
    {
        "exchange": "nse",
        "segment": "equities",
        "symbol": "NOSUCHSHARE",
    },
]
try:
    member_resolver.MemberResolver().resolve(rows)
except exceptions.BasketMemberError as error:
    print(error)
Source code in src/tradingmachine/asset_baskets/member_resolver.py
def resolve(self, rows: list[dict]) -> list[basket_member.BasketMember]:
    """Looks every row's instrument up in UBI and returns one member per row.

    An index becomes a tradingmachine.assets.instruments.NonTradeableInstrument and anything else a tradingmachine.assets.instruments.TradeableInstrument. A row that names its instrument by `instrument_id` is looked up by that alone, and any other row by the identity fields it has.

    Args:
        rows: A list of dicts, each with `instrument_id` or `exchange`, `segment` and the identity fields its shape needs, and optionally `weight`, `quantity` and `average_price`.

    Returns:
        A list of basket_member.BasketMember, in the order of rows.

    Raises:
        BasketMemberError: rows is empty, or UBI could not find one or more of the instruments, all of which the message lists.
        UnifiedBrokerInterfaceError: The whole request was refused by, or failed on the way to, UBI.

    Examples:
        Build weighted members for three shares in one request:

        ```python
        from tradingmachine.asset_baskets import member_resolver

        rows = [
            {
                "exchange": "nse",
                "segment": "equities",
                "symbol": "INFY",
                "weight": 0.5,
            },
            {
                "exchange": "nse",
                "segment": "equities",
                "symbol": "TCS",
                "weight": 0.3,
            },
            {
                "exchange": "nse",
                "segment": "equity_indices",
                "symbol": "NIFTY",
                "weight": 0.2,
            },
        ]
        for member in member_resolver.MemberResolver().resolve(rows):
            print(member, type(member.instrument).__name__)
        ```

        See every unknown symbol listed in one error:

        ```python
        from tradingmachine.asset_baskets import exceptions
        from tradingmachine.asset_baskets import member_resolver

        rows = [
            {
                "exchange": "nse",
                "segment": "equities",
                "symbol": "INFY",
            },
            {
                "exchange": "nse",
                "segment": "equities",
                "symbol": "NOSUCHSHARE",
            },
        ]
        try:
            member_resolver.MemberResolver().resolve(rows)
        except exceptions.BasketMemberError as error:
            print(error)
        ```
    """
    if not rows:
        raise exceptions.BasketMemberError("There are no rows to resolve")
    instrument_lookups = []
    for row in rows:
        instrument_lookups.append(self._lookup_for(row))
    response = self.unified_broker_interface.post(
        DETAILS_PATH,
        body={
            "instruments": instrument_lookups,
        },
    )
    members = []
    failures = []
    for row, result in zip(rows, response["results"]):
        if result["status"] != SUCCESS_STATUS:
            failures.append(
                f"{instrument_lookups[result['request_index']]}: {result.get('error')}"
            )
            continue
        instrument = self._instrument_from(result["data"])
        members.append(
            basket_member.BasketMember(
                instrument,
                weight=row.get("weight"),
                quantity=row.get("quantity"),
                average_price=row.get("average_price"),
            )
        )
    if failures:
        raise exceptions.BasketMemberError(
            f"UBI could not find {len(failures)} of {len(rows)} instruments: {'; '.join(failures)}"
        )
    return members

resolve_one

resolve_one(row)

Looks one row's instrument up in UBI.

Parameters:

Name Type Description Default
row dict

A dict with instrument_id or exchange, segment and the identity fields its shape needs.

required

Returns:

Type Description
Instrument

The tradingmachine.assets.instruments.NonTradeableInstrument for an index, or the tradingmachine.assets.instruments.TradeableInstrument for anything else.

Raises:

Type Description
BasketMemberError

UBI could not find the instrument.

UnifiedBrokerInterfaceError

The request was refused by, or failed on the way to, UBI.

Examples:

Look one share up by exchange, segment and symbol:

from tradingmachine.asset_baskets import member_resolver

resolver = member_resolver.MemberResolver()
idea = resolver.resolve_one(
    {
        "exchange": "nse",
        "segment": "equities",
        "symbol": "IDEA",
    }
)
print(idea.instrument_id, idea.last_price)

Look an index up again by the instrument id another lookup gave:

from tradingmachine.asset_baskets import member_resolver
from tradingmachine.assets import equities

nifty = equities.EquityIndex(exchange="nse", symbol="NIFTY")
resolver = member_resolver.MemberResolver()
same_index = resolver.resolve_one(
    {
        "instrument_id": nifty.instrument_id,
    }
)
print(type(same_index).__name__, same_index.symbol)
Source code in src/tradingmachine/asset_baskets/member_resolver.py
def resolve_one(self, row: dict) -> instruments.Instrument:
    """Looks one row's instrument up in UBI.

    Args:
        row: A dict with `instrument_id` or `exchange`, `segment` and the identity fields its shape needs.

    Returns:
        The tradingmachine.assets.instruments.NonTradeableInstrument for an index, or the tradingmachine.assets.instruments.TradeableInstrument for anything else.

    Raises:
        BasketMemberError: UBI could not find the instrument.
        UnifiedBrokerInterfaceError: The request was refused by, or failed on the way to, UBI.

    Examples:
        Look one share up by exchange, segment and symbol:

        ```python
        from tradingmachine.asset_baskets import member_resolver

        resolver = member_resolver.MemberResolver()
        idea = resolver.resolve_one(
            {
                "exchange": "nse",
                "segment": "equities",
                "symbol": "IDEA",
            }
        )
        print(idea.instrument_id, idea.last_price)
        ```

        Look an index up again by the instrument id another lookup gave:

        ```python
        from tradingmachine.asset_baskets import member_resolver
        from tradingmachine.assets import equities

        nifty = equities.EquityIndex(exchange="nse", symbol="NIFTY")
        resolver = member_resolver.MemberResolver()
        same_index = resolver.resolve_one(
            {
                "instrument_id": nifty.instrument_id,
            }
        )
        print(type(same_index).__name__, same_index.symbol)
        ```
    """
    members = self.resolve(
        [
            row,
        ]
    )
    return members[0].instrument

Example programs

Turn rows naming shares, a fund and an index into basket members with one request

Run it with .venv/bin/python examples/asset_baskets/member_resolver/member_resolver/resolve_mixed_rows.py.

"""Turn rows naming shares, a fund and an index into basket members with one request.

The program describes a small allocation as plain rows, the way a stored basket or a CSV file does: two shares and a gold fund by exchange, segment and symbol, and the NIFTY index by the instrument id a previous lookup gave. It resolves every row in one request to UBI and prints each member with the class of instrument it became and its last price.

Typical usage example:

  .venv/bin/python examples/asset_baskets/member_resolver/member_resolver/resolve_mixed_rows.py
"""

from tradingmachine.asset_baskets import member_resolver


class ResolveMixedRows:
    """A resolver run over rows of several kinds.

    Attributes:
        resolver: The tradingmachine.asset_baskets.member_resolver.MemberResolver that looks the rows up.
    """

    def __init__(self):
        """Creates the resolver on the shared UBI client.

        Raises:
            ValueError: The shared UBI client is not configured.
        """
        self.resolver = member_resolver.MemberResolver()

    def rows(self) -> list[dict]:
        """Builds the rows, looking the NIFTY index up first to name it by its id.

        Returns:
            A list of dicts, each naming one instrument and giving its weight.

        Raises:
            tradingmachine.asset_baskets.exceptions.BasketMemberError: UBI could not find the NIFTY index.
        """
        nifty = self.resolver.resolve_one(
            {
                "exchange": "nse",
                "segment": "equity_indices",
                "symbol": "NIFTY",
            }
        )
        return [
            {
                "exchange": "nse",
                "segment": "equities",
                "symbol": "INFY",
                "weight": 40,
            },
            {
                "exchange": "nse",
                "segment": "equities",
                "symbol": "HDFCBANK",
                "weight": 30,
            },
            {
                "exchange": "nse",
                "segment": "exchange_traded_funds",
                "symbol": "GOLDBEES",
                "weight": 20,
            },
            {
                "instrument_id": nifty.instrument_id,
                "weight": 10,
            },
        ]

    def run(self) -> None:
        """Resolves the rows and prints each member.

        Returns:
            None.

        Raises:
            tradingmachine.asset_baskets.exceptions.BasketMemberError: UBI could not find one of the instruments.
            tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused a request or could not be reached.
        """
        members = self.resolver.resolve(self.rows())
        for member in members:
            kind = type(member.instrument).__name__
            print(
                f"{member.label:<16} {kind:<24} weight {member.weight:>3}  last {member.instrument.last_price}"
            )


if __name__ == "__main__":
    ResolveMixedRows().run()

Resolve a list of symbols, report the ones UBI does not know, and keep the rest

Run it with .venv/bin/python examples/asset_baskets/member_resolver/member_resolver/skip_unknown_symbols.py.

"""Resolve a list of symbols, report the ones UBI does not know, and keep the rest.

The program tries to resolve a watch list typed by hand, which contains one misspelt symbol and one that was renamed. `MemberResolver.resolve` refuses the whole list and names every failure in one error, so the program then resolves each symbol on its own with `resolve_one`, keeps the ones UBI knows, and prints both groups.

Typical usage example:

  .venv/bin/python examples/asset_baskets/member_resolver/member_resolver/skip_unknown_symbols.py
"""

from tradingmachine.asset_baskets import exceptions
from tradingmachine.asset_baskets import member_resolver

SYMBOLS = [
    "INFY",
    "TCS",
    "INFOSYS",
    "SBIN",
    "HDFC",
]


class SkipUnknownSymbols:
    """A resolver run that separates the symbols UBI knows from the ones it does not.

    Attributes:
        resolver: The tradingmachine.asset_baskets.member_resolver.MemberResolver that looks the symbols up.
    """

    def __init__(self):
        """Creates the resolver on the shared UBI client.

        Raises:
            ValueError: The shared UBI client is not configured.
        """
        self.resolver = member_resolver.MemberResolver()

    def row_for(self, symbol: str) -> dict:
        """Describes one NSE share as a row.

        Args:
            symbol: The str NSE symbol.

        Returns:
            A dict with `exchange`, `segment` and `symbol`.

        Raises:
            Nothing.
        """
        return {
            "exchange": "nse",
            "segment": "equities",
            "symbol": symbol,
        }

    def run(self) -> None:
        """Tries the whole list, then each symbol alone, and prints what was found.

        Returns:
            None.

        Raises:
            tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused a request or could not be reached.
        """
        rows = []
        for symbol in SYMBOLS:
            rows.append(self.row_for(symbol))
        try:
            self.resolver.resolve(rows)
            print("Every symbol is known.")
            return
        except exceptions.BasketMemberError as error:
            print(f"The whole list was refused: {error}")
        known = []
        unknown = []
        for symbol in SYMBOLS:
            try:
                instrument = self.resolver.resolve_one(self.row_for(symbol))
            except exceptions.BasketMemberError:
                unknown.append(symbol)
                continue
            known.append(instrument.symbol)
        print(f"Known: {known}")
        print(f"Unknown: {unknown}")


if __name__ == "__main__":
    SkipUnknownSymbols().run()