DV01 for fixed and zero coupon treasury bonds as well as DV01 for Fixed-Float swaps

Hi

I need to calculate DV01 for Fixed and zero coupon bonds for US treasury bonds.
Also, DV01 for fixed-float interest rate swaps in order to support butterfly and curve trades.
Any help would be appreciated.

Regards
Sudharshan

I tried the following code:

import java.time.LocalDate;

import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.StandardId;
import com.opengamma.strata.basics.currency.Currency;
import com.opengamma.strata.basics.date.BusinessDayAdjustment;
import com.opengamma.strata.basics.date.BusinessDayConventions;
import com.opengamma.strata.basics.date.DayCount;
import com.opengamma.strata.basics.date.DayCounts;
import com.opengamma.strata.basics.date.DaysAdjustment;
import com.opengamma.strata.basics.date.HolidayCalendarId;
import com.opengamma.strata.basics.date.HolidayCalendarIds;
import com.opengamma.strata.basics.schedule.Frequency;
import com.opengamma.strata.basics.schedule.PeriodicSchedule;
import com.opengamma.strata.basics.schedule.StubConvention;
import com.opengamma.strata.pricer.bond.DiscountingFixedCouponBondProductPricer;
import com.opengamma.strata.product.SecurityId;
import com.opengamma.strata.product.bond.FixedCouponBond;
import com.opengamma.strata.product.bond.FixedCouponBondYieldConvention;
import com.opengamma.strata.product.bond.ResolvedFixedCouponBond;

public class Pricer {
	private static final ReferenceData REF_DATA = ReferenceData.standard();
	private static final StandardId SECURITY_ID = StandardId.of("OG-Ticker", "GOVT1-BOND1");
	private static final StandardId ISSUER_ID = StandardId.of("OG-Ticker", "GOVT1");
	private static final FixedCouponBondYieldConvention YIELD_CONVENTION = FixedCouponBondYieldConvention.US_STREET;
	private static final double NOTIONAL = 1000_000_000;
	private static final double FIXED_RATE = 0.01125;
	private static final HolidayCalendarId USNY_CALENDAR = HolidayCalendarIds.USNY;
	private static final DaysAdjustment DATE_OFFSET = DaysAdjustment.ofBusinessDays(1, USNY_CALENDAR);
	private static final DayCount DAY_COUNT = DayCounts.ACT_365F;
	private static final LocalDate START_DATE = LocalDate.of(2016, 6, 30);
	private static final LocalDate END_DATE = LocalDate.of(2021, 6, 30);
	private static final BusinessDayAdjustment BUSINESS_ADJUST = BusinessDayAdjustment.of(BusinessDayConventions.MODIFIED_FOLLOWING, USNY_CALENDAR);
	private static final PeriodicSchedule PERIOD_SCHEDULE = PeriodicSchedule.of(START_DATE, END_DATE, Frequency.P6M, BUSINESS_ADJUST, StubConvention.NONE, false);
	private static final DiscountingFixedCouponBondProductPricer PRICER = DiscountingFixedCouponBondProductPricer.DEFAULT;
	  
	private static final ResolvedFixedCouponBond PRODUCT = FixedCouponBond.builder()
		      .securityId(SecurityId.of(SECURITY_ID))
		      .dayCount(DAY_COUNT)
		      .fixedRate(FIXED_RATE)
		      .legalEntityId(ISSUER_ID)
		      .currency(Currency.USD)
		      .notional(NOTIONAL)
		      .accrualSchedule(PERIOD_SCHEDULE)
		      .settlementDateOffset(DATE_OFFSET)
		      .yieldConvention(YIELD_CONVENTION)
		      .build()
		      .resolve(REF_DATA);
	
	public static void main(String[] args) {
	    double price = 100.14;
	    openGammaCalc(price);
	}

	public static void openGammaCalc(double price){
		LocalDate settlementDate = LocalDate.of(2016, 7, 15);
		double accruedInterest = PRICER.accruedInterest(PRODUCT, settlementDate);
		double yield = PRICER.yieldFromDirtyPrice(PRODUCT, settlementDate, price);
		double modifiedDuration = PRICER.modifiedDurationFromYield(PRODUCT, settlementDate, yield);
		double macaulayDuration = PRICER.macaulayDurationFromYield(PRODUCT, settlementDate, yield);
		System.out.println("accruedInterest = "+accruedInterest);
		System.out.println("modifiedDuration = "+modifiedDuration);
		System.out.println("macaulayDuration = "+macaulayDuration);
	}
}

Doesn’t match with Bloomberg duration or DV01. This is for US treasury bond cusip = 912828S27.

I just realised that the formula for accruedInterest = ((15/184)*0.01125)/2)*1000.

