Skip to content

Commit

Permalink
Better support for SOFR futures
Browse files Browse the repository at this point in the history
  • Loading branch information
gavbrennan committed Apr 19, 2023
1 parent 3a88bf8 commit 981bc6b
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 4 deletions.
53 changes: 53 additions & 0 deletions src/Qwack.Core/Basic/SofrAverageCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Qwack.Core.Curves;

namespace Qwack.Core.Basic
{
public static class SofrAverageCalculator
{
//https://www.cmegroup.com/content/dam/cmegroup/rulebook/CME/IV/400/460.pdf
public static double ComputeAverage(List<DateTime> dates, Dictionary<DateTime, double> fixings, IIrCurve forecastCurve, DateTime? valDate = null)
{
var vd = valDate ?? forecastCurve.BuildDate;
var sortedDates = dates.OrderBy(x => x).ToList();
var daysInPeriod = (sortedDates.Last() - sortedDates.First()).TotalDays;
var sum = 1.0;

for(var i=0;i<sortedDates.Count; i++)
{
var date = sortedDates[i];
var rate = 0.0;

if (date < vd)
{
if (!fixings.TryGetValue(date, out rate))
{
var recentDate = sortedDates.Where(x=>x<date).Max();
rate = fixings[recentDate];
}
}
else if (date == vd)
{
if (!fixings.TryGetValue(date, out rate))
{
rate = forecastCurve.GetRate(date);
}
}
else
rate = forecastCurve.GetRate(date);

var d = i == sortedDates.Count - 1 ? 1 : (sortedDates[i + 1] - date).TotalDays;

sum *= 1 + d / 360.0 * rate;
}

sum -= 1.0;
sum *= 360.0 / daysInPeriod;

return sum;
}
}
}
2 changes: 2 additions & 0 deletions src/Qwack.Core/Curves/IrCurve.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public IrCurve(TO_IrCurve transportObject, ICurrencyProvider currencyProvider)
public string CollateralSpec { get; private set; }
public FloatRateIndex RateIndex { get; private set; }

public Dictionary<DateTime, double> Fixings { get; set; }

public void SetCollateralSpec(string collateralSpec) => CollateralSpec = collateralSpec;

public void SetRateIndex(FloatRateIndex rateIndex) => RateIndex = rateIndex;
Expand Down
27 changes: 26 additions & 1 deletion src/Qwack.Core/Instruments/Funding/OISFuture.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Qwack.Core.Basic;
using Qwack.Core.Curves;
using Qwack.Core.Models;
using Qwack.Dates;
using Qwack.Transport.BasicTypes;
Expand Down Expand Up @@ -39,7 +41,30 @@ public double Pv(IFundingModel Model, bool updateState)
public double CalculateParRate(IFundingModel Model)
{
var forecastCurve = Model.Curves[ForecastCurve];
var fwdRate = forecastCurve.GetForwardRate(AverageStartDate, AverageEndDate, RateType.Linear, Index.DayCountBasis);
var cal = Index?.HolidayCalendars ?? Currency.SettlementCalendar;
double fwdRate = 0;
if (Model?.BuildDate > AverageStartDate && forecastCurve is IrCurve fc && fc.Fixings!=null)
{
var avgDates = AverageStartDate.BusinessDaysInPeriod(AverageEndDate, cal);
var rates = avgDates.Select(d => fc.Fixings.TryGetValue(d, out var x) ? x : 0).ToArray();
var index = 1.0;
for(var i=0;i< avgDates.Count-1; i++)
{
if (rates[i] == 0 && avgDates[i]>= Model.BuildDate)
{
rates[i] = forecastCurve.GetForwardRate(avgDates[i], avgDates[i+1], RateType.Linear, Index.DayCountBasis);
}
else if (i>0 && rates[i] == 0 && rates[i-1]!=0)
rates[i] = rates[i-1];

var d = avgDates[i + 1].Subtract(avgDates[i]).TotalDays;
index *= 1 + rates[i] * d / 360.0;
}
var bigD = AverageEndDate.Subtract(AverageStartDate).TotalDays;
fwdRate = (index - 1) * 360.0 / bigD;
}
else
fwdRate = forecastCurve.GetForwardRate(AverageStartDate, AverageEndDate, RateType.Linear, Index.DayCountBasis);

var fairPrice = 100.0 - fwdRate * 100.0;
return fairPrice;
Expand Down
26 changes: 24 additions & 2 deletions src/Qwack.Models/Calibrators/CMEModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@ public static IrCurve GetCurveForCode(string cmeId, StreamReader stream, string
return GetCurveForCode(parsed, qwackCode, curveName, indices, curves, futureSettingsProvider, currencyProvider, calendarProvider);
}

public static IrCurve GetCurveForCode(IEnumerable<CMEFileRecord> parsed, string qwackCode, string curveName, Dictionary<string, FloatRateIndex> indices, Dictionary<string, string> curves, IFutureSettingsProvider futureSettingsProvider, ICurrencyProvider currencyProvider, ICalendarProvider calendarProvider)
public static IrCurve GetCurveForCode(IEnumerable<CMEFileRecord> parsed, string qwackCode, string curveName, Dictionary<string, FloatRateIndex> indices, Dictionary<string, string> curves, IFutureSettingsProvider futureSettingsProvider, ICurrencyProvider currencyProvider, ICalendarProvider calendarProvider, Dictionary<DateTime, double> fixings = null)
{
var q = parsed.ToDictionary(x => DateTime.ParseExact(x.MatDt, "yyyy-MM-dd", CultureInfo.InvariantCulture), x => x.SettlePrice);
var origin = DateTime.ParseExact(parsed.First().BizDt, "yyyy-MM-dd", CultureInfo.InvariantCulture);
var instruments = parsed.Select(p => ToQwackIns(p, qwackCode, futureSettingsProvider, currencyProvider, indices, curves)).ToList();
var pillars = instruments.Select(x => x.PillarDate).OrderBy(x => x).ToArray();
var fic = new FundingInstrumentCollection(currencyProvider);
fic.AddRange(instruments);
var curve = new IrCurve(pillars, pillars.Select(p => 0.01).ToArray(), origin, curveName, Interpolator1DType.Linear, currencyProvider.GetCurrency("USD"));
var curve = new IrCurve(pillars, pillars.Select(p => 0.01).ToArray(), origin, curveName, Interpolator1DType.Linear, currencyProvider.GetCurrency("USD"))
{
Fixings = fixings,
};

var fm = new FundingModel(origin, new[] { curve }, currencyProvider, calendarProvider);

var solver = new NewtonRaphsonMultiCurveSolverStaged();
Expand Down Expand Up @@ -240,6 +244,24 @@ private static IFundingInstrument ToQwackIns(this CMEFileRecord record, string q
AverageStartDate = ffStart,
AverageEndDate = ffEnd,
};
case "SR3":
var srEnd = FutureCode.GetExpiryFromCode(MmmYtoCode(record.MMY, qwackCode), futureSettingsProvider);
var srStart = srEnd.AddMonths(-3).ThirdWednesday();
return new OISFuture
{
ContractSize = 1e6,
Currency = currencyProvider.GetCurrency("USD"),
DCF = srStart.CalculateYearFraction(srEnd, DayCountBasis.ACT360),
Price = record.SettlePrice.Value,
Index = indices["SR3"],
PillarDate = srEnd,
TradeId = qwackCode + record.MMY,
Position = 1,
SolveCurve = forecastCurves["SR3"],
ForecastCurve = forecastCurves["SR3"],
AverageStartDate = srStart,
AverageEndDate = srEnd,
};
default:
throw new Exception($"No mapping found for code {qwackCode}");
}
Expand Down
60 changes: 60 additions & 0 deletions test/Qwack.Core.Tests/Instruments/OISFutureFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,65 @@ public void OISFuture()
Assert.Equal(97, s2.Price);
}


