Skip to content

Commit

Permalink
Added CreditDefaultSwap Valuation Analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
bleunguts committed May 1, 2024
1 parent d570e43 commit 0d9f8f0
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 89 deletions.
42 changes: 42 additions & 0 deletions ProjectX.AnalyticsLib.Tests/CreditDefaultSwapFunctionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using QLNet;

namespace ProjectX.AnalyticsLib.Tests;

public class CreditDefaultSwapFunctionsTest
{
[Test]
public void WhenValuingCdsPVUsingFlatInterestCurveItShouldReturnValidPV()
{
var evalDate = new DateTime(2009, 6, 15);
var effectiveDate = new DateTime(2009, 3, 20);
var maturityDate = new DateTime(2014, 6, 20);
var spreadsInBps = new int[] { 210 };
var tenors = new string[] { "5Y" };
var recoveryRate = 0.4;
var couponInBps = 100;
var notional = 10_000;
Protection.Side protectionSide = Protection.Side.Buyer;
var interestRate = 0.15;
var actual = CreditDefaultSwapFunctions.PV(
evalDate,
effectiveDate,
maturityDate,
spreadsInBps,
tenors,
recoveryRate,
couponInBps,
notional,
protectionSide,
interestRate);
Assert.That(actual.SurvivalProbabilityPercentage, Is.EqualTo(82).Within(1));
Assert.That(actual.DefaultProbabilityPercentage, Is.EqualTo(17).Within(1));
Assert.That(actual.HazardRatePercentage, Is.EqualTo(3).Within(1));
Assert.That(actual.PV, Is.EqualTo(405).Within(1), "PV must be equal to expected value within tolerance");
Assert.That(actual.FairSpread, Is.EqualTo(224).Within(1), "Fair spread must be equal to expected value within tolerance");
}
}
85 changes: 42 additions & 43 deletions ProjectX.AnalyticsLib.Tests/ExoticOptionsPricingCalculatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,49 @@
using System.Text;
using System.Threading.Tasks;

namespace ProjectX.AnalyticsLib.Tests
namespace ProjectX.AnalyticsLib.Tests;

public class ExoticOptionsPricingCalculatorTest
{
public class ExoticOptionsPricingCalculatorTest
private ExoticOptionsPricingCalculator _calculator = new ExoticOptionsPricingCalculator(new BlackScholesOptionsPricer());

[Test]
public void WhenPricingAnAmericanOption()
{
private ExoticOptionsPricingCalculator _calculator = new ExoticOptionsPricingCalculator(new BlackScholesOptionsPricer());

[Test]
public void WhenPricingAnAmericanOption()
{
var spot = 90.0;
var strike = 100.0;
var rate = 0.1;
var divYield = 0.1;
var maturity = 0.10;
var vol = 0.15;

var price = _calculator.American_BaroneAdesiWhaley(OptionType.Call, spot, strike, rate, divYield, maturity, vol);
Console.WriteLine($"Price of call american option is {price}");
Assert.That(price, Is.EqualTo(0.0260).Within(1).Percent);

var putPrice = _calculator.American_BaroneAdesiWhaley(OptionType.Put, spot, strike, rate, divYield, maturity, vol);
Console.WriteLine($"Price of put american option is {putPrice}");
Assert.That(putPrice, Is.EqualTo(10.00).Within(1).Percent);
}

[Test]
public void WhenPricingABarrierOption()
{
var spot = 100.0;
var strike = 100.0;
var rate = 0.1;
var yield = 0.06;
var maturity = 0.1;
var vol = 0.3;
var barrier = 90;
var rebate = 0;

var price = _calculator.BarrierOptions(OptionType.Call, BarrierType.DownIn, spot, strike, rate, yield, maturity, vol, barrier, rebate);
Console.WriteLine($"Price of call barrier option is {price}");
Assert.That(price, Is.EqualTo(0.0444).Within(1).Percent);

var putPrice = _calculator.BarrierOptions(OptionType.Put, BarrierType.DownIn, spot, strike, rate, yield, maturity, vol, barrier, rebate);
Console.WriteLine($"Price of put barrier option is {putPrice}");
Assert.That(putPrice, Is.EqualTo(2.65).Within(1).Percent);
}
var spot = 90.0;
var strike = 100.0;
var rate = 0.1;
var divYield = 0.1;
var maturity = 0.10;
var vol = 0.15;

var price = _calculator.American_BaroneAdesiWhaley(OptionType.Call, spot, strike, rate, divYield, maturity, vol);
Console.WriteLine($"Price of call american option is {price}");
Assert.That(price, Is.EqualTo(0.0260).Within(1).Percent);

var putPrice = _calculator.American_BaroneAdesiWhaley(OptionType.Put, spot, strike, rate, divYield, maturity, vol);
Console.WriteLine($"Price of put american option is {putPrice}");
Assert.That(putPrice, Is.EqualTo(10.00).Within(1).Percent);
}

[Test]
public void WhenPricingABarrierOption()
{
var spot = 100.0;
var strike = 100.0;
var rate = 0.1;
var yield = 0.06;
var maturity = 0.1;
var vol = 0.3;
var barrier = 90;
var rebate = 0;

var price = _calculator.BarrierOptions(OptionType.Call, BarrierType.DownIn, spot, strike, rate, yield, maturity, vol, barrier, rebate);
Console.WriteLine($"Price of call barrier option is {price}");
Assert.That(price, Is.EqualTo(0.0444).Within(1).Percent);

var putPrice = _calculator.BarrierOptions(OptionType.Put, BarrierType.DownIn, spot, strike, rate, yield, maturity, vol, barrier, rebate);
Console.WriteLine($"Price of put barrier option is {putPrice}");
Assert.That(putPrice, Is.EqualTo(2.65).Within(1).Percent);
}
}
91 changes: 45 additions & 46 deletions ProjectX.AnalyticsLib.Tests/OptionsPricingModelTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,52 @@
using ProjectX.Core.Requests;
using ProjectX.Core.Services;

