CallableBond - Incorrect "Missing observation" error

Hi,

Playing with the CallableBond (floating-rate) I’ve found out something unexpected.
The test bellow is a modified version of runFloating in module Daml.Finance.Instrument.Bond.Test.Callable.

Important parameters:

  • issueDate = date 2022 Jan 15
  • firstCouponDate = date 2022 Apr 15
  • holidays = [date 2022 Apr 15]
  • resetRelativeTo = CalculationPeriodStartDate

Given the configuration above, the reset date for the rate reference should be 2022 Jan 17 ( 2022 Jan 15 is Saturday), so I’ve created an observation on 2022 Jan 17.

In the test I electAndVerifyPaymentEffects on 2022 Apr 18 (Monday) because firstCouponDate is a Friday public holiday (2022 Apr 15)

When I run the test I get following error message:

message = “Missing observation for USD/LIBOR/3M at t = 2022-04-18T00:00:00Z”

  1. I think the error is not correct because the valid observation should be the one on 2022 Jan 17 (resetRelativeTo = CalculationPeriodStartDate) which has been created on the ledger.
  2. Should this test fail?

Thanks!
Jose

-- Create and lifecycle a floating coupon callable bond.
-- 2Y, 3M Libor + 0.1% p.a.
-- Issuer does not call the bond before maturity.
runFloating' : Script ()
runFloating' = script do
  [custodian, issuer, investor, calendarDataProvider, publicParty] <-
    createParties ["Custodian", "Issuer", "Investor", "Calendar Data Provider", "PublicParty"]

  -- Account and holding factory
  let pp = [("FactoryProvider", S.singleton publicParty)]

  -- Originate commercial-bank cash
  now <- getTime
  cashInstrumentCid <- originate custodian issuer "USD" "US Dollar" pp now

  -- Create and distribute bond
  -- Libor + 0.1% coupon every 3M
  -- CREATE_3M_FLOATING_CALLABLE_BOND_VARIABLES_BEGIN
  let
    issueDate = date 2022 Jan 15
    firstCouponDate = date 2022 Apr 15
    maturityDate = date 2024 Jan 15
    notional = 1.0
    couponRate = 0.001
    capRate = None
    floorRate = None
    couponPeriod = M
    couponPeriodMultiplier = 3
    dayCountConvention = Act360
    businessDayConvention = Following
    referenceRateId = "USD/LIBOR/3M"
    floatingRate = Some FloatingRate with
      referenceRateId
      resetRelativeTo = CalculationPeriodStartDate
      fixingDates = FixingDates with
        periodMultiplier = 0
        period = D
        dayType = Some Business
        businessDayConvention = NoAdjustment
        businessCenters = ["USD"]
    -- CREATE_3M_FLOATING_CALLABLE_BOND_VARIABLES_END
    observations = M.fromList
      [ (dateToDateClockTime $ date 2022 Jan 17, 0.010) ]
    holidayCalendarIds = ["USD"]
    cal =
      HolidayCalendarData with
        id = "USD"
        weekend = [Saturday, Sunday]
        holidays = [date 2022 Apr 15]

  -- A reference data provider publishes the holiday calendar on the ledger
  calendarCid <- submit calendarDataProvider do
    createCmd HolidayCalendar with
      provider = calendarDataProvider
      calendar = cal
      observers = M.fromList pp

  observableCid <- toInterfaceContractId <$> submit issuer do
    createCmd Observation with
      provider = issuer; id = Id $ referenceRateId; observations; observers = M.empty

  bondInstrument <- originateCallableBond issuer issuer "BONDTEST1" "Callable Bond" pp now
    issueDate holidayCalendarIds calendarDataProvider firstCouponDate maturityDate
    dayCountConvention businessDayConvention floatingRate couponRate capRate floorRate couponPeriod
    couponPeriodMultiplier cashInstrumentCid notional

  -- One day before the first coupon date: try to lifecycle and verify that there are no lifecycle
  -- effects.
  verifyNoLifecycleEffects [publicParty] (subtractDays firstCouponDate 1) bondInstrument issuer
    [observableCid]

  let
    amount = 1.0
    electorIsOwner = False

  -- Coupon date 1: Lifecycle and verify that there is an effect for one coupon.
  let
    expectedConsumed = []
    expectedProduced = [qty 0.0027805556 cashInstrumentCid]
  (Some bondInstrumentAfterCoupon1, effectCid) <- electAndVerifyPaymentEffects (date 2022 Apr 18)
    amount bondInstrument electorIsOwner issuer investor [publicParty] "NOT CALLED" [observableCid]
    expectedConsumed expectedProduced

  pure ()

