Building a modern financial instrument library

This project is currently under development and will be open-sourced shortly.

Financial instrument definitions are useful for many different purposes, including valuation, portfolio management, and backtesting. Existing instrument frameworks, such as QuantLib and GS Quant, offer a selection of instruments, but they have several fundamental flaws.

  • Lack of modularity and extensibility, meaning that it isn't easy to compose instruments and extend the framework to build custom instruments.
  • Bespoke instrument definitions that add unnecessary complexity.
  • Lack of serialization and deserialization functionality, meaning the framework can't be easily used with an API.

I built a new framework which fixes these issues, and greatly improves the developer experience. My framework supports extensibility, composability, and serialization and deserialization. It supports any instrument — even custom instruments, such as exotic options, built by users — and any asset class. In this article, I will provide an overview of how I built the framework, usage, and next steps.

Base instrument

Pydantic provides the BaseModel class which enables serialization, deserialization, and other useful functionality out of the box. But there are a few issues with BaseModel. For example, Python date and datetime aren't correctly serialized, and hashing doesn't work properly. I implemented an abstract Base class which fixes these problems with BaseModel, and adds other useful functionality, such as copy functionality.

Every instrument inherits from BaseInstrument which uses the custom Base class to enable field validation, serialization, and deserialization.

@serializable_base_class class BaseInstrument(Base, ABC): """ Base instrument data class to be inherited from all instrument subclasses. Contains core fields that are applicable to all instruments. """ agreed_discount_rate: Optional[str] = Field(default=None) pillar_date: Optional[date] = Field(default=None) denomination_currency: Optional[Currency] = Field(default=None) code: str descriptor: str @property def underlying_instrument(self) -> "BaseInstrument": """ Get and return BaseInstrument in "underlying" field if exists, else self. Returns: BaseInstrument: BaseInstrument in "underlying" field if exists, else self """ return getattr(self, "underlying", None) or self

Let's break down the fields in this class.

  • agreed_discount_rate: Agreed discount rate.
  • pillar_date: Pillar date, such as option expiration date.
  • denomination_currency: Denomination currency, such as USD.
  • code: Instrument code.
  • descriptor: Descriptor of instrument, used for serialization. Defaults to the class name.

Since instruments can be composed, the underlying_instrument property provides a mechanism to get the underlying instrument of the parent instrument, or the parent instrument itself if no child instrument exists. For example, CommonStock can be nested inside the EquityOption instrument, so the instantiated common stock is returned upon calling this method.

Finally, the @serializable_base_class annotation provides the functionality to serialize any subclass of BaseInstrument. This annotation needs to be used for all abstract base classes, and child (leaf node) classes must implement @serializable to enable serialization.

Building custom instruments

Now that we have the core BaseInstrument class for all instruments, let's create a BaseEquity instrument and some child classes.

@serializable_base_class class BaseEquity(BaseInstrument, ABC): """ Equity base class. """ ticker: str agreed_discount_rate: Optional[str] = Field(init=False, default=None) pillar_date: Optional[date] = Field(init=False, default=None) denomination_currency: Optional[Currency] = Field(default=None) code: str

Notice that the only addition to the BaseInstrument class fields is the ticker field, as everything else is inherited.

Extensability

Now that we have a base class for equities, we can extend it to build a custom instrument, such as CommonStock.

@serializable class CommonStock(BaseEquity): """ Common stock. """ code: str = Field(init=False, default="COMMON_STOCK")

Composability

Finally, we can compose instruments. Notice how in the EquityOption class below, we use an underlying of the abstract class we created above, BaseEquity. When we instantiate an EquityOption class, we use a child class of BaseEquity, such as CommonStock.

@serializable class EquityOption(VanillaOption): underlying: BaseEquity payoff: BaseFixedStrikePayoff exercise_type: BaseExerciseStyle contract_size: float denomination_currency: Currency agreed_discount_rate: Optional[str] = Field(default=None) code: str = Field(init=False, default="EQUITY_OPTION")

Using the framework

Let's instantiate the EquityOption class that we created above. An equity option requires a BaseEquity instrument object (e.g. CommonStock) as input for the underlying field. The payoff (VanillaPayoff, DigitalPayoff) and exercise_type (EuropeanExerciseStyle, AmericanExerciseStyle, BermudanExerciseStyle) fields need to be populated with objects as well.

equity_option = EquityOption( underlying=CommonStock(ticker='AAPL'), payoff=VanillaPayoff( option_type=OptionType.PUT, strike_price=100 ), exercise_type=AmericanExerciseStyle( minimum_exercise_date=date(2022, 1, 3), expiration_date=date(2025, 1, 3), cut=NysePMCut() ), denomination_currency=Currency.USD, contract_size=100 )

Serialization

Serialization is especially important to enable permanent storage and usage with APIs. The custom Base parent class provides the request_dict() method to serialize the instrument. Let's serialize the equity option we created above.

equity_option.request_dict()

This results in the following Python dict:

{ "agreed_discount_rate": "None", "pillar_date": 1735862400000, "denomination_currency": "USD", "code": "EQUITY_OPTION", "descriptor": "EquityOption", "payoff": { "option_type": "PUT", "strike_price": 100.0, "descriptor": "VanillaPayoff" }, "exercise_type": { "expiration_date": 1735862400000, "cut": { "timezone": "US/Eastern", "descriptor": "NysePMCut" }, "minimum_exercise_date": 1641168000000, "descriptor": "AmericanExerciseStyle" }, "underlying": { "agreed_discount_rate": "None", "pillar_date": "None", "denomination_currency": "None", "code": "COMMON_STOCK", "descriptor": "CommonStock", "ticker": "AAPL" }, "contract_size": 100.0 }

Deserialization

Instruments can be deserialized by passing a python dict to the appropriate instrument class. The deserialization of any nested classes will be properly handled.

serialized = equity_option.request_dict() EquityOption(**serialized)

Next steps

Star the repository on Github and contribute by building other instruments. Also feel free to email me with any feedback or suggested changes.