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 ....
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.