Hi Jose,

Thank you for reporting this. I have opened up an issue for it: Ensure correct rate fixing date with implied negative date offset · Issue #787 · digital-asset/daml-finance · GitHub
It will probably be fixed in the next couple of weeks.

Regards,
Markus

Thanks @Markus_Friberg
I understand that after the fix the test shouldn’t fail. Is this correct?

I think so, at least it should use the fixing from 2022 Jan 17.

@Markus_Friberg
The thing is it’s taking the value on 2022 Jan 17
Here the same test but with the observation on 2022 Apr 18 . (dateToDateClockTime $ date 2022 Apr 18, 0.999)
As you can check it doesn’t fail but it is taking the value on 2022 Jan 17.

-- Create and lifecycle a floating coupon callable bond.
-- 2Y, 3M Libor + 0.1% p.a.
-- Issuer does not call the bond before maturity.
runFloating' : Script ()
runFloating' = script do
  [custodian, issuer, investor, calendarDataProvider, publicParty] <-
    createParties ["Custodian", "Issuer", "Investor", "Calendar Data Provider", "PublicParty"]

  -- Account and holding factory
  let pp = [("FactoryProvider", S.singleton publicParty)]

  -- Originate commercial-bank cash
  now <- getTime
  cashInstrumentCid <- originate custodian issuer "USD" "US Dollar" pp now

  -- Create and distribute bond
  -- Libor + 0.1% coupon every 3M
  -- CREATE_3M_FLOATING_CALLABLE_BOND_VARIABLES_BEGIN
  let
    issueDate = date 2022 Jan 15
    firstCouponDate = date 2022 Apr 15
    maturityDate = date 2024 Jan 15
    notional = 1.0
    couponRate = 0.001
    capRate = None
    floorRate = None
    couponPeriod = M
    couponPeriodMultiplier = 3
    dayCountConvention = Act360
    businessDayConvention = Following
    referenceRateId = "USD/LIBOR/3M"
    floatingRate = Some FloatingRate with
      referenceRateId
      resetRelativeTo = CalculationPeriodStartDate
      fixingDates = FixingDates with
        periodMultiplier = 0
        period = D
        dayType = Some Business
        businessDayConvention = NoAdjustment
        businessCenters = ["USD"]
    -- CREATE_3M_FLOATING_CALLABLE_BOND_VARIABLES_END
    observations = M.fromList
      [ (dateToDateClockTime $ date 2022 Jan 17, 0.010)
      , (dateToDateClockTime $ date 2022 Apr 18, 0.999)]
    holidayCalendarIds = ["USD"]
    cal =
      HolidayCalendarData with
        id = "USD"
        weekend = [Saturday, Sunday]
        holidays = [date 2022 Apr 15]

  -- A reference data provider publishes the holiday calendar on the ledger
  calendarCid <- submit calendarDataProvider do
    createCmd HolidayCalendar with
      provider = calendarDataProvider
      calendar = cal
      observers = M.fromList pp

  observableCid <- toInterfaceContractId <$> submit issuer do
    createCmd Observation with
      provider = issuer; id = Id $ referenceRateId; observations; observers = M.empty

  bondInstrument <- originateCallableBond issuer issuer "BONDTEST1" "Callable Bond" pp now
    issueDate holidayCalendarIds calendarDataProvider firstCouponDate maturityDate
    dayCountConvention businessDayConvention floatingRate couponRate capRate floorRate couponPeriod
    couponPeriodMultiplier cashInstrumentCid notional

  -- One day before the first coupon date: try to lifecycle and verify that there are no lifecycle
  -- effects.
  verifyNoLifecycleEffects [publicParty] (subtractDays firstCouponDate 1) bondInstrument issuer
    [observableCid]

  let
    amount = 1.0
    electorIsOwner = False

  -- Coupon date 1: Lifecycle and verify that there is an effect for one coupon.
  let
    expectedConsumed = []
    expectedProduced = [qty 0.0027805556 cashInstrumentCid]
  (Some bondInstrumentAfterCoupon1, effectCid) <- electAndVerifyPaymentEffects (date 2022 Apr 18)
    amount bondInstrument electorIsOwner issuer investor [publicParty] "NOT CALLED" [observableCid]
    expectedConsumed expectedProduced

  pure ()

