Kyle Loomis · June 26, 2023
Building a modern financial instrument library
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.
Copied!@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.
Copied!@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.
Copied!@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.
Copied!@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.
Copied!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.
Copied!equity_option.request_dict()
This results in the following Python dict:
Copied!{
"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.
Copied!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.