Skip to content

Commit

Permalink
Support Getting Clean and Dirty Price from CDS
Browse files Browse the repository at this point in the history
  • Loading branch information
bleunguts committed May 1, 2024
1 parent 865d08b commit 2a5e0bd
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 6 deletions.
36 changes: 33 additions & 3 deletions ProjectX.AnalyticsLib.Tests/CreditDefaultSwapFunctionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ public void WhenValuingCdsPVWithMultipleSpreadPointsItShouldReturnValidPVAndProb
var recoveryRate = 0.4;
var couponInBps = 100;
var notional = 10_000;
Protection.Side protectionSide = Protection.Side.Buyer;
var interestRate = 0.07;
Protection.Side protectionSide = Protection.Side.Buyer;
var actual = CreditDefaultSwapFunctions.PV(
evalDate,
effectiveDate,
Expand All @@ -63,11 +62,42 @@ public void WhenValuingCdsPVWithMultipleSpreadPointsItShouldReturnValidPVAndProb
couponInBps,
notional,
protectionSide,
interestRate);
0.07);
Assert.That(actual.SurvivalProbabilityPercentage, Is.EqualTo(96.3).Within(1));
Assert.That(actual.DefaultProbabilityPercentage, Is.EqualTo(3.6).Within(1));
Assert.That(actual.HazardRatePercentage, Is.EqualTo(1.8).Within(1));
Assert.That(actual.PV, Is.EqualTo(-137).Within(1), "PV must be equal to expected value within tolerance");
Assert.That(actual.FairSpread, Is.EqualTo(51.3).Within(1), "Fair spread must be equal to expected value within tolerance");
}

[Test]
public void WhenCdsPriceShouldReturnCleanAndDirtyPrice()
{
var evalDate = new DateTime(2015, 5, 15);
var effectiveDate = new DateTime(2015, 3, 20);
var maturityDate = new DateTime(2018, 6, 20);
var spreadsInBps = new double[] { 34.93, 53.6, 72.02, 106.39, 129.39, 139.46 };
var tenors = new string[] { "1Y", "2Y", "3Y", "5Y", "7Y", "10Y" };
var recoveryRate = 0.4;
var couponInBps = 100;
var couponFrequency = Frequency.Annual;
Protection.Side protectionSide = Protection.Side.Buyer;

var actual = CreditDefaultSwapFunctions.Price(
evalDate,
effectiveDate,
maturityDate,
spreadsInBps,
tenors,
recoveryRate,
couponInBps,
couponFrequency,
protectionSide,
0.07);

Assert.That(actual.cleanPrice, Is.EqualTo(100.43).Within(1));
Assert.That(actual.dirtyPrice, Is.EqualTo(100.67).Within(1));
Assert.That(actual.dirtyPrice, Is.GreaterThan(actual.cleanPrice));
Assert.That(actual.riskyAnnuity, Is.EqualTo(-0.04).Within(1));
}
}
62 changes: 59 additions & 3 deletions ProjectX.AnalyticsLib/CreditDefaultSwapFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,54 @@
using QLNet;

namespace ProjectX.AnalyticsLib;

/// <summary>
/// For CDS buyers or sellers the present value of a CDS contract is all what they care about.
/// For quants, we want to calculate accrual amount, risk annuity (DV01), dirty price, clean price @ $100
/// </summary>
public class CreditDefaultSwapFunctions
{
public static CreditDefaultSwapPriceResult Price(
DateTime evalDate,
DateTime effectiveDate,
DateTime maturityDate,
double[] spreadsInBps,
string[] tenors,
double recoveryRate,
double couponInBps,
Frequency couponFrequency,
Protection.Side protectionSide,
double flatInterestRate)
{
// we want to know the dirty price and clean price for a notional of $100, just like a bond with a face value of $100
var cds = PV(evalDate, effectiveDate, maturityDate, spreadsInBps, tenors, recoveryRate, couponInBps, 100, protectionSide, flatInterestRate);
double upfront = cds.PV;
double dirtyPrice = protectionSide switch
{
Protection.Side.Buyer => 100 - upfront,
Protection.Side.Seller => 100 + upfront,
_ => throw new NotImplementedException(),
};
int numDays = effectiveDate.Subtract(evalDate).Days + 1;
double accrual = couponInBps * numDays / 360.0 / 100.0;
double cleanPrice = protectionSide switch
{
Protection.Side.Buyer => dirtyPrice + accrual,
Protection.Side.Seller => dirtyPrice - accrual,
_ => throw new NotImplementedException(),
};

// compute risky annuity (dv01)
// the risky duration (dv01) relates to a trade and is the change in mark-to-market of a CDS trade for a 1 basis point parallel shift in spreads.

// for a par trade Sinitial = SCurrent risky duration is equal to the risky annuity.
double cds2coupon = cds.FairSpread + 1;
var cds2 = PV(evalDate, effectiveDate, maturityDate, spreadsInBps, tenors, recoveryRate, cds2coupon, 100, protectionSide, flatInterestRate);
double riskyAnnuity = cds2.PV;
return (cleanPrice, dirtyPrice, riskyAnnuity);
}

/// <summary>
/// Uses Flat Interest Rate Curve - instead of PiecewiseLogCubic ISDA rate curves
/// Uses Flat Interest Rate Curve. Other possible implementations include market ISDA rate curve (PiecewiseLogCubic)
/// </summary>
public static CreditDefaultSwapPVResult PV(
DateTime evaluationDate,
Expand All @@ -19,7 +62,7 @@ public class CreditDefaultSwapFunctions
double[] spreadsInBps,
string[] tenors,
double recoveryRate,
int couponInBps,
double couponInBps,
int notional,
Protection.Side protectionSide,
double flatInterestRate)
Expand Down Expand Up @@ -96,3 +139,16 @@ public static implicit operator (double pv, double fairSpread, double survivalPr
return new CreditDefaultSwapPVResult(value.pv, value.fairSpread, value.survivalProbabilityPercentage, value.hazardRatePercenatge, value.defaultProbabilityPercentage);
}
}

public record struct CreditDefaultSwapPriceResult(double cleanPrice, double dirtyPrice, double riskyAnnuity)
{
public static implicit operator (double cleanPrice, double dirtyPrice, double riskyAnnuity)(CreditDefaultSwapPriceResult value)
{
return (value.cleanPrice, value.dirtyPrice, value.riskyAnnuity);
}

public static implicit operator CreditDefaultSwapPriceResult((double cleanPrice, double dirtyPrice, double riskyAnnuity) value)
{
return new CreditDefaultSwapPriceResult(value.cleanPrice, value.dirtyPrice, value.riskyAnnuity);
}
}

0 comments on commit 2a5e0bd

Please sign in to comment.