For UK Govt Fixed Coupon Bonds, there seems to be some issues around the yield and accrued interest calculations
when the settlement date is in the ex dividend period.
I have used historic prices and yield data from the UK Debt Management Office
https://www.dmo.gov.uk/data/gilt-market/historical-prices-and-yields/
to write some tests to validate these calculations (test code at the end of this post).
At the above URL we can specify a start and end date and get a excel file with the ISIN, cleanPrice, intAcc, dirtyPrice and Yield To Maturity for each business day in the range.
-
The accruedInterest() method returns incorrect values of the first ex dividend day.
DiscountingFixedCouponBondProductPricer#accruedYearFraction() has a check
if (settlementDate.isAfter(period.getDetachmentDate())) {
Looks like this should be more like
if (!settlementDate.isBefore(period.getDetachmentDate())) {
This fixes the accrued interest and the dirty price calculations. -
The yieldFromDirtyPrice() returns incorrect values. There seems to be 2 parts to this.
a) On the first ex-dividend period after the bond is issued, we get incorrect yield for all the days
in the ex dividend period, so the 6 business days.
b) On subsequent ex-dividend periods, we get errors on some days. This seems to be caused by the following
code in dirtyPriceFromYieldStandard()
if ((period.hasExCouponPeriod() && !settlementDate.isAfter(period.getDetachmentDate())) ||
Looks like this should be something like
if ((period.hasExCouponPeriod() && settlementDate.isBefore(period.getDetachmentDate())) ||
For the errors in the first ex dividend period after issue, I am not sure where the probnlem could be.
I suspect something around the following this code in dirtyPriceFromYieldStandard()
int pow = 0;
for (int loopcpn = 0; loopcpn < nbCoupon; loopcpn++) {
FixedCouponBondPaymentPeriod period = bond.getPeriodicPayments().get(loopcpn);
if ((period.hasExCouponPeriod() && settlementDate.isBefore(period.getDetachmentDate())) ||
(!period.hasExCouponPeriod() && period.getPaymentDate().isAfter(settlementDate))) {
pvAtFirstCoupon += fixedRate * period.getYearFraction() / Math.pow(factorOnPeriod, pow);
++pow;
}
}
Since we have the negative interest during the ex-coupon period, and this error seems to be on the
first ex-dividend period, maybe its something to do with the period.yearFraction() not being 1/2.
Maybe some we need to subtract some amount for this, like
int pow = 0;
for (int loopcpn = 0; loopcpn < nbCoupon; loopcpn++) {
FixedCouponBondPaymentPeriod period = bond.getPeriodicPayments().get(loopcpn);
if ((period.hasExCouponPeriod() && settlementDate.isBefore(period.getDetachmentDate())) ||
(!period.hasExCouponPeriod() && period.getPaymentDate().isAfter(settlementDate))) {
pvAtFirstCoupon += fixedRate * period.getYearFraction() / Math.pow(factorOnPeriod, pow);
++pow;
}
if (loopcpn == 0 && period.hasExCouponPeriod() && !settlementDate.isBefore(period.getDetachmentDate()) && settlementDate.isBefore(period.getEndDate())) {
// subtract an amount here ?
}
}
Test code: We use 2 isins
For GB00BZB26Y51 we test in the first ex-dividend period after issue and
For GB00B582JV65 we are testing at a much later ex dividend period.
package com.opengamma.strata.pricer.bond;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.currency.Currency;
import com.opengamma.strata.basics.date.*;
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.product.LegalEntityId;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
@RunWith(Parameterized.class)
public class TestUKTExDividendYieldCalculation {
private ReferenceData refData = ReferenceData.standard();
private static LocalDate localDate(String dateString) {
return LocalDate.parse(dateString, DateTimeFormatter.ofPattern("dd-MMM-yyyy"));
}
private static LocalDate nextWeekday(final LocalDate day) {
LocalDate nextDay = day.plusDays(1);
while (nextDay.getDayOfWeek() == DayOfWeek.SATURDAY || nextDay.getDayOfWeek() == DayOfWeek.SUNDAY) {
nextDay = nextDay.plusDays(1);
}
return nextDay;
}
@Parameterized.Parameters(name = "{index}: {0} {1} - {4}")
public static Collection data() {
return Arrays.asList(new Object[][] {
// ISIN , Ticker, Issue Date, Matirity date, Settlement Date, Coupon Rate, Clean Price, Interest Accumulated, Dirty Price, Yield
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("20-Feb-2017"), 1.750000, 97.440000, 0.502762, 97.942762, 1.901137 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("21-Feb-2017"), 1.750000, 97.120000, 0.507597, 97.627597, 1.920368 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("22-Feb-2017"), 1.750000, 97.700000, 0.512431, 98.212431, 1.885611 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("23-Feb-2017"), 1.750000, 98.450000, 0.517265, 98.967265, 1.841002 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("24-Feb-2017"), 1.750000, 99.640000, -0.038674, 99.601326, 1.771010 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("27-Feb-2017"), 1.750000, 99.640000, -0.033840, 99.606160, 1.771011 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("28-Feb-2017"), 1.750000, 100.040000, -0.029006, 100.010994, 1.747679 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("01-Mar-2017"), 1.750000, 99.300000, -0.024171, 99.275829, 1.790932 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("02-Mar-2017"), 1.750000, 98.970000, -0.019337, 98.950663, 1.810345 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("03-Mar-2017"), 1.750000, 99.080000, -0.004834, 99.075166, 1.803881 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("06-Mar-2017"), 1.750000, 98.780000, 0.000000, 98.780000, 1.821581 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("07-Mar-2017"), 1.750000, 99.270000, 0.004755, 99.274755, 1.792714 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("08-Mar-2017"), 1.750000, 98.600000, 0.009511, 98.609511, 1.832244 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("09-Mar-2017"), 1.750000, 98.820000, 0.014266, 98.834266, 1.819238 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("10-Mar-2017"), 1.750000, 98.300000, 0.028533, 98.328533, 1.850081 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("13-Mar-2017"), 1.750000, 98.070000, 0.033288, 98.103288, 1.863785 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("14-Mar-2017"), 1.750000, 98.260000, 0.038043, 98.298043, 1.852480 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("15-Mar-2017"), 1.750000, 98.550000, 0.042799, 98.592799, 1.835263 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("16-Mar-2017"), 1.750000, 98.400000, 0.047554, 98.447554, 1.844175 },
{ "GB00BZB26Y51", "UKT 1.75 07-Sep-2037", localDate("09-Nov-2016"), localDate("07-Sep-2037"), localDate("17-Mar-2017"), 1.750000, 98.590000, 0.061821, 98.651821, 1.832923 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("20-Feb-2017"), 3.750000, 112.310000, 1.729972, 114.039972, 0.253445 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("21-Feb-2017"), 3.750000, 112.230000, 1.740331, 113.970331, 0.272157 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("22-Feb-2017"), 3.750000, 112.360000, 1.750691, 114.110691, 0.235081 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("23-Feb-2017"), 3.750000, 112.420000, 1.761050, 114.181050, 0.216584 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("24-Feb-2017"), 3.750000, 112.510000, -0.082873, 112.427127, 0.184931 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("27-Feb-2017"), 3.750000, 112.540000, -0.072514, 112.467486, 0.174343 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("28-Feb-2017"), 3.750000, 112.550000, -0.062155, 112.487845, 0.169066 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("01-Mar-2017"), 3.750000, 112.430000, -0.051796, 112.378204, 0.198437 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("02-Mar-2017"), 3.750000, 112.410000, -0.041436, 112.368564, 0.201176 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("03-Mar-2017"), 3.750000, 112.470000, -0.010359, 112.459641, 0.177309 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("06-Mar-2017"), 3.750000, 112.430000, 0.000000, 112.430000, 0.185391 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("07-Mar-2017"), 3.750000, 112.490000, 0.010190, 112.500190, 0.166743 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("08-Mar-2017"), 3.750000, 112.420000, 0.020380, 112.440380, 0.182911 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("09-Mar-2017"), 3.750000, 112.410000, 0.030571, 112.440571, 0.183010 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("10-Mar-2017"), 3.750000, 112.350000, 0.061141, 112.411141, 0.191379 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("13-Mar-2017"), 3.750000, 112.360000, 0.071332, 112.431332, 0.186100 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("14-Mar-2017"), 3.750000, 112.430000, 0.081522, 112.511522, 0.164652 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("15-Mar-2017"), 3.750000, 112.520000, 0.091712, 112.611712, 0.137803 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("16-Mar-2017"), 3.750000, 112.400000, 0.101902, 112.501902, 0.167518 },
{ "GB00B582JV65", "UKT 3.75 07-Sep-2020", localDate("10-Jun-2010"), localDate("07-Sep-2020"), localDate("17-Mar-2017"), 3.750000, 112.380000, 0.132473, 112.512473, 0.165075 }
}).stream().map(row -> {
Object[] newRow = row;
newRow[4] = nextWeekday((LocalDate)row[4]);
return newRow;
}).collect(Collectors.toList());
}
private String isin;
private String ticker;
private LocalDate issueDate;
private LocalDate maturityDate;
private LocalDate settlementDate;
private double couponRate;
private double cleanPrice;
private double expectedIntAcc;
private double expectedDirtyPrice;
private double expectedYield;
public TestUKTExDividendYieldCalculation(String isin, String ticker, LocalDate issueDate,
LocalDate maturityDate, LocalDate settlementDate,
double couponRate, double cleanPrice, double expectedIntAcc, double expectedDirtyPrice, double expectedYield) {
this.isin = isin;
this.ticker = ticker;
this.issueDate = issueDate;
this.maturityDate = maturityDate;
this.settlementDate = settlementDate;
this.couponRate = couponRate;
this.cleanPrice = cleanPrice;
this.expectedIntAcc = expectedIntAcc;
this.expectedDirtyPrice = expectedDirtyPrice;
this.expectedYield = expectedYield;
}
@Test
public void testInterestAndYield() {
ResolvedFixedCouponBond bond = FixedCouponBond.builder()
.securityId(SecurityId.of("Bloomberg-Ticker", ticker))
.dayCount(DayCounts.ACT_ACT_ICMA)
.fixedRate(couponRate / 100d)
.legalEntityId(LegalEntityId.of("CMA-Ticker", "UK"))
.currency(Currency.of("GBP"))
.notional(1_000_000)
.accrualSchedule(PeriodicSchedule.of(issueDate, maturityDate, Frequency.P6M, BusinessDayAdjustment.of(BusinessDayConventions.MODIFIED_FOLLOWING, HolidayCalendarIds.GBLO), StubConvention.SMART_INITIAL, false))
.settlementDateOffset(DaysAdjustment.ofBusinessDays(1, HolidayCalendarIds.GBLO))
.yieldConvention(FixedCouponBondYieldConvention.GB_BUMP_DMO)
.exCouponPeriod(DaysAdjustment.ofBusinessDays(-6, HolidayCalendarIds.GBLO))
.build()
.resolve(refData);
final DiscountingFixedCouponBondProductPricer bondPricer = DiscountingFixedCouponBondProductPricer.DEFAULT;
final double accruedInterest = bondPricer.accruedInterest(bond, settlementDate);
final double dirtyPrice = bondPricer.dirtyPriceFromCleanPrice(bond, settlementDate, cleanPrice/100d);
final double ytm = bondPricer.yieldFromDirtyPrice(bond, settlementDate, dirtyPrice);
assertThat(accruedInterest).describedAs("accrued interest").isCloseTo(expectedIntAcc * 10000, within(1e-2));
assertThat(dirtyPrice).describedAs("dirty price").isCloseTo(expectedDirtyPrice / 100d, within(1e-8));
assertThat(ytm).describedAs("YTM").isCloseTo(expectedYield / 100d, within(1e-8));
}
}