Skip to content

The instrument model

Every instrument in the library is an object of one class, and the class says what kind of contract it is. This page shows how those classes are layered, what each layer adds, how an instrument is looked up in UBI, and how the asset baskets, the synthetic order classes and the account sit beside the instruments rather than inside them.

The levels of class

The instrument classes form three levels, with a fourth for contracts. The base, Instrument, holds everything any instrument can do, including 209 analysis methods it inherits from fourteen analysis classes. The middle level splits instruments into those that can be traded and indices, which cannot. Below TradeableInstrument, five derivative base classes hold what every futures or option contract shares. The bottom level is 27 family classes, one per UBI segment, which is what you actually construct.

The class diagram below shows the levels above the family classes, with their main members. The fourteen analysis classes are drawn as one box to keep the diagram readable; each is a separate class in tradingmachine.assets.analysis, and all fourteen share the base PriceAnalysis.

classDiagram
    direction TB
    class PriceAnalysis {
        shared candle helpers
    }
    class AnalysisClasses {
        <<fourteen classes>>
        PriceStatistics
        OverlapStudies
        MomentumIndicators
        VolumeIndicators
        CycleIndicators
        PriceTransforms
        VolatilityIndicators
        StatisticFunctions
        MathTransforms
        MathOperators
        CandlestickPatterns
        Signals
        StrategyBacktests
        PerformanceMeasures
    }
    class Instrument {
        instrument_id
        exchange, segment, shape
        symbol, underlying_symbol
        expiry_date, strike_price, option_type
        lot_size, tick_size, carried_by
        quote
        last_price
        ohlc
        prices(interval, from_date, to_date, days, adjusted)
        shared_unified_broker_interface()
    }
    class TradeableInstrument {
        bids, offers, best_bid, best_offer
        mid_price, bid_offer_spread
        orders, open_orders, trades
        net_positions, day_positions
        positions_value, positions_pnl
        place_order(...)
        modify_order(...)
        cancel_order(...)
        32 price wrappers
        add_to_position(...)
        reduce_position(...)
        liquidate_position(...)
    }
    class NonTradeableInstrument {
        accepts only an index
        constituents
    }
    class Derivative {
        underlying_segment
        days_to_expiry, expired
        expiry_kind, next_expiry
        underlying, underlying_price
        contract_value
    }
    class Futures {
        basis, basis_percent, cost_of_carry
        expiries(), contracts()
    }
    class Option {
        intrinsic_value, time_value
        moneyness_percent, breakeven_price
        implied_volatility(), greeks()
        expiries(), strikes(), chain()
    }
    class IndexFutures {
        underlying is an index
    }
    class IndexOption {
        underlying is an index
    }
    PriceAnalysis <|-- AnalysisClasses
    AnalysisClasses <|-- Instrument
    Instrument <|-- TradeableInstrument
    Instrument <|-- NonTradeableInstrument
    TradeableInstrument <|-- Derivative
    Derivative <|-- Futures
    Derivative <|-- Option
    Futures <|-- IndexFutures
    Option <|-- IndexOption

The diagram lists only the main members of TradeableInstrument and the derivative classes. The Python API tab documents every one of them, Derivatives the contract members in particular, and Analysis the inherited analysis methods.

The 27 family classes

The flowchart below shows the bottom level, grouped by the module each class lives in. The cash classes and the funds inherit TradeableInstrument directly, the four index classes are the only ones built on NonTradeableInstrument, and the sixteen futures and option classes inherit Futures, Option, IndexFutures or IndexOption, which all sit on TradeableInstrument through Derivative.

flowchart LR
    T["TradeableInstrument"]
    N["NonTradeableInstrument"]
    FB["Futures"]
    OB["Option"]
    IF["IndexFutures"]
    IO["IndexOption"]
    subgraph EQ["equities.py"]
        E1["Equity"]
        E2["EquityFutures"]
        E3["EquityOption"]
        E4["EquityIndex"]
        E5["EquityIndexFutures"]
        E6["EquityIndexOption"]
    end
    subgraph FI["fixed_income.py"]
        F1["FixedIncome"]
        F2["FixedIncomeFutures"]
        F3["FixedIncomeOption"]
        F4["FixedIncomeIndex"]
        F5["FixedIncomeIndexFutures"]
        F6["FixedIncomeIndexOption"]
    end
    subgraph CO["commodities.py"]
        C1["Commodity"]
        C2["CommodityFutures"]
        C3["CommodityOption"]
        C4["CommodityIndex"]
        C5["CommodityIndexFutures"]
        C6["CommodityIndexOption"]
    end
    subgraph CU["currencies.py"]
        U1["Currency"]
        U2["CurrencyFutures"]
        U3["CurrencyOption"]
        U4["CurrencyIndex"]
        U5["CurrencyIndexFutures"]
        U6["CurrencyIndexOption"]
    end
    subgraph FU["funds.py and mutual_funds.py"]
        D1["ExchangeTradedFund"]
        D2["InvestmentTrust"]
        D3["MutualFund"]
    end
    T --> E1 & F1 & C1 & U1
    T --> D1 & D2 & D3
    T --> FB & OB
    FB --> IF
    OB --> IO
    FB --> E2 & F2 & C2 & U2
    OB --> E3 & F3 & C3 & U3
    IF --> E5 & F5 & C5 & U5
    IO --> E6 & F6 & C6 & U6
    N --> E4 & F4 & C4 & U4