15 days is the days between start date i.e 6/30
184 is days remaining for the next coupon i.e. 12/31 - 06/30.
0.01125 is absolute value of coupon rate.
1000 is face value.

This results in accruedInterest of 45.85 which matches Bloomberg terminal.
However I am not able to replicate this with OpenGamma library call of PRICER.accruedInterest(PRODUCT, settlementDate); It gives accruedInterest = 46.10655737704918
using any of the standard dayCounts.

Hi Sudharshan,

The difference comes from day counting in Act/Act ICMA day count convention: OG counts 183 days for the period between 06/30/2016 and 12/30/2016, whereas Bloomberg does 184.
We continue to investigate this issue.

@yukiiwashita: The unadjusted end date for the first coupon payment between 6/30 and 12/31 should fall on 12/31/2016 right ?
Also, can you please help me implement the duration and DV01 calculation from the openGamma library ? Would the issue of accruedInterest affect these 2 numbers as well ?

In order to set the first coupon date to be 31/12/2016, RollConvention in PeriodicSchedule needs to be used:

private static final PeriodicSchedule PERIOD_SCHEDULE = 
PeriodicSchedule.of(START_DATE, END_DATE, Frequency.P6M, BUSINESS_ADJUST, StubConvention.NONE, false)

is replaced by

private static final PeriodicSchedule PERIOD_SCHEDULE = 
PeriodicSchedule.of(START_DATE, END_DATE, Frequency.P6M, BUSINESS_ADJUST, StubConvention.NONE, true)

Also the day count convention must be

private static final DayCount DAY_COUNT = DayCounts.ACT_ACT_ICMA;

Since yieldFromDirtyPrice is based on dirty price, the clean price needs to be converted:

double dirtyPrice = PRICER.dirtyPriceFromCleanPrice(PRODUCT, settlementDate, price * 0.01); 

The DV01 is not produced by DiscountingFixedCouponBondProductPricer, but can be computed from the modified duration.

I modified my code for the DayCountConvention and rollConvention boolean value as follows:

import java.time.LocalDate;

import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.StandardId;
import com.opengamma.strata.basics.currency.Currency;
import com.opengamma.strata.basics.date.BusinessDayAdjustment;
import com.opengamma.strata.basics.date.BusinessDayConventions;
import com.opengamma.strata.basics.date.DayCount;
import com.opengamma.strata.basics.date.DayCounts;
import com.opengamma.strata.basics.date.DaysAdjustment;
import com.opengamma.strata.basics.date.HolidayCalendarId;
import com.opengamma.strata.basics.date.HolidayCalendarIds;
import com.opengamma.strata.basics.schedule.Frequency;
import com.opengamma.strata.basics.schedule.PeriodicSchedule;
import com.opengamma.strata.basics.schedule.StubConvention;
import com.opengamma.strata.pricer.bond.DiscountingFixedCouponBondProductPricer;
import com.opengamma.strata.product.SecurityId;
import com.opengamma.strata.product.bond.FixedCouponBond;
import com.opengamma.strata.product.bond.FixedCouponBondYieldConvention;
import com.opengamma.strata.product.bond.ResolvedFixedCouponBond;

public class Pricer {
	private static final ReferenceData REF_DATA = ReferenceData.standard();
	private static final StandardId ISSUER_ID = StandardId.of("OG-Ticker", "GOVT1");
	private static final FixedCouponBondYieldConvention YIELD_CONVENTION = FixedCouponBondYieldConvention.US_STREET;
	private static final double NOTIONAL = 1000;
	private static final HolidayCalendarId USNY_CALENDAR = HolidayCalendarIds.USNY;
	private static final DaysAdjustment DATE_OFFSET = DaysAdjustment.ofBusinessDays(1, USNY_CALENDAR);
	private static final DayCount DAY_COUNT = DayCounts.ACT_ACT_ICMA;
	private static final LocalDate START_DATE = LocalDate.of(2016, 06, 30);
	private static final BusinessDayAdjustment BUSINESS_ADJUST = BusinessDayAdjustment.of(BusinessDayConventions.NO_ADJUST, USNY_CALENDAR);
	private static final DiscountingFixedCouponBondProductPricer PRICER = DiscountingFixedCouponBondProductPricer.DEFAULT;
	  