Hi Jose,

I have merged some internal changes to Daml Finance main regarding a new ObserveAt contingent claims node. This resolves a date related issue that we knew about before. Do you see your problem also with the latest version? If so, I will have closer look.

Regards,
Markus

1 Like

Working as expected.
Thanks @Markus_Friberg !

runFloating' : Script ()
runFloating' = script do
  [custodian, issuer, investor, calendarDataProvider, publicParty] <-
    createParties ["Custodian", "Issuer", "Investor", "Calendar Data Provider", "PublicParty"]

  -- Account and holding factory
  let pp = [("FactoryProvider", S.singleton publicParty)]

  -- Originate commercial-bank cash
  now <- getTime
  cashInstrumentCid <- originate custodian issuer "USD" "US Dollar" pp now

  -- Create and distribute bond
  -- Libor + 0.1% coupon every 3M
  -- CREATE_3M_FLOATING_CALLABLE_BOND_VARIABLES_BEGIN
  let
    issueDate = date 2022 Jan 15
    firstCouponDate = date 2022 Apr 15
    maturityDate = date 2024 Jan 15
    notional = 1.0
    couponRate = 0.001
    capRate = None
    floorRate = None
    couponPeriod = M
    couponPeriodMultiplier = 3
    dayCountConvention = Act360
    businessDayConvention = Following
    referenceRateId = "USD/LIBOR/3M"
    floatingRate = Some FloatingRate with
      referenceRateId
      referenceRateType = SingleFixing CalculationPeriodStartDate
      fixingDates = FixingDates with
        periodMultiplier = 0
        period = D
        dayType = Some Business
        businessDayConvention = NoAdjustment
        businessCenters = ["USD"]
    -- CREATE_3M_FLOATING_CALLABLE_BOND_VARIABLES_END
    observations = M.fromList
      [ (dateToDateClockTime $ date 2022 Jan 17, 0.010)]
    holidayCalendarIds = ["USD"]
    cal =
      HolidayCalendarData with
        id = "USD"
        weekend = [Saturday, Sunday]
        holidays = [date 2022 Apr 15]

  -- A reference data provider publishes the holiday calendar on the ledger
  calendarCid <- submit calendarDataProvider do
    createCmd HolidayCalendar with
      provider = calendarDataProvider
      calendar = cal
      observers = M.fromList pp

  observableCid <- toInterfaceContractId <$> submit issuer do
    createCmd Observation with
      provider = issuer; id = Id $ referenceRateId; observations; observers = M.empty

  bondInstrument <- originateCallableBond issuer issuer "BONDTEST1" "Callable Bond" pp now
    issueDate holidayCalendarIds calendarDataProvider firstCouponDate maturityDate
    dayCountConvention businessDayConvention floatingRate couponRate capRate floorRate couponPeriod
    couponPeriodMultiplier cashInstrumentCid notional

  -- One day before the first coupon date: try to lifecycle and verify that there are no lifecycle
  -- effects.
  verifyNoLifecycleEffects [publicParty] (subtractDays firstCouponDate 1) bondInstrument issuer
    [observableCid]

  let
    amount = 1.0
    electorIsOwner = False

  -- Coupon date 1: Lifecycle and verify that there is an effect for one coupon.
  let
    expectedConsumed = []
    expectedProduced = [qty 0.0027805556 cashInstrumentCid]
  (Some bondInstrumentAfterCoupon1, effectCid) <- electAndVerifyPaymentEffects (date 2022 Apr 18)
    amount bondInstrument electorIsOwner issuer investor [publicParty] "NOT CALLED" [observableCid]
    expectedConsumed expectedProduced

  pure ()

Perfect, thank you for the confirmation!

1 Like