How to leverage Contingent Claims in custom instrument implementations¶
They Payoff Modeling tutorial introduces the Contingent Claims modeling framework in the context of the Generic Instrument. In this chapter, we will see how the library can instead be used to lifecycle custom instrument implementations. The Bond and Swap instruments, for example, leverage Contingent Claims behind the scenes to calculate pending coupon payments.
Let us explore in detail how the fixed rate bond instrument is implemented in Daml Finance. The goal is for you to learn how to implement and lifecycle your own instrument template, should you need an instrument type that is not already implemented in the library.
To follow the code snippets used in this tutorial in Daml Studio, you can clone the Daml Finance repository and take a look at how the Bond Instrument template is implemented. In order to see how lifecycling is performed, you can run the script in the Instrument/Bond/Test/FixedRate.daml file.
Template Definition¶
We start by defining a new template for the instrument. Here are the fields used for 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.
holdingStandard : HoldingStandard
-- ^ The holding standard for holdings referencing this instrument.
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.
notional : Decimal
-- ^ The notional of the bond. This is the face value corresponding to one unit of the bond
-- instrument. For example, if one bond unit corresponds to 1000 USD, this should be 1000.0.
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 economic terms of a fixed rate bond.
The Claims
Interface¶
We now need to map the template variables to a Contingent Claims tree, the internal representation we wish to use for lifecycling. Note that the Contingent Claims tree is not a part of the template above, instead it will be created dynamically upon request.
In order do that, we implement the Claims interface. This interface provides access to a generic mechanism to process coupon payments and redemptions. It will work in a similar way for the majority of instrument types, regardless of their specific 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
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
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 calculate the coupon payment dates by rolling out a periodic coupon schedule.
- The payment dates are then used to build claim sub-trees for the coupon payments.
- A claim sub-tree for the final redemption is also created.
- Finally, the coupon and redemption sub-trees are joined. Together, they yield the desired economic terms for the bond.
How to define the redemption claim¶
The redemption claim depends on the currency and the maturity date of the bond.
-- | 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
cashInstrument valueDate =
let
fxLegClaimAmount = when (TimeGte valueDate)
$ scale (Const fxRateMultiplier)
$ scale (Const notional)
$ one cashInstrument
fxLegClaim = if ownerReceives then fxLegClaimAmount else give fxLegClaimAmount
in
prepareAndTagClaims dateToTime [fxLegClaim] "Principal payment"
Keywords like when, scale, one and give should be familiar from the Payoff Modeling tutorial. TimeGte is just a synonym for at.
The ownerReceives
flag is used to indicate whether the owner of a holding on the bond is meant
to receive the redemption payment. When this is set to false
, the holding custodian will be
entitled to the payment.
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.
createFixRatePaymentClaimsList : Schedule -> PeriodicSchedule -> Bool -> Decimal -> Bool ->
DayCountConventionEnum -> Decimal -> Deliverable -> [C]
createFixRatePaymentClaimsList schedule periodicSchedule useAdjustedDatesForDcf couponRate
ownerReceives dayCountConvention notional cashInstrument =
let
couponDatesAdjusted = map (.adjustedEndDate) schedule
couponAmounts = map (\p ->
couponRate *
(calcPeriodDcf dayCountConvention p useAdjustedDatesForDcf
periodicSchedule.terminationDate periodicSchedule.frequency)
) schedule
couponClaimAmounts = andList $
zipWith
(\d a ->
when (TimeGte d) $ scale (Const a) $ scale (Const notional) $ one cashInstrument
) couponDatesAdjusted couponAmounts
in
[if ownerReceives then couponClaimAmounts else give couponClaimAmounts]
For each coupon period, we calculate the adjusted end date and the actual coupon amount. We then create each coupon claim in a way similar to the redemption claim above.
Evolving the Instrument over time¶
The bond instrument gives the holder the right to receive future coupons and the redemption amount.
At issuance, all coupons are due. However, after the first coupon is paid, the holder of the
instrument is no longer entitled to receive it again.
The lastEventTimestamp
field in our template is used to keep track of the latest executed coupon
payment.
Evolution of the instrument over time (and calculation of the corresponding lifecycle effects) can be performed using the Lifecycle.Rule template provided in the Daml.Finance.Claims package. This rule is very generic and can be used for all instruments that implement the Claims interface.
Let us break its implementation apart to describe what happens in more detail:
First, we retrieve the claim tree corresponding to the initial state of the instrument. We do so by fetching the
Claims
interface we defined for the template.claimInstrument <- fetchInterfaceByKey @BaseInstrument.R instrument
By using the
lastEventTimestamp
(in our case: the last time a coupon was paid), we can now “fast forward” the claim tree to the current instrument state.-- Recover claims tree as of the lastEventTimestamp. For a bond, this just requires -- lifecycling as of the lastEventTimestamp. dynInstrView <- BaseInstrument.exerciseInterfaceByKey @DynamicInstrument.I instrument lifecycler DynamicInstrument.GetView with viewer = lifecycler -- fast-forward the claims tree to the current version by replaying the previous events let prevEvents = dynInstrView.prevEvents <> [timeEvent dynInstrView.lastEventTimestamp] claims <- fst <$> lifecycle lifecycler observableCids claimInstrument prevEvents
Finally, we lifecycle the current instrument state to calculate pending effects. If there are such effects (for example when a coupon payment is due), we create a Lifecycle Effect for it, which can then be claimed and settled.
evolve Lifecycle.Evolve{eventCid; observableCids; instrument} = do v <- view <$> fetch eventCid -- Fast-forward the instrument from inception to the timestamp of the last event. -- Then, perform a time-based lifecycling according to the current event. (remaining, pending, claims, claimInstrument, dynInstrView) <- fastForwardAndLifecycle instrument observableCids v.eventTime lifecycler 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 $ mconcat [show v.eventTime, show remaining] producedInstrument = if isZero' remaining then None else Some newKey tryCreateNewInstrument lifecycler dynInstrView.prevEvents v.eventTime None instrument newKey effectCid <- toInterfaceContractId <$> create Effect with providers = singleton currentKey.issuer id = v.id description = v.description targetInstrument = currentKey producedInstrument otherConsumed otherProduced settlementTime = Some v.eventTime observers = (.observers) . view $ toInterface @Disclosure.I claimInstrument pure (Some newKey, [effectCid])
The Dynamic.Instrument
Interface¶
In the tryCreateNewInstrument
part above, we create a new version of the instrument containing
the updated lastEventTimestamp
(and also including all previous events up until now). This is
done by exercising the
CreateNewVersion
choice of the
Dynamic.Instrument interface:
BaseInstrument.exerciseInterfaceByKey @DynamicInstrument.I oldKey actor DynamicInstrument.CreateNewVersion with lastEventTimestamp = eventTime prevEvents = case electionData of None -> prevEvents ++ [timeEvent eventTime] Some (electorIsOwner, claimTag) -> prevEvents ++ [newElection] where newElection = EventData with t = eventTime election = Some (electorIsOwner, claimTag) version = newKey.version
This ensures that the next time the instrument is lifecycled, the current coupon is no longer included. This also works for other types of events, for example a barrier hit on a derivative instrument: if such an event is ever lifecycled it will persist on (a new version of) the instrument, ensuring that it will not be forgotten when the instrument is lifecycled in the future.
Including market 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 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.
holdingStandard : HoldingStandard
-- ^ The holding standard for holdings referencing this instrument.
description : Text
-- ^ A description of the instrument.
floatingRate : FloatinRateTypes.FloatingRate
-- ^ A description of the floating rate to be used. This supports both Libor and SOFR style
-- reference rates (using a compounded index, e.g. the SOFR Index).
When we create the claims for the coupon payments, we can then use ObserveAt to refer to the value of the reference rate:
observedFloatingRate = ObserveAt fr.referenceRateId resetDate
In this example, the observable is a reference interest rate. Other instrument types can require other types of observables, such as an FX rate or a stock price.
Different ways to create and store the Contingent Claims tree¶
To summarize what we have seen so far, there are two different ways of using Contingent Claims in a Daml Finance instrument.
When using the Generic Instrument, we create the claim tree at instrument inception and store this representation explicitly on the ledger. After a lifecycle event, for example a coupon payment, a new version of the instrument (with a different claim tree) supersedes the previous version.
In contrast, the approach used in this tutorial only stores the key economic terms of the bond on the ledger. The claim tree is not stored on-ledger, but it is created “on-the-fly” when needed (for example, when lifecycling).
Which approach is preferred?¶
The latter approach has the advantage that the claim tree can adapt to changes in reference data. A change to e.g. a holiday calendar would automatically impact the claim tree the next time it is dynamically created. This is not the case for the first approach, where the tree is static.
Also, if the economic terms of the instrument result in a very large claim tree, it could be desirable not to store it on the ledger for performance reasons.
Finally, the “dynamic” approach allows for the terms of the template to be very descriptive to anyone familiar with the payoff at hand.
On the other hand, if you need to quickly create a one-off instrument, the on-ledger approach allows you to create the claims directly from a script, without first having to define a dedicated template.
Also, if you need to explicitly access Contingent Claims representations of older versions of the instrument on the ledger, for example for auditing reasons, that would be achieved out of the box with the first approach.