The table below counts the classes in each module and says what the module's cash class can do that the others cannot.

Module Classes Tradeable On NonTradeableInstrument Holdings members on
equities.py 6 5 1 Equity
fixed_income.py 6 5 1 FixedIncome
commodities.py 6 5 1 none
currencies.py 6 5 1 none
funds.py 2 2 0 ExchangeTradedFund, InvestmentTrust
mutual_funds.py 1 1 0 MutualFund
Total 27 23 4 five classes

Commodities and currencies have no holdings members because UBI can only report a holding for a segment in its CASH_SEGMENTS, which excludes both. The Asset classes tab covers each family's own traps.

What lives at each level

Each level adds only what is true of every class below it. The table below lists what each level holds and why it sits there rather than higher or lower.

Level Holds Why here
PriceAnalysis and the fourteen analysis classes TA-Lib indicators, candlestick patterns, statistics, crossovers, a backtest and performance measures such as the Sharpe ratio, 209 methods Every instrument with candles can be analysed, including an index, and so can every basket
Instrument Identity attributes, lot_size, tick_size, carried_by; prices, quote, last_price, ohlc; the protected discovery helpers; the shared client; a details argument for building from an answer already fetched UBI quotes indices too, and an index's last price is one of the most used values
TradeableInstrument The eleven order-book values, the order and trade readers, place_order, modify_order, cancel_order, cancel_open_orders, the parent members, the 32 price wrappers, the position readers, totals and the four position methods An index has no order book, no orders and no position
NonTradeableInstrument A refusal of any segment that does not end in _indices, and constituents, the index's stored basket An index is the one kind of instrument whose members are worth storing
Derivative The expiry members, the underlying's segment, object and price, the open interest range and the value of one lot Every futures and option contract expires and is written on something
Futures, Option The basis members on Futures; moneyness, intrinsic and time value, implied volatility and greeks on Option; the discovery class methods on both They were identical in all sixteen derivative classes apart from the segment
IndexFutures, IndexOption A check that the segment is an index one, and an underlying typed as NonTradeableInstrument An index cannot be delivered, so these contracts differ in what their underlying is
Family class A fixed segment, named in SEGMENT on a derivative class, a constructor taking exactly that segment's identity fields, its own error class, search on the cash and index classes, and on five classes the holdings members The kind of contract is the class, not a segment string passed by hand

The discovery calls follow the same pattern in every six-class module. The table below shows which class methods each kind of class offers.

Kind of class Examples Discovery class methods
A cash security or an index Equity, EquityIndex, Commodity, MutualFund search
A futures class EquityFutures, CurrencyIndexFutures expiries, contracts, inherited from Futures
An option class EquityOption, FixedIncomeIndexOption expiries, strikes, chain, inherited from Option

All of them read /api/instruments/master rather than /api/instruments/search, for the reason given in Design choices.

Properties and methods

A member that only reports a value is a property, and a member is a method only when it takes an argument or writes to the market. The user decided this on 2026-09-22, so that a caller reads what a member means, such as share.last_price, rather than how it is fetched. The table below sorts the instrument surface by that rule.

Kind Badge Members
Reads a value property quote, last_price, ohlc; bids, offers, best_bid, best_offer, bid_offer_spread, mid_price, volume_weighted_average_price, last_quantity, total_traded_volume, open_interest, last_trade_time; parents, orders, open_orders, completed_orders, rejected_orders, cancelled_orders, trades; net_positions, day_positions, positions_value, positions_pnl; holdings, holdings_value, holdings_pnl; constituents on an index, an exchange traded fund and a mutual fund; the twenty-one contract properties on Derivatives, from days_to_expiry to notional_value
Takes arguments, only reads method prices, parent, parent_orders, parent_trades, implied_volatility, greeks and every analysis method
Finds instruments classmethod search, expiries, contracts, strikes, chain
Sends orders places orders place_order, modify_order, cancel_order, cancel_open_orders, cancel_parent, the 32 price wrappers, add_to_position, reduce_position, liquidate_position, liquidate_all_positions, add_to_holdings, reduce_holdings, liquidate_holdings

A property is still a request

Every read of a property sends its own request to UBI, and nothing is cached. Code that needs a value twice, such as share.net_positions checked and then used, should bind it to a local variable first, or the two reads may see two different moments.

