Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions OREData/ored/marketdata/yieldcurve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1306,15 +1306,11 @@ void YieldCurve::buildZeroSpreadedCurve(const std::size_t index) {

QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();

// Fill a vector of zero spread quotes.
vector<QuantLib::ext::shared_ptr<ZeroQuote>> quotes;
QuantLib::ext::shared_ptr<ZeroSpreadedYieldCurveSegment> segment =
QuantLib::ext::dynamic_pointer_cast<ZeroSpreadedYieldCurveSegment>(curveSegments_[index][0]);
auto quoteIDs = segment->quotes();

Date today = Settings::instance().evaluationDate();
vector<Date> dates;
vector<Handle<Quote>> quoteHandles;

vector<string> quotesVector;
quotesVector.reserve(quoteIDs.size());
Expand All @@ -1331,6 +1327,9 @@ void YieldCurve::buildZeroSpreadedCurve(const std::size_t index) {
}
}

vector<pair<Date, Handle<Quote>>> nodes;
nodes.reserve(marketData.size());

// process market data
for (const auto& marketQuote : marketData) {
QL_REQUIRE(marketQuote->instrumentType() == MarketDatum::InstrumentType::ZERO,
Expand All @@ -1345,14 +1344,24 @@ void YieldCurve::buildZeroSpreadedCurve(const std::size_t index) {
if (it == quoteIDs.end())
continue;
}
quotes.push_back(zeroQuote);
dates.push_back(zeroQuote->tenorBased() ? today + zeroQuote->tenor() : zeroQuote->date());
quoteHandles.push_back(zeroQuote->quote());
nodes.emplace_back(zeroQuote->tenorBased() ? today + zeroQuote->tenor() : zeroQuote->date(),
zeroQuote->quote());
}

QL_REQUIRE(!quotes.empty(),
QL_REQUIRE(!nodes.empty(),
"Cannot build curve with spec " << curveSpec_[index]->name() << " because there are no spread quotes");

std::sort(nodes.begin(), nodes.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });

vector<Date> dates;
vector<Handle<Quote>> quoteHandles;
dates.reserve(nodes.size());
quoteHandles.reserve(nodes.size());
for (auto& [date, quote] : nodes) {
dates.emplace_back(date);
quoteHandles.emplace_back(std::move(quote));
}

string referenceCurveID = segment->referenceCurveID();
QuantLib::Handle<YieldTermStructure> referenceCurve;
if (referenceCurveID != curveConfig_[index]->curveID() && !referenceCurveID.empty()) {
Expand Down
74 changes: 60 additions & 14 deletions OREData/test/todaysmarket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ namespace {

class MarketDataLoader : public Loader {
public:
MarketDataLoader();
explicit MarketDataLoader(bool cloneZeroSpreadQuotes = false);
std::vector<QuantLib::ext::shared_ptr<MarketDatum>> loadQuotes(const QuantLib::Date&) const override;
QuantLib::ext::shared_ptr<MarketDatum> get(const std::string& name, const QuantLib::Date& d) const override;
std::set<QuantLib::Date> asofDates() const override { return {}; }
std::set<Fixing> loadFixings() const override { return fixings_; }
std::set<QuantExt::Dividend> loadDividends() const override { return dividends_; }
Expand All @@ -63,6 +64,7 @@ class MarketDataLoader : public Loader {
std::map<QuantLib::Date, std::vector<QuantLib::ext::shared_ptr<MarketDatum>>> data_;
std::set<Fixing> fixings_;
std::set<QuantExt::Dividend> dividends_;
bool cloneZeroSpreadQuotes_;
};

vector<QuantLib::ext::shared_ptr<MarketDatum>> MarketDataLoader::loadQuotes(const Date& d) const {
Expand All @@ -71,7 +73,15 @@ vector<QuantLib::ext::shared_ptr<MarketDatum>> MarketDataLoader::loadQuotes(cons
return it->second;
}

MarketDataLoader::MarketDataLoader() {
QuantLib::ext::shared_ptr<MarketDatum> MarketDataLoader::get(const std::string& name, const Date& d) const {
auto marketDatum = Loader::get(name, d);
// Clone Zero quotes so allocation follows the deliberately non-chronological lookup order.
if (cloneZeroSpreadQuotes_ && marketDatum->instrumentType() == MarketDatum::InstrumentType::ZERO)
return marketDatum->clone();
return marketDatum;
}

MarketDataLoader::MarketDataLoader(bool cloneZeroSpreadQuotes) : cloneZeroSpreadQuotes_(cloneZeroSpreadQuotes) {
// clang-format off
vector<string> data = boost::assign::list_of
// borrow spread curve
Expand All @@ -80,10 +90,10 @@ MarketDataLoader::MarketDataLoader() {
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_BORROW/A365/10Y -0.0010")
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_BORROW/A365/20Y -0.0010")
// lending spread curve
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y 0.0050")
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y 0.0020")
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/5Y 0.0050")
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y 0.0050")
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y 0.0050")
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y 0.0100")
("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y 0.0200")
// Eonia curve
("20160226 MM/RATE/EUR/0D/1D -0.0025")
("20160226 IR_SWAP/RATE/EUR/0D/1D/1D -0.0025")
Expand Down Expand Up @@ -567,10 +577,10 @@ QuantLib::ext::shared_ptr<CurveConfigurations> curveConfigurations() {
// Lending curve
segments.clear();
// clang-format off
quotes = {"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y",
"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/5Y",
"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y",
"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y"};
quotes = {"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/5Y",
"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y",
"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y",
"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y"};
// clang-format on
segments.push_back(QuantLib::ext::make_shared<ZeroSpreadedYieldCurveSegment>(
"Zero Spread", "EUR-ZERO-CONVENTIONS-TENOR-BASED", quotes, "EUR1D"));
Expand Down Expand Up @@ -777,11 +787,11 @@ class F : public TopLevelFixture {
public:
QuantLib::ext::shared_ptr<TodaysMarket> market;

F() {
explicit F(bool cloneZeroSpreadQuotes = false) {
Date asof(26, February, 2016);
Settings::instance().evaluationDate() = asof;

auto loader = QuantLib::ext::make_shared<MarketDataLoader>();
auto loader = QuantLib::ext::make_shared<MarketDataLoader>(cloneZeroSpreadQuotes);
auto params = marketParameters();
auto configs = curveConfigurations();
auto convs = conventions();
Expand All @@ -798,6 +808,29 @@ class F : public TopLevelFixture {
}
};

class FWithClonedZeroSpreadQuotes : public F {
public:
FWithClonedZeroSpreadQuotes() : F(true) {}
};

const vector<pair<Period, Real>> lendingCurveSpreads = {
{2 * Years, 0.002}, {5 * Years, 0.005}, {10 * Years, 0.010}, {20 * Years, 0.020}};

void checkLendingCurveSpreads(const Handle<YieldTermStructure>& referenceCurve,
const Handle<YieldTermStructure>& spreadedCurve) {
const Date today = Settings::instance().evaluationDate();
const DayCounter dc = Actual365Fixed();
constexpr Real tolerance = 1.0e-5;

for (const auto& [tenor, expectedSpread] : lendingCurveSpreads) {
const Date d = today + tenor;
const Real referenceZero = referenceCurve->zeroRate(d, dc, Continuous);
const Real spreadedZero = spreadedCurve->zeroRate(d, dc, Continuous);
BOOST_CHECK_MESSAGE(fabs(spreadedZero - referenceZero - expectedSpread) < tolerance,
"zero spread at " << io::iso_date(d) << " is not paired with its resolved maturity");
}
}

} // namespace

BOOST_FIXTURE_TEST_SUITE(OREDataTestSuite, TopLevelFixture)
Expand All @@ -819,16 +852,15 @@ BOOST_AUTO_TEST_CASE(testZeroSpreadedYieldCurve) {
Date today = Settings::instance().evaluationDate();
DayCounter dc = Actual365Fixed();
Real tolerance = 1.0e-5; // 0.1 bp
Real expected1 = 0.005;
Real expected2 = -0.001;
for (Size i = 1; i <= 120; i++) {
Date d = today + i * Months;
Real z0 = dts->zeroRate(d, dc, Continuous);
Real z1 = dtsLend->zeroRate(d, dc, Continuous);
Real z2 = dtsBorrow->zeroRate(d, dc, Continuous);
BOOST_CHECK_MESSAGE(fabs(z1 - z0 - expected1) < tolerance, "error in lending spread curve setup");
BOOST_CHECK_MESSAGE(fabs(z2 - z0 - expected2) < tolerance, "error in borrowing spread curve setup");
}

checkLendingCurveSpreads(dts, dtsLend);
}

BOOST_AUTO_TEST_CASE(testNormalOptionletVolatility) {
Expand Down Expand Up @@ -1041,4 +1073,18 @@ BOOST_AUTO_TEST_CASE(testCorrelationCurve) {

BOOST_AUTO_TEST_SUITE_END()

BOOST_FIXTURE_TEST_SUITE(ZeroSpreadRegressionTests, FWithClonedZeroSpreadQuotes)

BOOST_AUTO_TEST_CASE(testZeroSpreadedYieldCurveOrdersNodesByResolvedMaturity) {
Handle<YieldTermStructure> referenceCurve = market->discountCurve("EUR");
Handle<YieldTermStructure> spreadedCurve = market->yieldCurve("EUR_LEND");

QL_REQUIRE(!referenceCurve.empty(), "EUR discount curve not found");
QL_REQUIRE(!spreadedCurve.empty(), "EUR lending curve not found");

checkLendingCurveSpreads(referenceCurve, spreadedCurve);
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()