Adding an asset class¶
An asset class in this library is one module under src/tradingmachine/assets/ that puts a named class on each of UBI's segments for that family. Six family modules exist today, and every asset class the old project had is ported. This page is the checklist for the next one, written from how those six were built, so that a new module has the same shape and a reader never has to remember which family is the exception.
The shape every module has¶
Every family module follows the same pattern. The table below shows what each existing module holds, which is what a new one should be compared against.
| Module | Classes | Why that many | Holdings members | Main surprise, kept in its note |
|---|---|---|---|---|
equities.py |
6 | UBI has six equity segments | Equity |
none, it is the template |
fixed_income.py |
6 | Six segments, one never filled by any broker | FixedIncome |
Bonds are named by ISIN, and nothing in the family has candles |
commodities.py |
6 | Six segments | none | Quantity is in quotation units and must be whole lots |
currencies.py |
6 | Six segments, three of them empty on every exchange | none | lot_size is not the lot an order is measured against |
funds.py |
2 | UBI has no futures or option segment on a fund or a trust | ExchangeTradedFund, InvestmentTrust |
A trust has no candles |
mutual_funds.py |
1 | One segment, subscribed to rather than traded | MutualFund |
No quote, so orders need a limit price |
The class diagram below shows the six-class shape of a full family, using the equity family. The cash class inherits TradeableInstrument and the index class NonTradeableInstrument directly, the four derivative classes inherit the derivative bases in instruments.py, and each class raises its own error. A derivative class writes no discovery method of its own; it names its segment in SEGMENT and inherits the calls.
classDiagram
direction LR
class TradeableInstrument
class NonTradeableInstrument
class Futures {
expiries()
contracts()
}
class Option {
expiries()
strikes()
chain()
}
class IndexFutures
class IndexOption
class Equity {
search()
holdings members
}
class EquityFutures {
SEGMENT
}
class EquityOption {
SEGMENT
}
class EquityIndex {
search()
}
class EquityIndexFutures {
SEGMENT
}
class EquityIndexOption {
SEGMENT
}
TradeableInstrument <|-- Equity
TradeableInstrument <|-- Futures
TradeableInstrument <|-- Option
Futures <|-- IndexFutures
Option <|-- IndexOption
Futures <|-- EquityFutures
Option <|-- EquityOption
NonTradeableInstrument <|-- EquityIndex
IndexFutures <|-- EquityIndexFutures
IndexOption <|-- EquityIndexOption
The checklist¶
The steps below are in the order they are best done. Each one names the file it touches.
- Find the family's segments in UBI. UBI's canonical segment names are in
stock_brokers/instruments/mapping/utilities/segments.pyin the sibling project. Write down, for each segment, its shape (security,futureoroption), how many rows each exchange has, whether it is quoted, and whether UBI stores candles for it. Check whether the cash segment is in UBI'sCASH_SEGMENTS, because only those can ever be reported as a holding. - Decide the number of classes from UBI, not from what is populated. Write one class for every segment UBI's vocabulary has for the family, even one no broker fills today; an empty segment fails cleanly on its own. Write fewer only when the segments do not exist at all, as with funds.
- Create the module by copying
equities.py. Copy it tosrc/tradingmachine/assets/<family>.pyand rename, rather than factoring anything out of it into a shared base. The working modules are never touched when a new one is added. Rewrite the module docstring, including its "Typical usage example", for the new family. - Declare the segment constants. Put one bare segment name per class at module level, such as
EQUITY_FUTURES_SEGMENT = "equity_futures". These are values this project chooses and reuses, so constants are right here even though UBI's vocabulary is otherwise passed as plain strings. - Write one class per segment. The cash class inherits
instruments.TradeableInstrumentand the index classinstruments.NonTradeableInstrument. The four derivative classes inheritinstruments.Futures,instruments.Option,instruments.IndexFuturesandinstruments.IndexOption, and each declaresSEGMENTas its segment constant on the first line of its body. Add the family's four derivative segments toUNDERLYING_SEGMENT_FOR_DERIVATIVE_SEGMENTinsrc/tradingmachine/assets/instruments.py, mapping each to its cash or index segment, orDerivativewill refuse the contracts withDerivativeError. Do not add a family-level base or aListedSecuritybase. - Make the constructor take exactly the identity fields. A security or an index takes
exchangeandsymbol, a future addsunderlying_symbolandexpiry_datein place ofsymbol, and an option addsstrike_priceandoption_typeas well. Every identity argument is required with no default. The only optional arguments areunified_broker_interfaceon every class and, on the four derivative classes,underlying, an instrument object the contract keeps and passes tosuper().__init__. There is noinstrument_idargument, so there is exactly one way to name a contract. - Re-raise UBI's not-found as the class's own error, and check the segment. Wrap
super().__init__intry, catchexceptions.InstrumentError, and raise the class's own errorfrom errorwith a self-contained message. After it, compareself.segmentwithf"{self.exchange}_{SEGMENT}", the whole prefixed name UBI returns. - Add the error classes. Add one class per new family class to
src/tradingmachine/assets/exceptions.py, named<ClassName>Error, each a flat sibling directly underInstrumentError. Do not root the family's errors in the cash class's error, because an index future is not a kind of the cash instrument. - Add the discovery class methods. A cash or index class gets
search, callingcls._search_cataloguewith its own segment constant. The futures and option classes need nothing here:expiries,contracts,strikesandchainare inherited fromFuturesandOption, which readSEGMENT, so a caller still never types a segment string. - Add holdings members only where UBI can report a holding. If the cash segment is in
CASH_SEGMENTS, copy the six holdings members fromEquityinto the cash class: theholdings,holdings_valueandholdings_pnlproperties and theadd_to_holdings,reduce_holdingsandliquidate_holdingsmethods, with thecncproduct fixed. Give every holdable class all six, even when one of them is awkward for the family, and say so in its docstring rather than leaving it out. Never put holdings on a derivative or an index class. - Add no local validation. Do not reject an exchange UBI has no rows for, round a price, or check a lot. UBI's not-found already raises the class's own error, and its order validation is the single source of truth.
- Write complete docstrings. Every class, method and property has a Google-style docstring with
Args:,Returns:andRaises:, giving each type. A member that raises nothing saysRaises:followed byNothing.. No explanatory comments go into the source; the reasoning goes into the note in step 14. - Check it against the real UBI. From a scratchpad script, not from the repository, build one contract of every class and record its segment, shape,
lot_size,tick_size, number of brokers incarried_by,last_priceand whetherpricesreturns candles. Send no order. Run.venv/bin/ruff check .and.venv/bin/ruff format .. - Write the sidecar note. Create
.claude/notes/src/tradingmachine/assets/<family>.py.mdwith the table of class, base class, UBI segment, shape and constructor arguments; a section for everything that differs from the earlier families and why; and a "Verified on" section holding the table from step 13. Add a sentence to.claude/notes/src/tradingmachine/assets/exceptions.py.mdnaming the new error classes. - Update
CLAUDE.md. Add the module to the list of source files and write its paragraph in the same form as the others, naming its traps. - Add the documentation page. Write
docs/asset-classes/<family>.mdfollowing the existing family pages, add it to the Asset classes section ofnavinmkdocs.yml, and add a card todocs/asset-classes/index.md. Update the class counts on The instrument model and Repository structure, and add the error classes to Errors. The reference page for the new module is generated with no edit. Finish with.venv/bin/mkdocs build --strict. - Deliver it on its own branch and pull request. One module per pull request, and a second module is started only after the first is merged, so the shared
exceptions.pyandCLAUDE.mdnever conflict.
A skeleton to start from¶
The block below is one futures class written the way the checklist asks, for an imaginary family called "example", with its constant and its error. It is a template to rename, not code that exists in the library.
EXAMPLE_FUTURES_SEGMENT = "example_futures"
class ExampleFutures(instruments.Futures):
"""One futures contract on an example underlying."""
SEGMENT = EXAMPLE_FUTURES_SEGMENT
def __init__(
self,
exchange: str,
underlying_symbol: str,
expiry_date: datetime.date | str,
underlying: instruments.Instrument | None = None,
unified_broker_interface: client.UnifiedBrokerInterface | None = None,
):
"""Looks the contract up in UBI's example futures segment and keeps its details.
Args:
exchange: The str exchange the contract is listed on, such as `nse`.
underlying_symbol: The str symbol of the underlying.
expiry_date: The expiry as a datetime.date or a `YYYY-MM-DD` str.
underlying: The Instrument the contract is written on, which the contract keeps and uses, or None to use UBI's link to the underlying or else the family's default, looked up on every read.
unified_broker_interface: The client.UnifiedBrokerInterface to send requests through, or None to share one client among all instruments.
Raises:
TypeError: underlying is given and is not an Instrument.
ExampleFuturesError: UBI has no such contract, or the instrument it returned is not in the example futures segment.
UnifiedBrokerInterfaceError: Any other failure reported by, or on the way to, UBI.
"""
try:
super().__init__(
exchange=exchange,
segment=EXAMPLE_FUTURES_SEGMENT,
underlying_symbol=underlying_symbol,
expiry_date=expiry_date,
underlying=underlying,
unified_broker_interface=unified_broker_interface,
)
except exceptions.InstrumentError as error:
raise exceptions.ExampleFuturesError(
f"UBI has no {exchange} example futures contract on {underlying_symbol} expiring {expiry_date}"
) from error
if self.segment != f"{self.exchange}_{EXAMPLE_FUTURES_SEGMENT}":
raise exceptions.ExampleFuturesError(
f"An instrument outside the {EXAMPLE_FUTURES_SEGMENT} segment is not an ExampleFutures: {self!r}"
)
The class inherits expiries, contracts and every contract member from Futures, so the constructor is all it writes. The matching error class in src/tradingmachine/assets/exceptions.py is a class line and a one-line docstring, placed beside the others and worded the same way:
class ExampleFuturesError(InstrumentError):
"""An example futures contract UBI does not know, or one that is not in the example futures segment."""
What is left to port¶
Every asset class the old project had is now ported. The one UBI segment without a class is uncategorised, a catch-all that UBI does not accept orders for, so it has no family module.
Test with care
A module with holdings members can place real orders through add_to_holdings, reduce_holdings and liquidate_holdings, and those members have no dry_run argument. Check them against a recording client offline rather than against the real UBI, unless you intend to trade.