Closed bensonluk closed 1 month ago
Hi @bensonluk could you please post a quick code that reproduces the user case?
In case anyone else is interested, the chapter and verse from the CME rulebook is:
46103.A. Final Settlement Price
The final settlement price for an expiring contract shall be calculated by the Exchange on the day on which the FRBNY publishes the SOFR value for the last day of such contract’s delivery month.
The SOFR value for the last day of such delivery month shall be as first published by the FRBNY.
The final settlement price shall be 100 minus the arithmetic average of daily SOFR values during the contract delivery month. For any day during such delivery month for which the FRBNY does not publish a SOFR value (eg, a weekend day, a US government securities market holiday), the SOFR value assigned to such day shall be the SOFR value for the last preceding day for which a SOFR value was published.
@jakeheke75 Below is an example for illustration: assume all relevant historical fixings are zero, and a Jan 2023 futures price at 99 (implying average rate = 1% over Jan). The stripping result is 14.5%, indicating that the denominator used is 29 days only (because 1st and 2nd Jan are holidays) instead of 31 days.
Changing the 30 Dec fixing does not affect the result, whilst in reality it should (by affecting the 1 and 2 Jan values).
As a sanity check, changing fixings in Jan (e.g. 5 Jan) does affect the stripping result.
#include <iostream>
#include <ql/termstructures/yield/overnightindexfutureratehelper.hpp>
#include <ql/termstructures/yield/piecewiseyieldcurve.hpp>
#include <ql/indexes/ibor/sofr.hpp>
using namespace std;
using namespace QuantLib;
int main()
{
Date todaysDate(30, Jan, 2023);
Settings::instance().evaluationDate() = todaysDate;
ext::shared_ptr<OvernightIndex> index = ext::make_shared<Sofr>();
index->addFixing(Date(30, Dec, 2022),0.0); // same result when this fixing is changed
index->addFixing(Date(3, Jan, 2023), 0.0);
index->addFixing(Date(4, Jan, 2023), 0.0);
index->addFixing(Date(5, Jan, 2023), 0.0); // change this to 1% and the 30th fixing becomes 14%
index->addFixing(Date(6, Jan, 2023), 0.0);
index->addFixing(Date(9, Jan, 2023), 0.0);
index->addFixing(Date(10, Jan, 2023), 0.0);
index->addFixing(Date(11, Jan, 2023), 0.0);
index->addFixing(Date(12, Jan, 2023), 0.0);
index->addFixing(Date(13, Jan, 2023), 0.0);
index->addFixing(Date(17, Jan, 2023), 0.0);
index->addFixing(Date(18, Jan, 2023), 0.0);
index->addFixing(Date(19, Jan, 2023), 0.0);
index->addFixing(Date(20, Jan, 2023), 0.0);
index->addFixing(Date(23, Jan, 2023), 0.0);
index->addFixing(Date(24, Jan, 2023), 0.0);
index->addFixing(Date(25, Jan, 2023), 0.0);
index->addFixing(Date(26, Jan, 2023), 0.0);
index->addFixing(Date(27, Jan, 2023), 0.0);
float convexityAdj = 0.0;
vector<ext::shared_ptr<RateHelper> > helpers;
helpers.push_back(ext::make_shared<SofrFutureRateHelper>(
99.0, Jan, 2023, Monthly, 0));
helpers.push_back(ext::make_shared<SofrFutureRateHelper>(
102, Feb, 2023, Monthly, 0));
ext::shared_ptr<PiecewiseYieldCurve<ForwardRate, BackwardFlat> > curve =
ext::make_shared<PiecewiseYieldCurve<ForwardRate, BackwardFlat> >(todaysDate, helpers,
Actual365Fixed());
ext::shared_ptr<OvernightIndex> sofr =
ext::make_shared<Sofr>(Handle<YieldTermStructure>(curve));
vector<Date> dates = { Date(27, Jan, 2023), Date(30, Jan, 2023), Date(31, Jan, 2023),
Date(1, Feb, 2023) ,Date(2, Feb, 2023) };
for (auto x : dates) {
cout << "Fixing on " << x << ": " << sofr->fixing(x)*100 << "(%)" << endl;
}
}
Output:
Fixing on January 27th, 2023: 0(%)
Fixing on January 30th, 2023: 14.5(%)
Fixing on January 31st, 2023: 14.5(%)
Fixing on February 1st, 2023: -2.00006(%)
Fixing on February 2nd, 2023: -2.00006(%)
On a second thought it may be better to change the OvernightIndexFutureRateHelper
by adding two arguments to let the helper know how many days the first and last fixings should weigh.
// QuantLib/ql/termstructures/yield/overnightindexfutureratehelper.hpp
OvernightIndexFutureRateHelper(const Handle<Quote>& price,
// first day of reference period
const Date& valueDate, // e.g. 30 Dec 2022
// delivery date
const Date& maturityDate, // e.g. 1 Feb 2023
const ext::shared_ptr<OvernightIndex>& overnightIndex,
const Handle<Quote>& convexityAdjustment = {},
RateAveraging::Type averagingMethod = RateAveraging::Compound,
const Date& firstSettleDate = Date(), // e.g. 1 Jan 2023 <- add this argument
const Date& lastSettleDate = Date() // e.g. 1 Feb 2023 <- add this argument
);
Similarly, we need to add these two arguments to OvernightIndexFuture
constructor. Where unspecified, firstSettleDate
and lastSettleDate
defaults to valueDate
and maturityDate
respectively. In this way the behaviour is unchanged when these two arguments are not given.
Then in OvernightIndexFuture::averagedRate()
, account for special cases in the first and last fixings.
Finally, SofrFutureRateHelper
will refine the way it the dates: valueDate
should be the first business day of the month adjusted backward; provide the two new arguments firstSettleDate
and lastSettleDate
to be the first calendar days of the current and following month respectively.
Any thoughts?
Here's the CME example for settlement calculations on both 3M and 1M SOFR futures (link below). This should help make a set of tests for both types of contracts. Start and end dates for 1M contract are simply first and last day of month, while the 3M contract SR3M23 has start date 21-JUN-2023 (the M23 IMM date) and end date 19-SEP-2023 (the U23 IMM date minus one day). Example Settlement Calcs PDF
Fixed by https://github.com/lballabio/QuantLib/pull/2013 in version 1.35.
This concerns the fixing period of
SofrFutureRateHelper
with monthly frequency.Take Jan 2023, we currently have
getValidSofrStart
returns 3 Jan (first business day of the month), whilegetValidSofrEnd
gives 1 Feb. The helper would create a future referencing this period.That appears inconsistent with the contract specification, which states we should take average over 1 Jan - 31 Jan fixing. The helper should instead start by looking for 30 Dec 2022 fixing, which should weigh for two days (1 and 2 Jan 2023). On the last business day of the month, similar logic applies. (assume 31 Jan and 1 Feb were holidays, it should look at the forward rate from 30 Jan to 2 Feb, but this rate should weigh only for 2 days - 30 and 31 Jan)
I believe we can NOT adjust for holiday when creating the rate helpers (i.e. we always set
valueDate
andmaturityDate
to be the first and last calendar days of the month - even if it is a holiday). Then, inOvernightIndexFuture::averagedRate()
, we add the implementation of the logic above regarding holidays around the first and last fixings.