From 2a5e0bd0f7236c755685f1cae251524ba82d9628 Mon Sep 17 00:00:00 2001
From: Billy Leung <82518709+bleunguts@users.noreply.github.com>
Date: Wed, 1 May 2024 10:26:46 +0100
Subject: [PATCH] Support Getting Clean and Dirty Price from CDS
---
.../CreditDefaultSwapFunctionsTest.cs | 36 ++++++++++-
.../CreditDefaultSwapFunctions.cs | 62 ++++++++++++++++++-
2 files changed, 92 insertions(+), 6 deletions(-)
diff --git a/ProjectX.AnalyticsLib.Tests/CreditDefaultSwapFunctionsTest.cs b/ProjectX.AnalyticsLib.Tests/CreditDefaultSwapFunctionsTest.cs
index 598326a..96f2179 100644
--- a/ProjectX.AnalyticsLib.Tests/CreditDefaultSwapFunctionsTest.cs
+++ b/ProjectX.AnalyticsLib.Tests/CreditDefaultSwapFunctionsTest.cs
@@ -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,
@@ -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));
+ }
}
diff --git a/ProjectX.AnalyticsLib/CreditDefaultSwapFunctions.cs b/ProjectX.AnalyticsLib/CreditDefaultSwapFunctions.cs
index a36393e..ef50bff 100644
--- a/ProjectX.AnalyticsLib/CreditDefaultSwapFunctions.cs
+++ b/ProjectX.AnalyticsLib/CreditDefaultSwapFunctions.cs
@@ -6,11 +6,54 @@
using QLNet;
namespace ProjectX.AnalyticsLib;
-
+///
+/// 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
+///
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);
+ }
+
///
- /// 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)
///
public static CreditDefaultSwapPVResult PV(
DateTime evaluationDate,
@@ -19,7 +62,7 @@ public class CreditDefaultSwapFunctions
double[] spreadsInBps,
string[] tenors,
double recoveryRate,
- int couponInBps,
+ double couponInBps,
int notional,
Protection.Side protectionSide,
double flatInterestRate)
@@ -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);
+ }
+}
\ No newline at end of file