The identity attributes, such as exchange, symbol and lot_size, are plain attributes rather than properties. They are read once, when the object is built, and never change.

How an instrument is looked up

Building an instrument sends exactly one request, GET /api/instruments/details, unless the answer is passed in already through the details argument of Instrument, TradeableInstrument or NonTradeableInstrument, which is how a basket builds all its members from one POST /api/instruments/details. Every later request names the instrument only by the instrument_id that answer carried, which is a UUID UBI computes from the identity and which is the same at every broker. The sequence below shows the lookup for Equity("nse", "RELIANCE"), including the two ways it can fail.

sequenceDiagram
    autonumber
    participant P as Your program
    participant E as Equity.__init__
    participant I as Instrument.__init__
    participant C as Shared client
    participant A as UBI
    P->>E: Equity("nse", "RELIANCE")
    E->>I: super().__init__(exchange, segment="equities", symbol)
    I->>C: shared_unified_broker_interface()
    I->>C: get("/api/instruments/details", params)
    C->>A: GET /api/instruments/details
    alt UBI knows the share
        A-->>C: identity, lot_size, tick_size, carried_by
        C-->>I: dict
        I->>I: store attributes, parse dates, tick_size as Decimal
        I-->>E: done
        E->>E: segment is nse_equities, or raise EquityError
        E-->>P: Equity object
    else HTTP 404
        A-->>C: not found
        C-->>I: NotFoundError
        I-->>E: InstrumentError, chained
        E-->>P: EquityError, chained
    end

The table below lists what the constructor does with each part of UBI's answer.

From /details Stored as Note
instrument_id instrument_id, a str Used alone in every later request, and by __eq__ and __hash__
exchange, segment lower case, segment prefixed such as nse_equities UBI accepts a bare segment but always returns it prefixed
expiry_date, mapping_date, first_seen_date, last_seen_date datetime.date or None
lot_size int or None None when UBI's brokers disagree
tick_size decimal.Decimal or None Decimal so a step like 0.05 stays exact
carried_by a list of dicts, one per broker Each broker's own token, order symbol, lot size and tick size, raw

The output below shows the public attributes of a real lookup, captured from a local UBI on 2026-09-26. The carried_by list has one entry for each of nine brokers, and it was trimmed here to its first two entries, marked with ....

from tradingmachine.assets import equities

reliance = equities.Equity("nse", "RELIANCE")
print(repr(reliance))
attributes = {}
for key, value in vars(reliance).items():
    if not key.startswith("_"):
        attributes[key] = value
print(repr(attributes))
Equity(exchange='nse', segment='nse_equities', symbol='RELIANCE')
{'instrument_id': '3f92570a-9924-5bf5-9f9d-e006cd9f4202', 'exchange': 'nse', 'segment': 'nse_equities', 'shape': 'security', 'symbol': 'RELIANCE', 'underlying_symbol': None, 'expiry_date': None, 'strike_price': None, 'option_type': None, 'mapping_date': datetime.date(2026, 9, 26), 'first_seen_date': datetime.date(2026, 8, 7), 'last_seen_date': datetime.date(2026, 9, 26), 'lot_size': 1, 'tick_size': Decimal('0.1'), 'carried_by': [{'broker': 'dhan', 'broker_token': '2885', 'lot_size': '1.0', 'order_symbol': None, 'tick_size': '0.1'}, {'broker': 'flattrade', 'broker_token': '2885', 'lot_size': '1.0', 'order_symbol': 'RELIANCE-EQ', 'tick_size': None}, ...]}

Two objects for one instrument are equal

Equality and hashing use instrument_id, so two objects built separately for the same contract compare equal and can be used as the same dictionary key.

A family class refuses anything else. Its constructor takes only the identity fields its shape needs, all required, so a missing expiry date is a TypeError at the call site rather than an HTTP 400 a round trip later. A UBI 404 becomes the class's own error, such as EquityOptionError, chained to the InstrumentError underneath. Errors lists every one.

One shared client

Every instrument, synthetic order and Account in a process sends its requests through one UnifiedBrokerInterface, which Instrument.shared_unified_broker_interface() creates on first use. UBI holds a single access token for the whole application and usually hands the same one to every connect, but the first connect after 07:00 on an older token mints a new one, after which every other client pays a reconnect on its next call. One shared client keeps one cached token and one place that reconnects; One token for everyone has the rules.

The client is stored on Instrument by name rather than through cls. Assigning through cls would give each subclass its own attribute and therefore its own client, which is exactly what the sharing is meant to prevent. A caller can still pass a client of its own to any constructor, but then it owns the token clash that follows. The UBI client documents the client's own members.

Synthetic orders sit beside the instrument