	private static final ResolvedFixedCouponBond _912828S27 = FixedCouponBond.builder()
		      .securityId(SecurityId.of(StandardId.of("OG-Ticker", "912828S27")))
		      .dayCount(DAY_COUNT)
		      .fixedRate(1.125)
		      .legalEntityId(ISSUER_ID)
		      .currency(Currency.USD)
		      .notional(NOTIONAL)
		      .accrualSchedule(PeriodicSchedule.of(START_DATE, LocalDate.of(2021, 6, 30), Frequency.P6M, BUSINESS_ADJUST, StubConvention.NONE, true))
		      .settlementDateOffset(DATE_OFFSET)
		      .yieldConvention(YIELD_CONVENTION)
		      .build()
		      .resolve(REF_DATA);

	private static final ResolvedFixedCouponBond _912828S35 = FixedCouponBond.builder()
		      .securityId(SecurityId.of(StandardId.of("OG-Ticker", "912828S35")))
		      .dayCount(DAY_COUNT)
		      .fixedRate(1.375)
		      .legalEntityId(ISSUER_ID)
		      .currency(Currency.USD)
		      .notional(NOTIONAL)
		      .accrualSchedule(PeriodicSchedule.of(START_DATE, LocalDate.of(2023, 6, 30), Frequency.P6M, BUSINESS_ADJUST, StubConvention.NONE, true))
		      .settlementDateOffset(DATE_OFFSET)
		      .yieldConvention(YIELD_CONVENTION)
		      .build()
		      .resolve(REF_DATA);

	private static final ResolvedFixedCouponBond _912828R36 = FixedCouponBond.builder()
		      .securityId(SecurityId.of(StandardId.of("OG-Ticker", "912828R36")))
		      .dayCount(DAY_COUNT)
		      .fixedRate(1.625)
		      .legalEntityId(ISSUER_ID)
		      .currency(Currency.USD)
		      .notional(NOTIONAL)
		      .accrualSchedule(PeriodicSchedule.of(START_DATE, LocalDate.of(2026, 5, 15), Frequency.P6M, BUSINESS_ADJUST, StubConvention.SHORT_INITIAL, true))
		      .settlementDateOffset(DATE_OFFSET)
		      .yieldConvention(YIELD_CONVENTION)
		      .build()
		      .resolve(REF_DATA);
	
	public static void main(String[] args) {
	    openGammaCalc(_912828S27, 99.921875);
	    openGammaCalc(_912828S35, 100.109);
	    openGammaCalc(_912828R36, 100.8125);
	}

	public static void openGammaCalc(ResolvedFixedCouponBond bond, double cleanPrice){
		LocalDate settlementDate = LocalDate.of(2016, 7, 18);
		double accruedInterest = PRICER.accruedInterest(bond, settlementDate);
		double dirtyPrice = PRICER.dirtyPriceFromCleanPrice(bond, settlementDate, cleanPrice);
		double yield = PRICER.yieldFromDirtyPrice(bond, settlementDate, dirtyPrice);
		double modifiedDuration = PRICER.modifiedDurationFromYield(bond, settlementDate, yield);
		double macaulayDuration = PRICER.macaulayDurationFromYield(bond, settlementDate, yield);
		System.out.println(bond.toString());
		System.out.println("accruedInterest = "+accruedInterest);
		System.out.println("dirtyPrice = "+dirtyPrice);
		System.out.println("yield = "+yield);
		System.out.println("modifiedDuration = "+modifiedDuration);
		System.out.println("macaulayDuration = "+macaulayDuration);
	}
}

Both yield and duration (modified & Macaulay) seem to be different from BBG terminal. Am I building the fixed coupon bond correctly ?

The fixed rates and prices need to be rescaled by 0.01 as done in your first code and my previous comment.

Also, I am wondering whether the third ResolvedFixedCouponBond actually matches what you had intended to create, especially in terms of start date, stub convention and roll convention.

I hope this will help.

Thanks. I was able to match the modifed duration and Macaulay Duration (labelled duration on BBG) for 912828S27 and 912828S35, however yield seems to be still different. Is this a matter of different yield conventions between OpenGamma and BBG.

Also, for cusip 912828R36, when I put in the correct issue date i.e. 2016-05-16 and maturity date 2026-05-15,
I get the following exception:
Caused by: com.opengamma.strata.basics.schedule.ScheduleException: Period ‘2016-05-16’ to ‘2026-05-15’ resulted in a disallowed stub with frequency 'P6M’
at com.opengamma.strata.basics.schedule.PeriodicSchedule.generateForwards(PeriodicSchedule.java:587)
at com.opengamma.strata.basics.schedule.PeriodicSchedule.generateUnadjustedDates(PeriodicSchedule.java:468)
at com.opengamma.strata.basics.schedule.PeriodicSchedule.createSchedule(PeriodicSchedule.java:392)
at com.opengamma.strata.product.bond.FixedCouponBond.resolve(FixedCouponBond.java:163)
at Pricer.(Pricer.java:72)

