Kyle Loomis · October 17, 2024
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 have a number of fundamental flaws.
- Lack of modularity and extensibility, making it difficult to compose instruments and extend the framework to create custom instruments.
- Bespoke instrument definitions that add unnecessary complexity.
- Lack of serialization and deserialization functionality, preventing easy integration with APIs.
This is why I built finstruments, a Python library which fixes these issues, and greatly improves the developer experience. finstruments is an intuitive, simple, and modular framework that natively supports serialization and deserialization. If an instrument doesn't already exist, you can easily leverage the building blocks to create a new instrument. In this article, I provide an overview of how I built the framework, how to use it, 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's date and datetime types aren't serialized correctly, and hashing doesn't work as expected. 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
Here’s a breakdown of 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.
Extensibility
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 instance of the abstract class BaseEquity that we created above. 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 also need to be populated with appropriate objects.
Copied!from datetime import date
from finstruments.common.enum import Currency
from finstruments.instrument.common.cut import NysePMCut
from finstruments.instrument.common.exercise_style import AmericanExerciseStyle
from finstruments.instrument.common.option.enum import OptionType
from finstruments.instrument.common.option.payoff import VanillaPayoff
from finstruments.instrument.equity import EquityOption, CommonStock
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 crucial for enabling permanent storage and easy integration 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 adding other instruments. Also feel free to email me with any feedback or suggested changes.