[Fact]
public void OISFuturePastFixings()
{
var bd = new DateTime(2023, 04, 19);
var pillars = new[] { bd, bd.AddDays(1000) };
var flatRate = 0.05;
var rates = pillars.Select(p => flatRate).ToArray();
CalendarProvider.Collection.TryGetCalendar("LON", out var cal);
var usd = TestProviderHelper.CurrencyProvider["USD"];

var curve = new IrCurve(pillars, rates, bd, "USD.BLAH", Interpolator1DType.Linear, usd);

var ix = new FloatRateIndex
{
Currency = usd,
DayCountBasis = DayCountBasis.ACT360,
FixingOffset = 2.Bd(),
HolidayCalendars = cal,
ResetTenor = 3.Months(),
RollConvention = RollType.MF
};

var maturity = bd.AddDays(-1);
var accrualStart = maturity.SubtractPeriod(RollType.F, ix.HolidayCalendars, 3.Months());
var accrualEnd = maturity;
var dcf = accrualStart.CalculateYearFraction(accrualEnd, DayCountBasis.ACT360);
var fModel = new FundingModel(bd, new[] { curve }, TestProviderHelper.CurrencyProvider, TestProviderHelper.CalendarProvider);

var sut = new OISFuture()
{
Currency = usd,
ContractSize = 1e6,
Position = 1,
DCF = dcf,
AverageStartDate = accrualStart,
AverageEndDate = accrualEnd,
ForecastCurve = "USD.BLAH",
Price = 100,
Index = ix
};

var fixRate = 0.025;
var bizDates = accrualStart.BusinessDaysInPeriod(accrualEnd, cal);
var fixings = bizDates.ToDictionary(x => x, x => fixRate);
curve.Fixings = fixings;

var par = sut.CalculateParRate(fModel);
var nDays = (accrualEnd - accrualStart).TotalDays;
var index = 1.0;
for (var i = 0; i < bizDates.Count -1; i++)
{
var d = (bizDates[i+1] - bizDates[i]).TotalDays;
index *= (1 + fixRate * d / 360);
}

var expectedPar = 100.0 - (index - 1) * 360 / nDays * 100.0;
Assert.Equal(expectedPar, par, 8);
}

}
}
2 changes: 1 addition & 1 deletion test/Qwack.Core.Tests/TestProviderHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public static class TestProviderHelper
public static readonly string JsonFuturesPath = System.IO.Path.Combine(AppContext.BaseDirectory, "futuresettings.json");
public static readonly string JsonCurrencyPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Currencies.json");
public static readonly ILoggerFactory LoggerFactory = new LoggerFactory();
public static readonly ICurrencyProvider CurrencyProvider = new CurrenciesFromJson(CalendarProvider, JsonCurrencyPath, LoggerFactory.CreateLogger<CurrenciesFromJson>());
public static readonly ICalendarProvider CalendarProvider = CalendarsFromJson.Load(JsonCalendarPath);
public static readonly ICurrencyProvider CurrencyProvider = new CurrenciesFromJson(CalendarProvider, JsonCurrencyPath, LoggerFactory.CreateLogger<CurrenciesFromJson>());
public static readonly IFutureSettingsProvider FutureSettingsProvider = new FutureSettingsFromJson(CalendarProvider, JsonFuturesPath, LoggerFactory);
}
}

0 comments on commit 981bc6b

Please sign in to comment.