/**
* Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.impl.credit.isda;
import static com.opengamma.strata.pricer.impl.credit.isda.IsdaCompliantScheduleGenerator.getIntegrationNodesAsDates;
import static com.opengamma.strata.pricer.impl.credit.isda.IsdaCompliantScheduleGenerator.truncateList;
import java.time.LocalDate;
import java.time.Period;
import com.opengamma.strata.basics.date.BusinessDayConvention;
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.HolidayCalendar;
import com.opengamma.strata.basics.date.HolidayCalendars;
import com.opengamma.strata.basics.schedule.StubConvention;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.math.MathException;
/**
*This prices a CDS using the ISDA methodology. The API of the public functions mimic as far a possible the ISDA high level ISDA c
*functions. However this is NOT a line-by-line translation of the ISDA code. We find agreement with ISDA to better than 1 part in 10^12
*on a test suit of 200 example.
*/
public class IsdaCompliantPresentValueCreditDefaultSwap {
@SuppressWarnings("unused")
private static final int DEFAULT_CASH_SETTLEMENT_DAYS = 3;
private static final BusinessDayConvention FOLLOWING = BusinessDayConventions.FOLLOWING;
private static final HolidayCalendar DEFAULT_CALENDAR = HolidayCalendars.SAT_SUN;
private static final DayCount ACT_365 = DayCounts.ACT_365F;
private static final DayCount ACT_360 = DayCounts.ACT_360;
private final BusinessDayConvention _businessdayAdjustmentConvention;
private final HolidayCalendar _calandar;
private final DayCount _accuralDayCount;
private final DayCount _curveDayCount;
public IsdaCompliantPresentValueCreditDefaultSwap() {
_businessdayAdjustmentConvention = FOLLOWING;
_calandar = DEFAULT_CALENDAR;
_accuralDayCount = ACT_360;
_curveDayCount = ACT_365;
}
/**
* This is the present value of the premium leg per unit of fractional spread.
* <p>
* It hence it is equal to 10,000 times the RPV01 (Risky PV01). The actual PV of the leg
* is this multiplied by the notional and the fractional spread (i.e. spread in basis
* points divided by 10,000).
* <p>
* This mimics the ISDA c function <b>JpmcdsCdsFeeLegPV</b>.
*
* @param today the 'current' date
* @param stepinDate the date when party assumes ownership. This is normally today + 1 (T+1).
* Aka assignment date or effective date.
* @param valueDate the valuation date. The date that values are PVed to. Is is normally today + 3 business days. Aka cash-settle date.
* @param startDate the protection start date. If protectStart = true, then protections starts
* at the beginning of the day, otherwise it is at the end.
* @param endDate the protection end date (the protection ends at end of day)
* @param payAccOnDefault is the accrued premium paid in the event of a default
* @param tenor the nominal step between premium payments (e.g. 3 months, 6 months)
* @param stubType the stub convention
* @param yieldCurve the curve from which payments are discounted
* @param hazardRateCurve the curve giving survival probability
* @param protectStart whether protection starts at the beginning of the day
* @param priceType the Clean or Dirty price flag. The clean price removes the accrued premium
* if the trade is between payment times.
* @return 10,000 times the RPV01 (on a notional of 1)
*/
public double pvPremiumLegPerUnitSpread(
LocalDate today,
LocalDate stepinDate,
LocalDate valueDate,
LocalDate startDate,
LocalDate endDate,
boolean payAccOnDefault,
Period tenor,
StubConvention stubType,
IsdaCompliantDateYieldCurve yieldCurve,
IsdaCompliantDateCreditCurve hazardRateCurve,
boolean protectStart,
CdsPriceType priceType) {
ArgChecker.notNull(today, "null today");
ArgChecker.notNull(stepinDate, "null stepinDate");
ArgChecker.notNull(valueDate, "null valueDate");
ArgChecker.notNull(startDate, "null startDate");
ArgChecker.notNull(endDate, "null endDate");
ArgChecker.notNull(tenor, "null tenor");
ArgChecker.notNull(stubType, "null stubType");
ArgChecker.notNull(yieldCurve, "null yieldCurve");
ArgChecker.notNull(hazardRateCurve, "null hazardRateCurve");
ArgChecker.notNull(priceType, "null priceType");
ArgChecker.isFalse(valueDate.isBefore(today), "Require valueDate >= today");
ArgChecker.isFalse(stepinDate.isBefore(today), "Require stepin >= today");
IsdaPremiumLegSchedule paymentSchedule = new IsdaPremiumLegSchedule(
startDate, endDate, tenor, stubType, _businessdayAdjustmentConvention, _calandar, protectStart);
int nPayments = paymentSchedule.getNumPayments();
// these are potentially different from startDate and endDate
LocalDate globalAccStart = paymentSchedule.getAccStartDate(0);
LocalDate golobalAccEnd = paymentSchedule.getAccEndDate(nPayments - 1);
// TODO this logic could be part of ISDAPremiumLegSchdule
LocalDate matDate = protectStart ? golobalAccEnd.minusDays(1) : golobalAccEnd;
if (today.isAfter(matDate) || stepinDate.isAfter(matDate)) {
return 0.0; // trade has expired
}
LocalDate[] yieldCurveDates = yieldCurve.getCurveDates();
LocalDate[] creditCurveDates = hazardRateCurve.getCurveDates();
// This is common to the protection leg
LocalDate[] integrationSchedule = payAccOnDefault ?
getIntegrationNodesAsDates(globalAccStart, golobalAccEnd, yieldCurveDates, creditCurveDates) :
null;
int obsOffset = protectStart ? -1 : 0; // protection start at the beginning or end day
double rpv01 = 0.0;
for (int i = 0; i < nPayments; i++) {
LocalDate accStart = paymentSchedule.getAccStartDate(i);
LocalDate accEnd = paymentSchedule.getAccEndDate(i);
LocalDate pay = paymentSchedule.getPaymentDate(i);
if (!accEnd.isAfter(stepinDate)) {
continue; // this cashflow has already been realised
}
double[] temp = calculateSinglePeriodRPV01(today, accStart, accEnd, pay, obsOffset, yieldCurve, hazardRateCurve);
rpv01 += temp[0];
if (payAccOnDefault) {
LocalDate offsetStepinDate = stepinDate.plusDays(obsOffset);
LocalDate offsetAccStartDate = accStart.plusDays(obsOffset);
LocalDate offsetAccEndDate = accEnd.plusDays(obsOffset);
rpv01 += calculateSinglePeriodAccrualOnDefault(
today, offsetStepinDate, offsetAccStartDate, offsetAccEndDate,
temp[1], yieldCurve, hazardRateCurve, integrationSchedule);
}
}
// Compute the discount factor discounting the upfront payment made on the cash settlement date back to the valuation date
double t = _curveDayCount.yearFraction(today, valueDate);
double df = yieldCurve.getDiscountFactor(t);
rpv01 /= df;
// Do we want to calculate the clean price (includes the previously accrued portion of the premium)
if (priceType == CdsPriceType.CLEAN) {
rpv01 -= calculateAccruedInterest(paymentSchedule, stepinDate);
}
return rpv01;
}
/**
* Computes the risky present value of a premium payment.
* <p>
* This mimics the ISDA c code function <b>FeePaymentPVWithTimeLine<b>.
*
* @param today the date today
* @param accStartDate the start date
* @param accEndDate the end date
* @param paymentDate the payment date
* @param obsOffset the offset
* @param yieldCurve the yield curve
* @param hazardRateCurve the hazard curve
* @return PV
*/
private double[] calculateSinglePeriodRPV01(
LocalDate today,
LocalDate accStartDate,
LocalDate accEndDate,
LocalDate paymentDate,
int obsOffset,
IsdaCompliantDateYieldCurve yieldCurve,
IsdaCompliantDateCreditCurve hazardRateCurve) {
double accTime = _accuralDayCount.yearFraction(accStartDate, accEndDate);
double t = _curveDayCount.yearFraction(today, paymentDate);
double tObsOffset = _curveDayCount.yearFraction(today, accEndDate.plusDays(obsOffset));
// TODO Do we need this?
// Compensate Java shortcoming
if (Double.compare(t, -0.0) == 0) {
t = 0;
}
if (Double.compare(tObsOffset, -0.0) == 0) {
tObsOffset = 0;
}
double survival = hazardRateCurve.getSurvivalProbability(tObsOffset);
double discount = yieldCurve.getDiscountFactor(t);
return new double[] {accTime * discount * survival, accTime};
}
/**
* This mimics the ISDA c JpmcdsAccrualOnDefaultPVWithTimeLine.
*
* @param today the date today
* @param offsetStepinDate the step in date
* @param offsetAccStartDate the start date
* @param offsetAccEndDate the end date
* @param accTime the time
* @param yieldCurve the yield curve
* @param hazardRateCurve the hazard rate curve
* @param integrationSchedule the schedule
* @return the single period accrual on default
*/
private double calculateSinglePeriodAccrualOnDefault(
LocalDate today,
LocalDate offsetStepinDate,
LocalDate offsetAccStartDate,
LocalDate offsetAccEndDate,
double accTime,
IsdaCompliantDateYieldCurve yieldCurve,
IsdaCompliantDateCreditCurve hazardRateCurve,
LocalDate[] integrationSchedule) {
LocalDate[] truncatedDateList = truncateList(offsetAccStartDate, offsetAccEndDate, integrationSchedule);
int nItems = truncatedDateList.length;
// max(offsetStepinDate,offsetAccStartDate)
LocalDate subStartDate = offsetStepinDate.isAfter(offsetAccStartDate) ? offsetStepinDate : offsetAccStartDate;
double tAcc = ACT_365.yearFraction(offsetAccStartDate, offsetAccEndDate); // This is hardcoded to ACT/365 in ISDA code
double accRate = accTime / tAcc;
double t = ACT_365.yearFraction(today, subStartDate);
// Compensate Java shortcoming
if (Double.compare(t, -0.0) == 0) {
t = 0;
}
double s0 = hazardRateCurve.getSurvivalProbability(t);
double df0 = yieldCurve.getDiscountFactor(t);
double myPV = 0.0;
for (int j = 1; j < nItems; ++j) {
if (!truncatedDateList[j].isAfter(offsetStepinDate)) {
continue;
}
double thisAccPV = 0.0;
t = ACT_365.yearFraction(today, truncatedDateList[j]);
double s1 = hazardRateCurve.getSurvivalProbability(t);
double df1 = yieldCurve.getDiscountFactor(t);
double t0 = ACT_365.yearFraction(offsetAccStartDate, subStartDate) + 1 / 730.; // add on half a day
double t1 = ACT_365.yearFraction(offsetAccStartDate, truncatedDateList[j]) + 1 / 730.;
t = t1 - t0; // t repurposed
// TODO check for s0 == s1 -> zero prob of default (and thus zero PV contribution) from this section
double lambda = Math.log(s0 / s1) / t;
double fwdRate = Math.log(df0 / df1) / t;
double lambdafwdRate = lambda + fwdRate + 1.0e-50;
thisAccPV = lambda * accRate * s0 * df0 *
((t0 + 1.0 / (lambdafwdRate)) / (lambdafwdRate) - (t1 + 1.0 / (lambdafwdRate)) / (lambdafwdRate) * s1 / s0 * df1 / df0);
myPV += thisAccPV;
s0 = s1;
df0 = df1;
subStartDate = truncatedDateList[j];
}
return myPV;
}
/**
* Calculate the accrued premium at the start of a trade.
*
* @param premiumLegSchedule the schedule
* @param stepinDate the trade effective date
* @return accrued premium
*/
private double calculateAccruedInterest(IsdaPremiumLegSchedule premiumLegSchedule, LocalDate stepinDate) {
int n = premiumLegSchedule.getNumPayments();
// stepinDate is before first accStart or after last accEnd
if (!stepinDate.isAfter(premiumLegSchedule.getAccStartDate(0)) ||
!stepinDate.isBefore(premiumLegSchedule.getAccEndDate(n - 1))) {
return 0.0;
}
int index = premiumLegSchedule.getAccStartDateIndex(stepinDate);
if (index >= 0) {
return 0.0; // on accrual start date
}
index = -(index + 1); // binary search notation
if (index == 0) {
throw new MathException("Error in calculateAccruedInterest - check logic"); // this should never be hit
}
return _accuralDayCount.yearFraction(premiumLegSchedule.getAccStartDate(index - 1), stepinDate);
}
/**
* Get the value of the protection leg for unit notional<p>
* This mimics the ISDA c function <b>JpmcdsCdsContingentLegPV</b>.
*
* @param today the date today
* @param stepinDate the date when party assumes ownership. This is normally today
* + 1 (T+1). Aka assignment date or effective date.
* @param valueDate the valuation date. The date that values are PVed to. Is is
* normally today + 3 business days. Aka cash-settle date.
* @param startDate the protection start date. If protectStart = true, then protections
* starts at the beginning of the day, otherwise it* is at the end.
* @param endDate the protection end date (the protection ends at end of day)
* @param yieldCurve the curve from which payments are discounted
* @param hazardRateCurve the curve giving survival probability
* @param recoveryRate the recovery rate of the protected debt
* @param protectStart whether protection starts at the beginning of the day
* @return unit notional PV of protection (or contingent) leg
*/
public double calculateProtectionLeg(
LocalDate today,
LocalDate stepinDate,
LocalDate valueDate,
LocalDate startDate,
LocalDate endDate,
IsdaCompliantDateYieldCurve yieldCurve,
IsdaCompliantDateCreditCurve hazardRateCurve,
double recoveryRate,
boolean protectStart) {
ArgChecker.notNull(today, "null today");
ArgChecker.notNull(valueDate, "null valueDate");
ArgChecker.notNull(startDate, "null startDate");
ArgChecker.notNull(endDate, "null endDate");
ArgChecker.notNull(yieldCurve, "null yieldCurve");
ArgChecker.notNull(hazardRateCurve, "null hazardRateCurve");
ArgChecker.inRangeInclusive(recoveryRate, 0d, 1d, "recoveryRate");
ArgChecker.isFalse(valueDate.isBefore(today), "Require valueDate >= today");
ArgChecker.isFalse(stepinDate.isBefore(today), "Require stepin >= today");
if (recoveryRate == 1.0) {
return 0.0;
}
LocalDate temp = stepinDate.isAfter(startDate) ? stepinDate : startDate;
LocalDate effectiveStartDate = protectStart ? temp.minusDays(1) : temp;
if (!endDate.isAfter(effectiveStartDate)) {
return 0.0; // the protection has expired
}
LocalDate[] yieldCurveDates = yieldCurve.getCurveDates();
LocalDate[] creditCurveDates = hazardRateCurve.getCurveDates();
double[] integrationSchedule = IsdaCompliantScheduleGenerator.getIntegrationNodesAsTimes(today, effectiveStartDate,
endDate, yieldCurveDates, creditCurveDates);
double ht1 = hazardRateCurve.getRT(integrationSchedule[0]);
double rt1 = yieldCurve.getRT(integrationSchedule[0]);
double s1 = Math.exp(-ht1);
double p1 = Math.exp(-rt1);
double pv = 0.0;
int n = integrationSchedule.length;
for (int i = 1; i < n; ++i) {
double ht0 = ht1;
double rt0 = rt1;
double p0 = p1;
double s0 = s1;
ht1 = hazardRateCurve.getRT(integrationSchedule[i]);
rt1 = yieldCurve.getRT(integrationSchedule[i]);
s1 = Math.exp(-ht1);
p1 = Math.exp(-rt1);
double dht = ht1 - ht0;
double drt = rt1 - rt0;
double dhrt = dht + drt;
// this is equivalent to the ISDA code without explicitly calculating the time step - it also handles the limit
double dPV;
if (Math.abs(dhrt) < 1e-5) {
dPV = dht * (1 - dhrt * (0.5 - dhrt / 6)) * p0 * s0;
} else {
dPV = dht / dhrt * (p0 * s0 - p1 * s1);
}
// ISDA code
pv += dPV;
}
pv *= 1.0 - recoveryRate;
// Compute the discount factor discounting the upfront payment made on the cash settlement date back to the valuation date
double t = _curveDayCount.yearFraction(today, valueDate);
double df = yieldCurve.getDiscountFactor(t);
pv /= df;
return pv;
}
}