namespace ProjectX.AnalyticsLib.Tests
namespace ProjectX.AnalyticsLib.Tests;

public class OptionsPricingModelTest
{
public class OptionsPricingModelTest
private Mock<IBlackScholesCSharpPricer> _mockCSharpCalculator = new Mock<IBlackScholesCSharpPricer>();
private OptionsPricingModel _sut;
private Random _random = new Random();

[SetUp]
public void SetUp()
{
private Mock<IBlackScholesCSharpPricer> _mockCSharpCalculator = new Mock<IBlackScholesCSharpPricer>();
private OptionsPricingModel _sut;
private Random _random = new Random();

[SetUp]
public void SetUp()
{
_sut = new OptionsPricingModel(_mockCSharpCalculator.Object, Mock.Of<IBlackScholesCppPricer>(), Mock.Of<IMonteCarloCppOptionsPricer>());
_mockCSharpCalculator.Setup(m => m.PV(It.IsAny<OptionType>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
.Returns(() => RandomFloat(50, 60));
}

private double RandomFloat(int min, int max) => _random.Next(min, max) + _random.NextDouble();

[Test]
public async Task WhenPricingOptionsPricingByMaturitiesRequestItShouldReturnValidValues()
{
var request = new OptionsPricingByMaturitiesRequest(10, OptionType.Call, 100.0, 150.0, 1.0, 1.0, 0.3, OptionsPricingCalculatorType.OptionsPricer);

Console.WriteLine($"JSON={JsonConvert.SerializeObject(request)}");
var actual = _sut.Price(request);
Assert.That(actual, Is.Not.Null);
Assert.That(actual.ResultsCount, Is.EqualTo(10));
Assert.That(actual[0].Maturity, Is.EqualTo(0.1));
Assert.That(actual[0].OptionGreeks.price, Is.Not.EqualTo(0));
}

[Test]
public void WhenPlottingGreeksZValuesAreValid()
{
var plotOptionsResult = _sut.PlotGreeks(new PlotOptionsPricingRequest(OptionGreeks.Price, OptionType.Call, 100, 0.1, 0.04, 0.3, OptionsPricingCalculatorType.OptionsPricer));
var result = plotOptionsResult.PlotResults;
AssertZValue(result.zmin, result.zmax, rounding: 1);
}

private static void AssertZValue(double zmin, double zmax, int rounding)
{
var theZmin = Math.Round(zmin, rounding);
var theZmax = Math.Round(zmax, rounding);
var theZTick = Math.Round((zmax - zmin) / 5.0, rounding);

Assert.IsTrue(zmin < zmax);
Assert.IsTrue(theZTick > 0);
}
_sut = new OptionsPricingModel(_mockCSharpCalculator.Object, Mock.Of<IBlackScholesCppPricer>(), Mock.Of<IMonteCarloCppOptionsPricer>());
_mockCSharpCalculator.Setup(m => m.PV(It.IsAny<OptionType>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
.Returns(() => RandomFloat(50, 60));
}

private double RandomFloat(int min, int max) => _random.Next(min, max) + _random.NextDouble();

[Test]
public async Task WhenPricingOptionsPricingByMaturitiesRequestItShouldReturnValidValues()
{
var request = new OptionsPricingByMaturitiesRequest(10, OptionType.Call, 100.0, 150.0, 1.0, 1.0, 0.3, OptionsPricingCalculatorType.OptionsPricer);

Console.WriteLine($"JSON={JsonConvert.SerializeObject(request)}");
var actual = _sut.Price(request);
Assert.That(actual, Is.Not.Null);
Assert.That(actual.ResultsCount, Is.EqualTo(10));
Assert.That(actual[0].Maturity, Is.EqualTo(0.1));
Assert.That(actual[0].OptionGreeks.price, Is.Not.EqualTo(0));
}

[Test]
public void WhenPlottingGreeksZValuesAreValid()
{
var plotOptionsResult = _sut.PlotGreeks(new PlotOptionsPricingRequest(OptionGreeks.Price, OptionType.Call, 100, 0.1, 0.04, 0.3, OptionsPricingCalculatorType.OptionsPricer));
var result = plotOptionsResult.PlotResults;
AssertZValue(result.zmin, result.zmax, rounding: 1);
}

private static void AssertZValue(double zmin, double zmax, int rounding)
{
var theZmin = Math.Round(zmin, rounding);
var theZmax = Math.Round(zmax, rounding);
var theZTick = Math.Round((zmax - zmin) / 5.0, rounding);

Assert.IsTrue(zmin < zmax);
Assert.IsTrue(theZTick > 0);
}
}
95 changes: 95 additions & 0 deletions ProjectX.AnalyticsLib/CreditDefaultSwapFunctions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using QLNet;

namespace ProjectX.AnalyticsLib;

public class CreditDefaultSwapFunctions
{
public static CreditDefaultSwapPVResult PV(
DateTime evaluationDate,
DateTime effectiveDate,
DateTime maturityDate,
int[] spreadsInBps,
string[] tenors,
double recoveryRate,
int couponInBps,
int notional,
Protection.Side protectionSide,
double flatInterestRate)
{
Calendar calendar = new TARGET();
var evalDate = calendar.adjust(evaluationDate.ToQuantLibDate());
var settlementDate = calendar.advance(evalDate, new Period(2, TimeUnit.Days));
var maturity = maturityDate.ToQuantLibDate();
var flatInterestRateQuote = new Handle<Quote>(new SimpleQuote(flatInterestRate));
var curve = new RelinkableHandle<YieldTermStructure>();
var flatCurve = new FlatForward(evalDate, flatInterestRateQuote, new Actual365Fixed());
curve.linkTo(flatCurve);

Settings.setEvaluationDate(evalDate);

// compute hazard rates based on spreads (basis points) and corresponding tenors
List<Date> dates = [evalDate];
List<double> hazardRates = [0.0];
for (int i = 0; i < tenors.Length; i++)
{
var period = tenors[i].ToPeriod();
var spread = spreadsInBps[i] / 10_000.0;
var date = evalDate + period;

// implied hazard rates by building a cds
var cdsValuation = new MakeCreditDefaultSwap(period, spread)
.withNominal(10_000_000.0)
.value();
var hazardRate = cdsValuation.impliedHazardRate(
0.0,
curve,
new Actual365Fixed(),
recoveryRate,
1E-10,
PricingModel.Midpoint
);
dates.Add(date);
hazardRates.Add(hazardRate);
}
hazardRates[0] = hazardRates[1];

// bootstrap hazard rates
RelinkableHandle<DefaultProbabilityTermStructure> piecewiseFlatHazardRate = new RelinkableHandle<DefaultProbabilityTermStructure>();
var hazardRateCurve = new InterpolatedHazardRateCurve<Linear>(dates, hazardRates, new Actual365Fixed());
piecewiseFlatHazardRate.linkTo(hazardRateCurve);
piecewiseFlatHazardRate.link.enableExtrapolation();

// build instrument
var schedule = new Schedule(effectiveDate.ToQuantLibDate(), maturity, new Period(Frequency.Annual), calendar, BusinessDayConvention.Following, BusinessDayConvention.Following, DateGeneration.Rule.Twentieth, false);
CreditDefaultSwap cds = new CreditDefaultSwap(protectionSide, notional, couponInBps / 10_000.0, schedule, BusinessDayConvention.ModifiedFollowing, new Actual365Fixed());
cds.setPricingEngine(new MidPointCdsEngine(piecewiseFlatHazardRate, recoveryRate, curve));
cds.recalculate();

// results
var pv = cds.NPV();
var fairSpread = 10000.0 * cds.fairSpread();
var survivalProbabilityPercentage = 100.0 * piecewiseFlatHazardRate.link.survivalProbability(maturity);
var hazardRatePercentage = 100.0 * piecewiseFlatHazardRate.link.hazardRate(maturity);
var defaultProbabilityPercentage = 100.0 * piecewiseFlatHazardRate.link.defaultProbability(maturity);

return (pv, fairSpread, survivalProbabilityPercentage, hazardRatePercentage, defaultProbabilityPercentage);
}
}

public record struct CreditDefaultSwapPVResult(double PV, double FairSpread, double SurvivalProbabilityPercentage, double HazardRatePercentage, double DefaultProbabilityPercentage)
{
public static implicit operator (double pv, double fairSpread, double survivalProbabilityPercentage, double hazardRatePercenatge, double defaultProbabilityPercentage)(CreditDefaultSwapPVResult value)
{
return (value.PV, value.FairSpread, value.SurvivalProbabilityPercentage, value.HazardRatePercentage, value.DefaultProbabilityPercentage);
}

public static implicit operator CreditDefaultSwapPVResult((double pv, double fairSpread, double survivalProbabilityPercentage, double hazardRatePercenatge, double defaultProbabilityPercentage) value)
{
return new CreditDefaultSwapPVResult(value.pv, value.fairSpread, value.survivalProbabilityPercentage, value.hazardRatePercenatge, value.defaultProbabilityPercentage);
}
}
22 changes: 22 additions & 0 deletions ProjectX.AnalyticsLib/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using QLNet;

namespace ProjectX.AnalyticsLib;

public static class Extensions
{
public static QLNet.Date ToQuantLibDate(this DateTime dt) => new QLNet.Date((int)dt.ToOADate());
public static DateTime ToDatetime(this QLNet.Date date) => Convert.ToDateTime(date.month() + " " + date.Day.ToString() + ", " + date.year().ToString());
public static Period[] ToPeriods(this string[] tenors) => tenors.Select(t => t.ToPeriod()).ToArray();
public static Period ToPeriod(this string tenor) => new(int.Parse(tenor[..^1]), ToTimeUnits(tenor[^1]));
private static TimeUnit ToTimeUnits(char timeUnit)
{
var u = Char.ToLower(timeUnit);
switch (u)
{
case 'm': return TimeUnit.Months;
case 'y': return TimeUnit.Years;
default:
throw new NotSupportedException($"{timeUnit} conversion not supported");
}
}
}
4 changes: 4 additions & 0 deletions ProjectX.AnalyticsLib/ProjectX.AnalyticsLib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="QLNet" Version="1.13.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ProjectX.AnalyticsLib.Shared\ProjectX.AnalyticsLib.Shared.csproj" />
<ProjectReference Include="..\ProjectX.Core\ProjectX.Core.csproj" />
Expand Down

0 comments on commit 0d9f8f0

Please sign in to comment.