The bond contruction looks like follows. What should be the correct stub convention and rollconvention be ?

private static final ResolvedFixedCouponBond _912828R36 = FixedCouponBond.builder()
	      .securityId(SecurityId.of(StandardId.of("OG-Ticker", "912828R36")))
	      .dayCount(DAY_COUNT)
	      .fixedRate(1.625 * 0.01)
	      .legalEntityId(ISSUER_ID)
	      .currency(Currency.USD)
	      .notional(NOTIONAL)
	      .accrualSchedule(PeriodicSchedule.of(LocalDate.of(2016, 05, 16), LocalDate.of(2026, 5, 15), Frequency.P6M, BUSINESS_ADJUST, StubConvention.NONE, false))
	      .settlementDateOffset(DATE_OFFSET)
	      .yieldConvention(YIELD_CONVENTION)
	      .build()
	      .resolve(REF_DATA);

Also, for zero coupon bonds, I am using the following constructor:

private static final ResolvedFixedCouponBond _912796KC2_ZERO_COUPON = FixedCouponBond.builder()
	      .securityId(SecurityId.of(StandardId.of("OG-Ticker", "912796KC2")))
	      .dayCount(DayCounts.ACT_ACT_ICMA)
	      .fixedRate(0.0d)
	      .legalEntityId(ISSUER_ID)
	      .currency(Currency.USD)
	      .notional(NOTIONAL)
	      .accrualSchedule(PeriodicSchedule.of(LocalDate.of(2016, 07, 14), LocalDate.of(2017, 1, 12), Frequency.TERM,
	    		  BusinessDayAdjustment.of(BusinessDayConventions.MODIFIED_FOLLOWING, USNY_CALENDAR), StubConvention.NONE, false))
	      .settlementDateOffset(DaysAdjustment.ofBusinessDays(1, USNY_CALENDAR))
	      .yieldConvention(FixedCouponBondYieldConvention.US_STREET)
	      .build()
	      .resolve(REF_DATA);

It gives me the following exception:

Exception in thread “main” com.opengamma.strata.math.MathException: Failed to bracket root: function invalid at x = 0.0 f(x) = NaN
at com.opengamma.strata.math.impl.rootfinding.BracketRoot.getBracketedPoints(BracketRoot.java:43)
at com.opengamma.strata.pricer.bond.DiscountingFixedCouponBondProductPricer.yieldFromDirtyPrice(DiscountingFixedCouponBondProductPricer.java:613)
at Pricer.openGammaCalc(Pricer.java:98)
at Pricer.main(Pricer.java:92)

Ok. I am able to match yield, Macaulay duration and DV01 for 912828R36 with BBG now. The interest accrual start date is to be taken as 5/15/2016 even though issue date of cusip 912828R36 is 5/16/2016.

The issue with zero-coupon bond 912796KC2 remains though. Any help would be appreciated. Thanks.

One way to get the bond to start on 2016-05-16 but accrue from a day earlier is to use “overrideStartDate” and a stub convention as follows:

  private static final ResolvedFixedCouponBond _912828R36 = FixedCouponBond.builder()
      .securityId(SecurityId.of(StandardId.of("OG-Ticker", "912828R36")))
      .dayCount(DAY_COUNT)
      .fixedRate(1.625 * 0.01)
      .legalEntityId(ISSUER_ID)
      .currency(Currency.USD)
      .notional(NOTIONAL)
      .accrualSchedule(PeriodicSchedule.builder()
          .startDate(LocalDate.of(2016, 5, 16))
          .endDate(LocalDate.of(2026, 5, 15))
          .overrideStartDate(AdjustableDate.of(LocalDate.of(2016, 5, 15)))
          .frequency(Frequency.P6M)
          .businessDayAdjustment(BUSINESS_ADJUST)
          .stubConvention(StubConvention.SHORT_INITIAL)
          .build())
      .settlementDateOffset(DATE_OFFSET)
      .yieldConvention(YIELD_CONVENTION)
      .build()
      .resolve(REF_DATA);

Thanks Stephen for clarifying on the override for start date.
For the zero-coupon bond, if I specify the payment frequency as Frequency.P6M, then openGamma library doesn’t throw an exception. Is this a bug ? I am asking because the javadoc mentions clearly that TERM is for zero-coupon bonds.
Also, please note that even after specifying the P6M frequency I am not able to match duration numbers for 912796KC2.

Sadly, at present, Bills are not supported in Strata. To make them work, we would need to add new pricing logic, including new yield conventions (the yield convention is probably part of the reason why the result is wrong).

We do not currently have Bills on our roadmap, but feel free to contact us directly if it is of commercial interest.

Can you please share how you calculated DV01 from the modified duration?
Thanks