Asset baskets¶
Everything else in the library describes one instrument. An asset basket describes a group of them: a portfolio, a watchlist, an index, or what an exchange traded fund or a mutual fund holds. Every basket reads what it needs about all its members in one list request to UBI, and every basket can be analysed exactly like an instrument, from moving averages to the Sharpe ratio.
Two members place real orders
Portfolio.place_orders and Portfolio.rebalance send one real order per instrument, in one request, with real money. UBI sends each market order as a limit that follows the other side of the book and is cancelled after 30 seconds, so a member with an empty book or no fresh quote gets HTTP 409 in its row and nothing is sent for it; pass as_marketable_limit=False to send real market orders. The orders are placed in parallel and not as one unit, so some can be accepted while others are refused. Run them with dry_run=True first and read the table they return.
The classes¶
The package is one base class and five kinds of basket, each in its own module under src/tradingmachine/asset_baskets/. The diagram below shows how they relate and which instrument classes link to them.
classDiagram
direction LR
class AssetBasket {
members
weights
prices()
sharpe_ratio()
}
AssetBasket <|-- Portfolio
AssetBasket <|-- Watchlist
AssetBasket <|-- Index
AssetBasket <|-- ExchangeTradedFundConstituents
AssetBasket <|-- MutualFundConstituents
NonTradeableInstrument ..> Index : constituents
ExchangeTradedFund ..> ExchangeTradedFundConstituents : constituents
MutualFund ..> MutualFundConstituents : constituents
The table below says what each kind of basket is for.
| Class | Members carry | What it is for |
|---|---|---|
AssetBasket |
a weight, or nothing | The shared base: live prices, weights, correlation, risk, and every analysis method |
Portfolio |
a quantity and an average price | What you hold: value, profit and loss, buying the whole basket, rebalancing |
Watchlist |
nothing | Instruments followed together, each counting equally |
Index |
a weight | A weighted index, published or your own, and turning it into a portfolio |
ExchangeTradedFundConstituents |
a weight | What an ETF holds, compared with the ETF's own price |
MutualFundConstituents |
a weight | What a mutual fund holds, the only way to measure one since UBI has no NAV |
An index or a fund is two things¶
NIFTY is an instrument with its own price, which UBI quotes. It is also a basket of fifty shares with weights. The library keeps these apart rather than merging them into one class, because a merged class could not say whether prices() meant the official price or the price rebuilt from the members. The instrument keeps its price, and its constituents property returns the basket.
| Class | instrument.sharpe_ratio(days=365) |
instrument.constituents.sharpe_ratio(days=365) |
|---|---|---|
EquityIndex (NIFTY) |
Works on the official index candles | Works on the rebuilt basket; comparing the two shows how well the stored weights reproduce the index |
ExchangeTradedFund (NIFTYBEES) |
Works on the fund's traded price | Works on its holdings; fund.tracking_error(benchmark=fund.constituents) shows how closely the fund follows them |
MutualFund |
Returns None, because UBI stores no candles or NAV for mutual funds | The only way to measure a mutual fund |
holdings on a fund still means the units of the fund your account holds, which is why the fund's own contents are called constituents.
Where the members come from¶
UBI stores nothing about what an index or a fund holds, so baskets live in this project's MongoDB, in the asset_baskets collection. You supply the members, by hand in Python or from a CSV file. Each stored document is one version of one basket, dated by the day it takes effect, because an index is rebalanced and a fund's holdings change every month.
flowchart LR
CSV["CSV file<br/>symbol, weight"] --> IMP["BasketCsvImporter"]
PY["Python<br/>BasketMember list"] --> B["a basket object"]
IMP -->|"one POST /api/instruments/details"| B
B -->|"BasketStore.save"| DB[("MongoDB<br/>asset_baskets")]
DB -->|"BasketStore.load<br/>or instrument.constituents"| B2["the basket again"]
AssetBasket¶
class AssetBasket(name, members, linked_instrument=None, unmapped_weight=0.0, unified_broker_interface=None)
The base class holds a list of BasketMember objects, each an instrument with a weight, a quantity, or neither. Every member must be a different instrument, and either every member has a weight or none does; without weights the members count equally. unmapped_weight is the share of a fund held in things UBI cannot price, such as cash.
The table below lists the members every basket has.
| Kind | Member | Description | UBI route |
|---|---|---|---|
| property | instruments, labels, size |
What the basket holds; a label reads like nse:INFY |
none |
| property | weights |
The weights normalised to sum to 1, or equal weights | none |
| property | last_prices, ohlc, quotes |
One row per member, with an error column for a member UBI has no price for |
POST /ltp, /ohlc, /quote |
| property | day_change_percent |
The weighted day move of the whole basket | POST /ohlc |
| property | advancers, decliners, breadth |
How many members are up, down, unchanged or unpriced today | POST /ohlc |
| property | exposure_by_segment, exposure_by_exchange |
Where the weight sits | none |
| property | concentration, effective_number_of_members, largest_weight |
The Herfindahl index, the number of equal members it corresponds to, and the biggest weight | none |
| method | prices(...) |
Candles for the basket as a whole, which every analysis method reads | POST /prices |
| method | member_prices(...), member_closes(...), member_returns(...) |
Every member's candles, closes or returns, lined up by time | POST /prices |
| method | covariance_matrix(...), correlation_matrix(...) |
How every pair of members moves together | POST /prices |
| method | risk_contributions(...) |
Each member's share of the basket's volatility | POST /prices |
| method | diversification_ratio(...) |
How much the members' moves cancel out | POST /prices |
| method | return_contributions(...) |
What each member added to the basket's return | POST /prices |
| method | top_gainers(count), top_losers(count) |
The biggest movers today | POST /ohlc |
| method | overlap_with(other) |
The weight two baskets have in common, the standard test of whether two funds differ | none |
| method | add_member(member), remove_member(instrument) |
Change the members in memory | none |
Every basket also inherits the fourteen analysis classes an instrument does, the last of which is the performance measures, because its prices method gives candles for the whole basket. Each candle is the sum over members of a fixed quantity times the member's candle. A weighted basket takes those quantities from its weights at the first candle of the range, starting from 100, which is how a price index moves between rebalances. The open and close are exact. The high and low are an approximation, because the members do not all reach their highs at the same moment, and volume and open interest are left empty.
Example¶
The example below builds a three-stock basket with one details request, then reads its live state and its history. The output is real, from 2026-09-28.
from tradingmachine.asset_baskets import asset_basket, member_resolver
members = member_resolver.MemberResolver().resolve(
[
{"exchange": "nse", "segment": "equities", "symbol": "INFY", "weight": 50},
{"exchange": "nse", "segment": "equities", "symbol": "TCS", "weight": 30},
{"exchange": "nse", "segment": "equities", "symbol": "WIPRO", "weight": 20},
]
)
basket = asset_basket.AssetBasket("IT test", members)
print(basket.weights.to_dict())
print(basket.breadth)
print(basket.risk_contributions(days=365).round(4))
print(basket.return_contributions(days=365).round(4))
print(basket.cumulative_return(days=365))
{'nse:INFY': 0.5, 'nse:TCS': 0.3, 'nse:WIPRO': 0.2}
{'advancers': 1, 'decliners': 2, 'unchanged': 0, 'unavailable': 0, 'advance_decline_ratio': 0.5}
weight risk_contribution
label
nse:INFY 0.5 0.5592
nse:TCS 0.3 0.2968
nse:WIPRO 0.2 0.1440
starting_weight member_return contribution
label
nse:WIPRO 0.2 -0.3156 -0.0631
nse:TCS 0.3 -0.2811 -0.0843
nse:INFY 0.5 -0.3063 -0.1531
-0.3005838911839611
The three contributions add up to the basket's cumulative return of −30.06 percent, and INFY carries 56 percent of the risk on 50 percent of the weight.
Raises¶
| Exception | When |
|---|---|
BasketMemberError |
The members are empty, name an instrument twice, or give weights to only some members; or UBI answered an error for a member when candles were asked for |
ValueError |
unmapped_weight is not between 0 and 1 |
Portfolio¶
class Portfolio(name, members, unified_broker_interface=None)
A portfolio gives every member a quantity, negative for a short position. Its weights are each member's share of today's value, and its candles show what the same quantities were worth at each candle, so its Sharpe ratio or drawdown describes the portfolio as you hold it now.
| Kind | Member | Description | UBI route |
|---|---|---|---|
| classmethod | from_holdings(name="holdings") |
Builds a portfolio of the account's long-term holdings | GET /api/portfolio/holdings |
| classmethod | from_positions(name="positions", day=False) |
Builds a portfolio of the open positions | GET /api/portfolio/positions |
| property | quantities, values, value |
What is held and what it is worth at last prices | POST /ltp |
| property | invested_value, unrealized_pnl |
What was paid and the profit on it | POST /ltp |
| property | day_pnl, day_change_percent |
The profit and move since the previous close | POST /ohlc |
| method | rebalance_trades(target, capital=None) |
The buys and sells that would give the portfolio another basket's weights, without sending anything | POST /ltp |
| places orders | place_orders(product, transaction_type="buy", ...) |
One market order per member, all in one request | POST /api/orders/place |
| places orders | rebalance(target, product, ...) |
Sends the trades rebalance_trades works out |
POST /api/orders/place |
The orders go in UBI's list form of POST /api/orders/place, which takes up to 500 orders and places them in parallel, each at the broker that suits it. UBI's basket synthetic order is not used, because it takes at most 25 legs and sends every leg to the first leg's broker. Quantities are floored to whole units and sent as computed, with no lot or tick check, because UBI checks orders itself.
Example¶
The example below reads the account's holdings as a portfolio. The output is real, from 2026-09-28; UBI's own holdings summary said 9,237.33 for the value at the same moment, because it prices each holding from its holdings document rather than from the live last price.
Watchlist¶
class Watchlist(name, instruments, unified_broker_interface=None)
A watchlist takes plain instruments and weights them equally. It adds add(instrument) and rank_by(column="change_percent", ascending=False), which sorts the members by any column of ohlc.
Index¶
class Index(name, members, weighting="stated", base_value=100, base_date=None, linked_instrument=None, unified_broker_interface=None)
An index weights its members in one of three ways, which the table below lists.
weighting |
Each member's weight | Example |
|---|---|---|
stated |
Its own weight, as a factsheet gives it |
NIFTY, loaded with its published weights |
equal |
The same for every member | NIFTY50 Equal Weight, or a file without weights |
price |
Its share of the sum of last prices, which is holding one unit of each | The Dow Jones |
linked_instrument is the official index the basket describes, when there is one, so tracking_error(benchmark=index.linked_instrument) measures how well the stored weights reproduce it. level reports today's level measured from base_value at base_date, and to_portfolio(capital) turns the index into whole units of each member for a sum of money, ready for place_orders.
ExchangeTradedFundConstituents¶
class ExchangeTradedFundConstituents(name, members, fund=None, indicative_net_asset_value=None, unmapped_weight=0.0, unified_broker_interface=None)
This is what an ETF holds, linked to the ETF through fund. tracking_difference(...) is the fund's return minus its holdings' return over a range, which is mostly the fund's fees. premium_or_discount compares the fund's last price with its indicative net asset value, when the index row that carries it is given as indicative_net_asset_value.
MutualFundConstituents¶
class MutualFundConstituents(name, members, fund=None, unmapped_weight=0.0, unified_broker_interface=None)
This is what a mutual fund holds, linked to the scheme through fund. estimated_day_change_percent is the holdings' day move scaled down by unmapped_weight, the usual guess at today's change in the net asset value before the fund publishes it, and estimated_net_asset_value(previous_net_asset_value) applies it to the last published value. The weights come from the fund's monthly disclosure, so the basket is an estimate of the fund, not the fund.
BasketStore and BasketCsvImporter¶
class BasketStore(project_configuration=None, unified_broker_interface=None)
The store saves and loads baskets in MongoDB. The table below lists its members.
| Kind | Member | Description |
|---|---|---|
| method | save(basket, effective_date=None, source="user") |
Stores the basket as the version in effect from a date, replacing one stored for the same date |
| method | load(name, as_of=None) |
Rebuilds the version in effect on a day, as the class its stored kind names |
| method | load_for_instrument(instrument, as_of=None) |
Finds the basket linked to an instrument; constituents calls this |
| method | names(kind=None), history(name), delete(name, effective_date) |
List, inspect and remove stored versions |
| method | build(document, linked_instrument=None) |
Builds a basket from a document without storing it |
load raises BasketNotFoundError when no version of the name is in effect on the day asked for, while load_for_instrument returns None instead. A CSV file the importer cannot use raises BasketCsvImportError, and every basket error is listed on Errors.
class BasketCsvImporter(project_configuration=None, unified_broker_interface=None)
The importer's one method, import_file(path, name, kind="index", exchange="nse", segment="equities", linked_instrument=None, effective_date=None, unmapped_weight=0.0, source="csv"), reads a CSV with a symbol column and optional exchange, segment, weight, quantity and instrument_id columns. Column names are read without regard to case, so the NSE's own constituent files, whose header has Symbol, import as they are. A file without weights makes an equally weighted index. Weights may be fractions or percentages, with or without a % sign. The kind names the class the basket is stored and rebuilt as: basket, portfolio, watchlist, index, exchange_traded_fund_constituents or mutual_fund_constituents.
Example¶
The example below imports five NIFTY stocks with their weights, links them to the NIFTY index, and reads them back through constituents. The output is real, from 2026-09-28.
from tradingmachine.asset_baskets import basket_csv_importer
from tradingmachine.assets import equities
nifty = equities.EquityIndex(exchange="nse", symbol="NIFTY")
basket_csv_importer.BasketCsvImporter().import_file(
"five.csv", name="NIFTY FIVE", kind="index", linked_instrument=nifty, effective_date="2026-09-01"
)
five = nifty.constituents
print(five, five.weights.round(4).to_dict())
print(five.tracking_error(benchmark=nifty, days=365))
Under the hood
- Every live member sends one
POSTto UBI's list form of the instrument routes, described in Several instruments at once. UBI answers one entry per instrument, and a bad instrument fails only its own entry. MemberResolverinsrc/tradingmachine/asset_baskets/member_resolver.pyturns rows into members with onePOST /api/instruments/details, and builds each instrument from its entry through thedetailsargument of the instrument constructors, so no instrument is looked up twice.- The reasoning behind each file is in
.claude/notes/src/tradingmachine/asset_baskets/.