How to Implement a Contingent Claims-Based Instrument¶
In this chapter we will look at how to create a strongly typed instrument, which leverages the Contingent Claims library. As an example, we will see how the fixed rate bond instrument is implemented in Daml Finance. The goal is that you will learn how to implement your own instrument template, if you need an instrument type that is not already implemented in Daml Fincance.
To follow the script used in this tutorial, you can
clone the Daml Finance repository. In particular,
the file src/test/daml/Daml/Finance/Instrument/Bond/Test/FixedRate.daml
is the starting point of
this tutorial. It also refers to some utility functions in
src/test/daml/Daml/Finance/Instrument/Bond/Test/Util.daml
.
Template Definition¶
We start by defining a new template for the instrument. Here are the first few lines of the fixed rate instrument:
-- | This template models a fixed rate bond.
-- It pays a fixed coupon rate at the end of every coupon period.
template Instrument
with
depository : Party
-- ^ The depository of the instrument.
issuer : Party
-- ^ The issuer of the instrument.
id : Id
-- ^ The identifier of the instrument.
version : Text
-- ^ The instrument's version.
description : Text
-- ^ A description of the instrument.
couponRate : Decimal
-- ^ The fixed coupon rate, per annum. For example, in case of a "3.5% p.a coupon" this should
-- be 0.035.
periodicSchedule : PeriodicSchedule
-- ^ The schedule for the periodic coupon payments.
holidayCalendarIds : [Text]
-- ^ The identifiers of the holiday calendars to be used for the coupon schedule.
calendarDataProvider : Party
-- ^ The reference data provider to use for the holiday calendar.
dayCountConvention : DayCountConventionEnum
-- ^ The day count convention used to calculate day count fractions. For example: Act360.
currency : InstrumentKey
-- ^ The currency of the bond. For example, if the bond pays in USD this should be a USD cash
-- instrument.
observers : PartiesMap
-- ^ The observers of the instrument.
lastEventTimestamp : Time
-- ^ (Market) time of the last recorded lifecycle event. If no event has occurred yet, the
-- time of creation should be used.
These template variables describe the payoff of the fixed rate bond. They will be used to create a Contingent Claims tree, which is the internal representation used for modelling and lifecycling in Daml Finance. Note that this tree is not part of the template above. Instead, it will be created dynamically, as described in the next sections.
The Claims Interface¶
In order for the instrument to work with the general Daml Finance lifecycling framework, we will implement the Claims interface. This provides a generic mechanism to process coupon payments and the redemption amount. It will work in a similar way for all instrument types, regardless of their economic terms.
Here is a high level implementation of the Claims interface:
interface instance Claim.I for Instrument where
view = Claim.View with acquisitionTime = dateToDateClockTime (daysSinceEpochToDate 0)
asBaseInstrument = toInterface @BaseInstrument.I this
getClaims Claim.GetClaims{actor} = do
-- get the initial claims tree (as of the bond's acquisition time)
let getCalendars = getHolidayCalendars actor calendarDataProvider
schedule <- rollSchedule getCalendars periodicSchedule holidayCalendarIds
let
useAdjustedDatesForDcf = True
notional = 1.0
ownerReceives = True
fxAdjustment = 1.0
couponClaims =
createFixRatePaymentClaims dateToDateClockTime schedule periodicSchedule
useAdjustedDatesForDcf couponRate ownerReceives dayCountConvention notional currency
redemptionClaim =
createFxAdjustedPrincipalClaim dateToDateClockTime ownerReceives fxAdjustment notional
currency periodicSchedule.terminationDate
pure $ [couponClaims, redemptionClaim]
The getClaims
function is where we define the payoff of the instrument.
First, we create a coupon schedule, which depends on the coupon dates and a holiday calendar. This
is then used to create the actual coupon claims. The redemption claim is also created. Finally, the
coupon claims and the redemption claim are joined. Together, they define the economic terms of the
instrument.
How to Define the Redemption Claim¶
In the above example, we see that the redemption claim depends on the currency and the maturity date.
We will now create the actual redemption claim:
-- | Create an FX adjusted principal claim.
-- This can be used for both FX swaps (using the appropriate FX rate) and single currency bonds
-- (setting the FX rate to 1.0).
createFxAdjustedPrincipalClaim : (Date -> Time) -> Bool -> Decimal -> Decimal -> Deliverable ->
Date -> TaggedClaim
createFxAdjustedPrincipalClaim dateToTime ownerReceives fxRateMultiplier notional
cashInstrumentCid valueDate =
let
fxLegClaimAmount = when (TimeGte valueDate)
$ scale (Const fxRateMultiplier)
$ scale (Const notional)
$ one cashInstrumentCid
fxLegClaim = if ownerReceives then fxLegClaimAmount else give fxLegClaimAmount
in
prepareAndTagClaims dateToTime [fxLegClaim] "Principal payment"
Keywords like when, TimeGte, scale, one and give are defined in the Contingent Claims documentation.
How to Define the Coupon Claims¶
The coupon claims are a bit more complicated to define. We need to take a schedule of adjusted coupon dates and the day count convention into account.
Here is how we create the coupon claims:
createFixRatePaymentClaimsList : Schedule -> PeriodicSchedule -> Bool -> Decimal -> Bool ->
DayCountConventionEnum -> Decimal -> Deliverable -> [Claim Date Decimal Deliverable Observable]
createFixRatePaymentClaimsList schedule periodicSchedule useAdjustedDatesForDcf couponRate
ownerReceives dayCountConvention notional cashInstrumentCid =
let
couponDatesAdjusted = map (.adjustedEndDate) schedule
couponAmounts = map (\p ->
couponRate *
(calcPeriodDcf dayCountConvention p useAdjustedDatesForDcf
periodicSchedule.terminationDate periodicSchedule.frequency)
) schedule
couponClaimAmounts = mconcat $
zipWith
(\d a ->
when (TimeGte d) $ scale (Const a) $ scale (Const notional) $ one cashInstrumentCid
) couponDatesAdjusted couponAmounts
in
[if ownerReceives then couponClaimAmounts else give couponClaimAmounts]
For each coupon period, we calculate the adjusted end date and the amount of the coupon. We then create each coupon claim in a way similar to the redemption claim above.
How the Instrument Evolves Over Time¶
The bond instrument gives the holder the right to receive future coupons and the redemption amount. At issuance, this means all the coupons, since they are all in the future. However, when the first coupon is paid, the holder of the instrument is no longer entitled to receive this coupon again. In other words, the claims representation of the instrument changes. It evolves over time.
In our implementation of the fixed rate bond, we want a simple and reliable mechanism for evolving the instrument. Luckily for us, when the lifecycle function returns a coupon to be paid today, it also returns the remaining claims of the instrument (excluding today’s and any previous coupons). Hence, we can use this to evolve our instrument, in a way that is guaranteed to be consistent with the lifecycle mechanism.
This is all done in the Lifecycle.Rule. We will now break it apart to describe the steps in more detail:
evolve Lifecycle.Evolve{eventCid; observableCids; instrument} = do
claimInstrument <- fetchInterfaceByKey @BaseInstrument.R instrument
First, we retrieve the inital claims of the instrument. This represents the bond as of inception.
By keeping track of lastEventTimestamp
(in our case: the last time a coupon was paid), we can
“fast forward” to the remaining claims of the instrument:
-- Recover claims tree as of the lastEventTimestamp. For a bond, this just requires
-- lifecycling as of the lastEventTimestamp.
nv <- BaseInstrument.exerciseInterfaceByKey @DynamicInstrument.I instrument
lifecycler DynamicInstrument.GetView with viewer = lifecycler
claims <- Prelude.fst <$>
lifecycle lifecycler observableCids claimInstrument [timeEvent nv.lastEventTimestamp]
Finally, we can lifecycle the instrument as of the current time. If there is a lifecycle effect (for example a coupon), we will create an Effect for it, which can then be settled.
-- Lifecycle
let
acquisitionTime = Claim.getAcquisitionTime claimInstrument
v <- view <$> fetch eventCid
(remaining, pending) <-
lifecycleClaims observableCids acquisitionTime claims [timeEvent v.eventTime]
let
pendingAfterNetting = netOnTag pending
(otherConsumed, otherProduced) = splitPending pendingAfterNetting
if remaining == claims && null pendingAfterNetting then
pure (None, [])
else do
let
currentKey = BaseInstrument.getKey $ toInterface claimInstrument
newKey = currentKey with version = sha256 $ show remaining
producedInstrument = if isZero' remaining then None else Some newKey
newInstrumentCid <- BaseInstrument.exerciseInterfaceByKey @DynamicInstrument.I instrument
lifecycler DynamicInstrument.CreateNewVersion with
lastEventTimestamp = v.eventTime; version = newKey.version
effectCid <- toInterfaceContractId <$> create Effect with
providers = fromList [currentKey.issuer]
id = v.id
description = v.description
targetInstrument = currentKey
producedInstrument
otherConsumed
otherProduced
settlementTime = Some v.eventTime
observers = Disclosure.flattenObservers . (.observers) . view $
toInterface @Disclosure.I claimInstrument
pure (producedInstrument, [effectCid])
Observables¶
In our fixed rate bond example above, the coupon amount is pre-determined at the inception of the instrument. In contrast, a floating rate coupon is defined by the value of a reference rate during the lifetime of the bond. Since we do not know this value when the instrument is created, we need to define the coupon based on a future observation of the reference rate. In order to do this we introduce the concept of a numeric observation.
In the instrument definition, we need an identifier for the reference rate:
-- | This template models a floating rate bond.
-- It pays a floating coupon rate at the end of every coupon period.
-- This consists of a reference rate (observed at the beginning of the coupon period) plus a coupon
-- spread. For example, 3M Euribor + 0.5%.
template Instrument
with
depository : Party
-- ^ The depository of the instrument.
issuer : Party
-- ^ The issuer of the instrument.
id : Id
-- ^ An identifier of the instrument.
version : Text
-- ^ The instrument's version.
description : Text
-- ^ A description of the instrument.
referenceRateId : Text
-- ^ The floating rate reference ID. For example, in case of "3M Euribor + 0.5%" this should
-- be a valid reference to the "3M Euribor" reference rate.
When we create the claims, we can then use Observe to refer to the value of the reference rate:
-- | Calculate a floating rate amount for each payment date and create claims.
-- The floating rate is always observed on the first day of each payment period and used for the
-- corresponding payment on the last day of that payment period. This means that the calculation
-- agent needs to provide such an Observable, irrespective of the kind of reference rate used (e.g.
-- a forward looking LIBOR or a backward looking SOFR-COMPOUND).
createFloatingRatePaymentClaims : (Date -> Time) -> Schedule -> PeriodicSchedule -> Bool ->
Decimal -> Bool -> DayCountConventionEnum -> Decimal -> Deliverable -> Observable -> TaggedClaim
createFloatingRatePaymentClaims dateToTime schedule periodicSchedule useAdjustedDatesForDcf
floatingRateSpread ownerReceives dayCountConvention notional cashInstrumentCid referenceRateId =
let
couponClaimAmounts = mconcat $ map (\p ->
when (TimeGte p.adjustedStartDate)
$ scale (
(Observe referenceRateId + Const floatingRateSpread) *
(Const (calcPeriodDcf dayCountConvention p useAdjustedDatesForDcf
periodicSchedule.terminationDate periodicSchedule.frequency)
))
$ when (TimeGte p.adjustedEndDate)
$ scale (Const notional)
$ one cashInstrumentCid
) schedule
couponClaims = if ownerReceives then couponClaimAmounts else give couponClaimAmounts
in prepareAndTagClaims dateToTime [couponClaims] "Floating rate payment"
In this example, the observable is an interest reference rate. Other instrument types can require other types of observables, for example an FX rate or a stock price.