A synthetic order, such as a bracket, a trailing stop or an iceberg, is not a member of the instrument. It is an object of its own, in tradingmachine.orders, that holds an instrument and an order template and sends both through the instrument's place_order. The class diagram below shows how the pieces relate.

classDiagram
    direction LR
    class SyntheticOrder {
        SYNTHETIC_TYPE simple
        instrument
        transaction_type, product, order_type
        quantity, price, trigger_price
        price_reference, quantity_reference
        closes_position, reduce_only, dry_run
        parent_id
        synthetic
        synthetic_fields()
        place()
        cancel()
        parent, orders, trades
    }
    class BracketOrder {
        SYNTHETIC_TYPE bracket
        stop_price, stop_limit_price, target_price
        synthetic_fields()
    }
    class BasketOrder {
        SYNTHETIC_TYPE basket
        candidates
        synthetic_fields()
    }
    class ExposureHedgeOrder {
        SYNTHETIC_TYPE exposure_hedge
        watched, lower_band, upper_band
        synthetic_fields()
    }
    class OrderCandidate {
        instrument
        document()
    }
    class ExposureWatch {
        instrument
        exposure_per_unit
    }
    class TradeableInstrument {
        place_order(..., synthetic)
    }
    SyntheticOrder <|-- BracketOrder
    SyntheticOrder <|-- BasketOrder
    SyntheticOrder <|-- ExposureHedgeOrder
    SyntheticOrder --> TradeableInstrument : place() calls place_order
    BasketOrder o-- OrderCandidate
    ExposureHedgeOrder o-- ExposureWatch
    OrderCandidate --> TradeableInstrument
    ExposureWatch --> TradeableInstrument

The three subclasses in the diagram stand for all 53. Every type is its own class in its own module, and each adds only its settings and a synthetic_fields method that names them the way UBI does. The base class builds the synthetic object, {"type": ..., **settings}, and place() calls place_order with it, so a synthetic order is placed exactly like a plain one, through UBI's order engine. place() keeps the parent_id UBI answers with, and cancel() and the parent, orders and trades properties use it to reach the parent through the instrument's parent members.

Five types take a list of instruments rather than one. BasketOrder, OneCancelsAllOrder, LeggedSpreadOrder and StrategyStopOrder take a list of OrderCandidate objects, and the first candidate's instrument anchors the request. ExposureHedgeOrder is built on the hedge instrument and takes a list of ExposureWatch objects for the instruments it watches. Synthetic orders documents every type.

Baskets sit beside the instruments

An asset basket is a group of instruments, such as a portfolio, a watchlist, an index's constituents or what a fund holds. It is not an instrument and does not inherit Instrument, but AssetBasket inherits the same fourteen analysis classes, so every analysis method and performance measure works on a basket too, over candles the basket builds from its members. The class diagram below shows how the two sides link.

classDiagram
    direction LR
    class AnalysisClasses {
        <<fourteen classes>>
    }
    class AssetBasket {
        members, weights
        last_prices, quotes
        prices()
    }
    class Portfolio {
        place_orders()
        rebalance()
    }
    class Index
    class Instrument
    class NonTradeableInstrument {
        constituents
    }
    class BasketStore {
        save(), load()
        load_for_instrument()
    }
    AnalysisClasses <|-- AssetBasket
    Instrument <|-- NonTradeableInstrument
    AssetBasket <|-- Portfolio
    AssetBasket <|-- Index
    NonTradeableInstrument ..> BasketStore : constituents calls
    BasketStore ..> AssetBasket : builds
    AssetBasket --> Instrument : linked_instrument

Watchlist, ExchangeTradedFundConstituents and MutualFundConstituents are left out of the diagram and sit on AssetBasket in the same way. ExchangeTradedFund and MutualFund have a constituents property just like NonTradeableInstrument. The link runs both ways without merging the two objects: an instrument's constituents returns the stored basket, whose linked_instrument is that instrument again, and the official price stays on the instrument. Asset baskets documents the classes and Design choices records why they are built this way.

The account sits above the instruments

Account, in tradingmachine.accounts.account, stands for the whole trading account rather than one instrument. It takes the same shared client. Its flatten sends POST /api/orders/flatten, which halts every synthetic order the engine is running, cancels every open order at every broker and then closes every position; its parents property lists every open parent in the account; and its intent method reads the engine's answer to an order whose placement stopped waiting. The caller must pass confirm="FLATTEN". The table below compares it with the closest per-instrument member.

TradeableInstrument.liquidate_all_positions Account.flatten
Scope This instrument's positions Every position and open order in the account
Open orders Left alone Cancelled first, at every broker
UBI route One POST /api/orders/place per product held One POST /api/orders/flatten
Armed synthetic orders Left alone Halted first, so they cannot trade afterwards
Timeout The client's 30 seconds per request 120 seconds by default, through post(..., timeout_seconds=...)

The account documents its members in full.