Case Study: The PricingModel Aggregate

Introduction

This is the final of a four-part series of posts related to ChronosES, its implementation, and its usage.  

  1. Introduction
  2. Basic usage (Hello, world!)
  3. Architecture
  4. In-depth usage

In this post, we’re going to dive into a real-world example of how we use ChronosES to organize data related to our option pricing model.


The PricingModel Aggregate

Here at Belvedere we use a proprietary option pricing model that is based on the Black Scholes formula. Throughout the day, our model parameter values are constantly updated by traders as market conditions change. We have decided to utilize ChronosES in order to more easily store, organize, and analyze current and past states of our model. We are constantly iterating on our model, and have taken care to design the aggregate in an extendable way.

 Black-Scholes Call Formula

Black-Scholes Call Formula

Below is a simplified version of our PricingModel aggregate protobuf definition:

message PricingModel
{
  string symbol = 1;
  ProductParameters productParameters = 2;
  repeated ExpriationParameters = expirationParameters 3;
}

Product Parameters

The productParameters field refers to parameters that are relevant in pricing all option of a certain symbol. The expirationParameters field refers to parameters that are relevant in pricing all options of a certain symbol and expiration date. Below is the protobuf definition of the ProductParameters message:

message ProductParameters
{
  int64 startTradingSessionTime = 1;
  int64 endTradingSessionTime = 2;
  int32 tickSize = 3;
  OptionTypeBase optionType = 4;
}

The first two fields refer to the time of day that a given product is tradable, and the third refers to the level of precision that prices for a product are listed as on the exchange. The fourth field is more interesting -  it contains a collection of parameters that are specific to the properties of the option we’re pricing. Below is the protobuf definition of our OptionTypeBase message:

message OptionTypeBase
{
  message European
  {
    double euroParam1 = 1;
  }

  message American
  {
    double americanParam1 = 1;
    double americanParam2 = 2;
  }

  oneof OptionType
  {
     European european = 1;
     American american = 2;
  }
}

We trade many kinds of options here at Belvedere, and have multiple versions of our model designed to price different types of options. As such, we have designed our PricingModel protobuf definition to allow us the flexibility to configure model components independently of one another. The two main styles of options we trade are European and American. These two option types have different model parameters associated with them. In order to accommodate both option styles, we use the protobuf oneof keyword in our OptionTypeBase definition. The result of this design is that each PricingModel aggregate contains only the relevant parameters for the specific symbol it is associated with. This design is also extendable – it would be trivial to add an additional option type by defining a message under the American definition, and then including a field of that type in OptionType oneof field.


Expiration Parameters

Below is our definition of our ExpriationParameters and VolatilityCurveBase messages. You’ll notice many similarities to our product-specific message definitions:

message ExpirationParameters
{
  int64 expirationDate = 1;
  int32 underlyingAssetId = 2;
  VolatilityCurve volCurve = 3;
}

message VolatilityCurveBase
{

  message VolCurve_A
  {
    double paramA = 1;
  }

  message VolCurve_B
  {
    double paramB = 1;
  }

  message VolCurve_C
  {
    double paramC = 1;
  }

  oneof VolCurveType
  {
     VolCurveA curveA = 1;
     VolCurveB curveB = 2;
     VolCurveC curveC = 3;
  }
}

The first two ExpirationParameters fields are self-explanatory, and you’ll notice that the volCurve field is very similar to the optionType field in the ProductParameters message. Each of these components contains multiple message definitions and then a single oneof field. Just as we use different model parameters to price European and American options, we have developed multiple algorithms to model volatility for different types of products. One benefit of organizing these components in a similar way is that we can interact with model components in a generic way throughout our C++ and C# codebases. In reality our PricingModel aggregate definition contains many more components defined in this manner, and being able to manipulate them in a standardized way simplifies our business logic and makes it easy to add additional components to our model in the future.


Event Logic Example

Below is the protobuf definition and Python event logic for a simple ChronosES event that adds a new ExpirationParameters to a PricingModel aggregate:

// protobuf definition

message AddExpirationEvent
{
  ExpirationParameters newExpiration = 1;
}

# Python event logic

class DuplicateExpiration(Exception):
    pass

class AddExpirationEvent(Event):
    Aggregate = PricingModel
    Proto = AddExpirationEvent

    def RaiseFor(self, aggregate):
        startIndex = len(aggregate.expirationParameters) - 1
        insertIndex = 0
        aggregate.expirationParameters.extend([ExpirationParameters()])
        for i in range(startIndex, 0, -1):
            expiration = aggregate.expirationParameters[i]
            if expiration.expirationDate == self.newExpiration.expirationDate:
                raise DuplicateExpiration('Expiration already exists in the model')
            elif expiration.expirationDate > self.newExpiration.expirationDate:
                aggregate.expirationParameters[i + 1].CopyFrom(aggregate.expirationParameters[i])
            else:
                insertIndex = i + 1
                break
        aggregate.expirationParameters[insertIndex].CopyFrom(self.newExpiration)

Takeaways

Chronos provides us a greater ability to analyze past model states and troubleshoot issues if they arise. Previously, we stored our model inputs in a SQL table that we would snapshot periodically. Chronos allows us to not only save model snapshots, but observe each subsequent model state in between these snapshots. As opposed to simply logging model updates throughout the day, with Chronoswe can “play back” events sequentially as they originally occurred. This proves helpful in diagnosing issues that occur as we alpha new model components, and simulating our production environment during development.

We hope you enjoyed this case-study of ChronosES in action at Belvedere. Stay tuned for more blogs in the future!

Architecture (shift to PostgreSQL)

Architecture (shift to PostgreSQL)