Skip to Content
Course content

153: Modeling Financial Instruments with ADTs

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a colleague who was building a pricing engine for a boutique trading desk. They had used a standard class hierarchy for their financial instruments—a base Instrument class with several subclasses like Bond and Option. To handle the specific logic for each, they were using instanceOf checks and manual casting inside a giant if-else block. It looked "fine" until the desk added a new CreditDefaultSwap instrument. They updated the data model but forgot to update one specific risk-calculation method. The code didn't crash; it just silently skipped the new instrument, returning a value of zero. By the time they noticed the bug in production, the portfolio's risk exposure was wildly miscalculated.

This is exactly where Algebraic Data Types (ADTs) save your skin. In the financial world, an instrument isn't just "some object"; it's one of a strictly defined set of possibilities. By using a sealed trait, we move the burden of correctness from the developer's memory to the Scala compiler.

Defining the Instrument Hierarchy

When we model financial instruments, we want to ensure that any function dealing with an instrument handles every possible type. By marking our trait as sealed, we tell the compiler that all implementations must be defined in the same file. This allows the compiler to perform exhaustiveness checking.

sealed trait Instrument

case class Stock(symbol: String, shares: Double) extends Instrument
case class Bond(issuer: String, faceValue: Double, couponRate: Double) extends Instrument
case class Option(
  underlying: Instrument, 
  strikePrice: Double, 
  expiryDate: java.time.LocalDate, 
  isCall: Boolean
) extends Instrument

Notice how the Option case class takes another Instrument as its underlying asset. This is the "Algebraic" part of ADTs—we're composing types. An option could be based on a stock, or it could even be an option on another option (though that's where things get weird in the real world).

Ensuring Total Functions with Pattern Matching

The real magic happens when you write logic to process these instruments. Instead of if-else chains, you use pattern matching. Because the trait is sealed, if you add a Future instrument to the trait but forget to add it to your valuation logic, the compiler will give you a warning: match may not be exhaustive.

def calculateMarketValue(instrument: Instrument, currentPrice: Double): Double = instrument match {
  case Stock(_, shares) => 
    shares * currentPrice
    
  case Bond(_, faceValue, coupon) => 
    faceValue * (1 + coupon) // Simplified for this example
    
  case Option(underlying, strike, _, isCall) => 
    val underlyingVal = calculateMarketValue(underlying, currentPrice)
    if (isCall) Math.max(0, underlyingVal - strike) 
    else Math.max(0, strike - underlyingVal)
}

I personally love this pattern because it turns a potential production outage into a compile-time fix. You no longer have to grep the entire codebase for every place an instrument is handled whenever the business decides to support a new asset class.

Handling Complex State with Nested ADTs

Financial instruments often have states that are mutually exclusive. For instance, a trade is either Pending, Executed, or Cancelled. Rather than using a bunch of nullable fields (like executionDate which is null if the trade is pending), you can wrap your instrument in another ADT.

sealed trait TradeStatus
case object Pending extends TradeStatus
case class Executed(tradeDate: java.time.LocalDate, price: Double) extends TradeStatus
case class Cancelled(reason: String) extends TradeStatus

case class Trade(id: String, instrument: Instrument, status: TradeStatus)

Now, you can't accidentally access a tradeDate on a trade that hasn't been executed yet. The type system literally prevents the state from being invalid. This "making illegal states unrepresentable" is the gold standard for writing robust financial software.




📋 Practical Task

Build a Multi-Asset Portfolio Risk Aggregator

Your task is to implement a risk aggregation system that calculates the total exposure of a portfolio. You will need to model the domain using ADTs to ensure no asset type is overlooked.

  • Define the ADT: Create a sealed trait Asset. Implement three case classes: Equity (symbol, quantity), FixedIncome (issuer, principal, yield), and Commodity (name, amount, purity).
  • Implement the Valuator: Create a function calculateExposure(asset: Asset, marketPrice: Double): Double.
    • Equity exposure = quantity * marketPrice.
    • FixedIncome exposure = principal * (1 + yield).
    • Commodity exposure = amount * purity * marketPrice.
  • Aggregate the Portfolio: Create a function totalPortfolioValue(assets: List[Asset], marketPrice: Double): Double that sums the exposure of all assets in the list.
  • Verify Exhaustiveness: Temporarily add a new case class Crypto to the Asset trait and observe how the compiler warns you about the calculateExposure match block. Once you've seen the warning, implement the Crypto logic (quantity * marketPrice) to resolve it.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.