/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.analytics.financial.credit.options; import static com.opengamma.analytics.financial.credit.options.YieldCurveProvider.ISDA_USD_20140205; import static org.testng.AssertJUnit.assertEquals; import org.testng.annotations.Test; import org.threeten.bp.LocalDate; import org.threeten.bp.Period; import com.opengamma.analytics.financial.credit.isdastandardmodel.CDSAnalytic; import com.opengamma.analytics.financial.credit.isdastandardmodel.CDSAnalyticFactory; import com.opengamma.analytics.financial.credit.isdastandardmodel.ISDABaseTest; import com.opengamma.analytics.financial.credit.isdastandardmodel.ISDACompliantCreditCurve; import com.opengamma.analytics.financial.credit.isdastandardmodel.ISDACompliantYieldCurve; import com.opengamma.analytics.financial.credit.isdastandardmodel.MarketQuoteConverter; import com.opengamma.analytics.financial.credit.isdastandardmodel.PriceType; import com.opengamma.analytics.financial.credit.isdastandardmodel.QuotedSpread; import com.opengamma.analytics.financial.model.volatility.BlackFormulaRepository; /** * These test look at options to enter a CDS on * Republic of Italy * RED Pair code 4AB951AB1 * Trade date 5-Feb-2014 * * The options on single name CDS on BBG CDSO screen appears to be an option to enter a bespoke CDS with a running coupon equal to the strike and accrual from T+1 * (i.e. a pre-`Big Bang' type CDS), so these can be priced using the annuity as the numeraire, thus can be priced with the Black formula. * However the test below show that BBG is using some other model. It is likely the BBG is using its own proprietary model which would be impossible to replicate * without more details. */ public class SingleNameCDSOptionTest extends ISDABaseTest { private static final double NOTIONAL = 1e7; private static final LocalDate TRADE_DATE = LocalDate.of(2014, 2, 5); private static final LocalDate CASH_SETTLEMENT_DATE = LocalDate.of(2014, 2, 10); private static final LocalDate EXPIRY = LocalDate.of(2014, 3, 20); private static final LocalDate MATURITY = LocalDate.of(2019, 6, 20); private static final double TRADE_SPREAD = 173.66 * ONE_BP; private static final double COUPON = 100 * ONE_BP; private static final double EXP_FWD_ANNUITY = 4.83644025; private static final double FWD_SPREAD = 182.7666347 * ONE_BP; private static final Period TENOR = Period.ofYears(5); private static final Period[] PILLAR_TENORS = new Period[] {Period.ofMonths(6), Period.ofYears(1), Period.ofYears(2), Period.ofYears(3), Period.ofYears(4), Period.ofYears(5), Period.ofYears(7), Period.ofYears(10) }; private static final double[] PILLAR_PAR_SPREADS; private static final QuotedSpread[] PILLAR_QUOTED_SPREADS; private static final MarketQuoteConverter CONVERTER = new MarketQuoteConverter(); private static final CDSAnalyticFactory FACTORY = new CDSAnalyticFactory(); private static final CDSAnalytic SPOT_CDS = FACTORY.makeIMMCDS(TRADE_DATE, TENOR); private static final CDSAnalytic[] PILLAR_CDS = FACTORY.makeIMMCDS(TRADE_DATE, PILLAR_TENORS); // private static final CDSAnalytic[] PILLAR_CDS = FACTORY.makeCDS(EXPIRY, EXPIRY.plusDays(1), getIMMDateSet(getNextIMMDate(TRADE_DATE), PILLAR_TENORS)); private static ISDACompliantYieldCurve YIELD_CURVE = ISDA_USD_20140205; private static final double[] STRIKES = new double[] {100, 140, 150, 160, 170, 180, 182.767, 190, 200, 210, 220, 230, 250, 300 }; private static final double[] MTM = new double[] {400295.95, 207773.36, 161867.8, 119561.3, 83049.12, 53969.18, 47325.4, 32746.86, 18566.8, 9863.46, 4929, 2327.63, 446.51, 3.6693 }; private static boolean PRINT = false; static { final double[] spreads = new double[] {57.43, 74.97, 111.32, 139.32, 157.64, 173.66, 209.28, 228.35 }; final int n = spreads.length; PILLAR_PAR_SPREADS = new double[n]; PILLAR_QUOTED_SPREADS = new QuotedSpread[n]; for (int i = 0; i < n; i++) { PILLAR_PAR_SPREADS[i] = spreads[i] * ONE_BP; PILLAR_QUOTED_SPREADS[i] = new QuotedSpread(COUPON, PILLAR_PAR_SPREADS[i]); } if (PRINT) { System.out.println("SingleNameCDSOptionTest - set PRINT to false before push"); } } @Test public void upfrontModelTest() { final double puf = CONVERTER.quotedSpreadToPUF(SPOT_CDS, COUPON, YIELD_CURVE, TRADE_SPREAD); assertEquals(3.45676772, puf * ONE_HUNDRED, 1e-8); final double cs01 = NOTIONAL * ONE_BP * CS01_CAL.parallelCS01(SPOT_CDS, new QuotedSpread(COUPON, TRADE_SPREAD), YIELD_CURVE, ONE_BP); assertEquals(4547.5729663, cs01, 1e-7); } /** * BBG treats the pillar spreads (which are quoted spreads) as par spreads to build the credit curve */ @Test public void isdaFairValueTest() { final ISDACompliantCreditCurve cc = CREDIT_CURVE_BUILDER.calibrateCreditCurve(PILLAR_CDS, PILLAR_PAR_SPREADS, YIELD_CURVE); final double puf = PRICER.pv(SPOT_CDS, YIELD_CURVE, cc, COUPON); assertEquals(3.50365852774, puf * ONE_HUNDRED, 1e-11); final double cs01 = NOTIONAL * ONE_BP * CS01_CAL.parallelCS01FromCreditCurve(SPOT_CDS, COUPON, PILLAR_CDS, YIELD_CURVE, cc, ONE_BP); if (PRINT) { System.out.println("CS01:\t" + cs01); } //TODO clearly BBG do not calculate CS01 exactly the same way as us - hence this discrepancy assertEquals(4607.92895, cs01, 2e-1); //0.2 out on notional of 10MM } @Test public void forwardPriceTest() { final double tE = ACT365F.getDayCountFraction(TRADE_DATE, EXPIRY); final double tCS = ACT365F.getDayCountFraction(TRADE_DATE, CASH_SETTLEMENT_DATE); final double dealSpread = 175 * ONE_BP; //The CDS 'seen' at expiry final CDSAnalytic fwdStartingCDS = FACTORY.makeForwardStartingCDS(TRADE_DATE, EXPIRY, EXPIRY.plusDays(1), MATURITY); //fair value curve form par spreads final ISDACompliantCreditCurve cc = CREDIT_CURVE_BUILDER.calibrateCreditCurve(PILLAR_CDS, PILLAR_PAR_SPREADS, YIELD_CURVE); final double expFwdProt = PRICER.protectionLeg(fwdStartingCDS, YIELD_CURVE, cc, 0.0); final double expFwdAnnuity = PRICER.annuity(fwdStartingCDS, YIELD_CURVE, cc, PriceType.CLEAN, 0.0); final double fwdSpread = PRICER.parSpread(fwdStartingCDS, YIELD_CURVE, cc); assertEquals(fwdSpread, expFwdProt / expFwdAnnuity, 1e-15); if (PRINT) { System.out.println("Expected fwd annuity: " + expFwdAnnuity); System.out.println("Forward Par spread: " + fwdSpread * TEN_THOUSAND); } assertEquals("Expected fwd annuity", EXP_FWD_ANNUITY, expFwdAnnuity, 2e-3); //expressed per unit notional and unit coupon assertEquals("Forward spread", FWD_SPREAD * TEN_THOUSAND, fwdSpread * TEN_THOUSAND, 4e-3); //it is not clear what BBG means exactly by ATM Fwd , but this is close; 4-1000th of a bps final double dfCS = YIELD_CURVE.getDiscountFactor(tCS); //this is the expected value of a CDS with a coupon of dealSpread on cash settlement date final double cashAmt = NOTIONAL * (fwdSpread - dealSpread) * expFwdAnnuity / dfCS; if (PRINT) { System.out.println("cashAmt: " + cashAmt); } assertEquals("cashAmt", 37563.68, cashAmt, 21); //$21 out on notional of 10MM //price option with Black final double vol = 0.4; final double oPrice = NOTIONAL * expFwdAnnuity * BlackFormulaRepository.price(fwdSpread, dealSpread, tE, vol, true); if (PRINT) { System.out.println("option price: " + oPrice); } assertEquals(67521.44, oPrice, 1000); //option price out by $1000 when using Black formula with fwd spread and annuity we calculated } /** * in this test we used the annuity and forward derived from the put-call parity of BBG option prices in the Black formula, and imply the volatility * corresponding to the BBG price. We find a large discrepancy, which suggests BBG does not use the standard Black framework for these options */ @Test(enabled = false) public void blackOptionTest() { final double tEAlt = ACT_ACT_ISDA.getDayCountFraction(TRADE_DATE, EXPIRY); System.out.println("time-to-expiry: " + tEAlt); final double vol = 0.4; final int n = STRIKES.length; for (int i = 0; i < n; i++) { final double p = NOTIONAL * EXP_FWD_ANNUITY * BlackFormulaRepository.price(FWD_SPREAD, STRIKES[i] * ONE_BP, tEAlt, vol, true); final double impVol = BlackFormulaRepository.impliedVolatility(MTM[i] / NOTIONAL / EXP_FWD_ANNUITY, FWD_SPREAD, STRIKES[i] * ONE_BP, tEAlt, true); // final double impVol2 = BlackFormulaRepository.impliedVolatility(p / NOTIONAL / EXP_FWD_ANNUITY, FWD_SPREAD, STRIKES[i] * ONE_BP, tEAlt, true); System.out.println(STRIKES[i] + "\t" + p + "\t" + impVol + "\t" + (p / MTM[i])); // assertEquals(vol, impVol, 2e-2); } } /** * In this test we used our own calculations for the forward and annuity in the Black formula. The same comments as above apply */ @Test public void blackOptionTest2() { final double tEAlt = ACT_ACT_ISDA.getDayCountFraction(TRADE_DATE, EXPIRY); //The CDS 'seen' at expiry final CDSAnalytic fwdCDS = FACTORY.makeCDS(TRADE_DATE, EXPIRY.plusDays(1), EXPIRY.plusDays(1), EXPIRY.plusDays(1), MATURITY); //fair value curve form par spreads final ISDACompliantCreditCurve cc = CREDIT_CURVE_BUILDER.calibrateCreditCurve(PILLAR_CDS, PILLAR_PAR_SPREADS, YIELD_CURVE); final double expFwdProt = PRICER.protectionLeg(fwdCDS, YIELD_CURVE, cc, 0.0); final double expFwdAnnuity = PRICER.annuity(fwdCDS, YIELD_CURVE, cc, PriceType.CLEAN, 0.0); final double fwdSpread = expFwdProt / expFwdAnnuity; final double vol = 0.4; final int n = STRIKES.length; for (int i = 0; i < n; i++) { final double p = NOTIONAL * expFwdAnnuity * BlackFormulaRepository.price(fwdSpread, STRIKES[i] * ONE_BP, tEAlt, vol, true); final double p2 = NOTIONAL * EXP_FWD_ANNUITY * BlackFormulaRepository.price(FWD_SPREAD, STRIKES[i] * ONE_BP, tEAlt, vol, true); final double impVol = BlackFormulaRepository.impliedVolatility(MTM[i] / NOTIONAL / expFwdAnnuity, fwdSpread, STRIKES[i] * ONE_BP, tEAlt, true); final double impVol2 = BlackFormulaRepository.impliedVolatility(MTM[i] / NOTIONAL / EXP_FWD_ANNUITY, FWD_SPREAD, STRIKES[i] * ONE_BP, tEAlt, true); if (PRINT) { System.out.println(STRIKES[i] + "\t" + p + "\t" + p2 + "\t" + impVol + "\t" + impVol2); } if (i > 0) { assertEquals(vol, impVol, 2e-2); } } } }