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