# Building a financial instrument library with Python and Pydantic

> Finstruments - financial instruments - library provides an intuitive, simple, and modular framework to build financial instrument definitions for any asset class.

Author: Kyle Loomis
Published: 2024-10-17
Source: https://kyleloomis.com/articles/financial-instrument-library

Financial instrument definitions are useful for many different purposes, including valuation, portfolio
management, and backtesting. Existing instrument frameworks, such as
[QuantLib](https://quantlib-python-docs.readthedocs.io/en/latest/index.html) and [GS Quant](https://developer.gs.com/docs/gsquant/), 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`](https://github.com/kyleloomis/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](https://docs.pydantic.dev/latest/) 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.

```python
@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.

```python
@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`.

```python
@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`.

```python
@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.

```python
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.

```python
equity_option.request_dict()
```

This results in the following Python `dict`:

```json
{
  "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.

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

### Next steps

Star the repository on [GitHub](https://github.com/kyleloomis/finstruments) and contribute by adding other instruments. Also feel free to
[email me](mailto:kyleloomis74@gmail.com) with any feedback or suggested changes.
