406 {
407
408 BOOST_TEST_MESSAGE("Checking sensitivity analysis results vs analytic sensi engine results...");
409
410 SavedSettings backup;
411
413 ObservationMode::instance().setMode(ObservationMode::Mode::None);
414
415 Date today = Date(14, April, 2016);
416 Settings::instance().evaluationDate() = today;
417
418 BOOST_TEST_MESSAGE("Today is " << today);
419
420
421 QuantLib::ext::shared_ptr<Market> initMarket = QuantLib::ext::make_shared<testsuite::TestMarket>(today);
422
423
424 QuantLib::ext::shared_ptr<analytics::ScenarioSimMarketParameters> simMarketData =
setupSimMarketData5();
425
426
428
429
430 InstrumentConventions::instance().setConventions(conv());
431 QuantLib::ext::shared_ptr<analytics::ScenarioSimMarket> simMarket =
432 QuantLib::ext::make_shared<analytics::ScenarioSimMarket>(initMarket, simMarketData);
433
434
435 QuantLib::ext::shared_ptr<Scenario> baseScenario = simMarket->baseScenario();
436 QuantLib::ext::shared_ptr<ScenarioFactory> scenarioFactory = QuantLib::ext::make_shared<DeltaScenarioFactory>(baseScenario);
437
438
439 QuantLib::ext::shared_ptr<SensitivityScenarioGenerator> scenarioGenerator =
440 QuantLib::ext::make_shared<SensitivityScenarioGenerator>(sensiData, baseScenario, simMarketData, simMarket,
441 scenarioFactory, false);
442 simMarket->scenarioGenerator() = scenarioGenerator;
443
444
445 QuantLib::ext::shared_ptr<EngineData>
data = QuantLib::ext::make_shared<EngineData>();
446 data->model(
"Swap") =
"DiscountedCashflows";
447 data->engine(
"Swap") =
"DiscountingSwapEngine";
448 data->model(
"CrossCurrencySwap") =
"DiscountedCashflows";
449 data->engine(
"CrossCurrencySwap") =
"DiscountingCrossCurrencySwapEngine";
450 data->model(
"FxOption") =
"GarmanKohlhagen";
451 data->engine(
"FxOption") =
"AnalyticEuropeanEngine";
452 QuantLib::ext::shared_ptr<EngineFactory> factory = QuantLib::ext::make_shared<EngineFactory>(data, simMarket);
453
454
455 QuantLib::ext::shared_ptr<Portfolio> portfolio(
new Portfolio());
456 portfolio->add(
testsuite::buildSwap(
"1_Swap_EUR",
"EUR",
true, 10.0, 0, 10, 0.03, 0.00,
"1Y",
"30/360",
457 "6M", "A360", "EUR-EURIBOR-6M"));
459 portfolio->build(factory);
460
461 BOOST_TEST_MESSAGE("Portfolio size after build: " << portfolio->size());
462
463
464
465 std::map<string, Real> analyticalResultsDelta, analyticalResultsGamma, analyticalResultsCrossGamma;
466 std::vector<Real> bucketTimes, bucketTimesVegaOpt, bucketTimesVegaUnd, bucketTimesFxOpt;
467 std::vector<string> bucketStr, numStr, bucketStrVega, numStrVega, bucketStrFxVega, numStrFxVega;
468 ActualActual dc(ActualActual::ISDA);
469 Size i = 0;
470 for (auto const& p : sensiData->discountCurveShiftData()["EUR"]->shiftTenors) {
471 bucketTimes.push_back(dc.yearFraction(today, today + p));
472 ostringstream bs;
473 bs << p;
474 bucketStr.push_back(bs.str());
475 ostringstream num;
476 num << i++;
477 numStr.push_back(num.str());
478 }
479 for (auto const& p : sensiData->swaptionVolShiftData()["EUR"].shiftExpiries) {
480 bucketTimesVegaOpt.push_back(dc.yearFraction(today, today + p));
481 }
482 for (auto const& p : sensiData->swaptionVolShiftData()["EUR"].shiftTerms) {
483 bucketTimesVegaUnd.push_back(dc.yearFraction(today, today + p));
484 }
485 i = 0;
486 for (auto const& p1 : sensiData->swaptionVolShiftData()["EUR"].shiftExpiries) {
487 for (auto const& p2 : sensiData->swaptionVolShiftData()["EUR"].shiftTerms) {
488 ostringstream bs, num;
489 bs << p1 << "/" << p2;
490 num << i++;
491 bucketStrVega.push_back(bs.str());
492 numStrVega.push_back(num.str());
493 }
494 }
495 i = 0;
496 for (auto const& p : sensiData->fxVolShiftData()["EURUSD"].shiftExpiries) {
497 bucketTimesFxOpt.push_back(dc.yearFraction(today, today + p));
498 ostringstream bs, num;
499 bs << p;
500 num << i++;
501 bucketStrFxVega.push_back(bs.str());
502 numStrFxVega.push_back(num.str());
503 }
504
505 Size n = bucketTimes.size();
506 auto analyticSwapEngine = QuantLib::ext::make_shared<DiscountingSwapEngineDeltaGamma>(simMarket->discountCurve("EUR"),
507 bucketTimes, true, true, true, false);
508 auto swap0 = portfolio->trades().begin()->second->instrument()->qlInstrument();
509 swap0->setPricingEngine(analyticSwapEngine);
510 auto deltaDsc0 = swap0->result<std::vector<Real>>("deltaDiscount");
511 auto deltaFwd0 = swap0->result<std::vector<Real>>("deltaForward");
512 auto gamma0 = swap0->result<Matrix>("gamma");
513
514 auto process =
515 QuantLib::ext::make_shared<GarmanKohlagenProcess>(simMarket->fxRate("EURUSD"), simMarket->discountCurve("EUR"),
516 simMarket->discountCurve("USD"), simMarket->fxVol("EURUSD"));
517 auto analyticFxEngine =
518 QuantLib::ext::make_shared<AnalyticEuropeanEngineDeltaGamma>(process, bucketTimes, bucketTimesFxOpt, true, true, false);
519 auto fxoption2 = (++portfolio->trades().begin())->second->instrument()->qlInstrument();
520 fxoption2->setPricingEngine(analyticFxEngine);
521 Real deltaSpot2 = fxoption2->result<Real>("deltaSpot");
522 Real gammaSpot2 = fxoption2->result<Real>("gammaSpot");
523 auto vega2 = fxoption2->result<std::vector<Real>>("vega");
524 auto deltaRate2 = fxoption2->result<std::vector<Real>>("deltaRate");
525 auto deltaDividend2 = fxoption2->result<std::vector<Real>>("deltaDividend");
526 auto gamma2 = fxoption2->result<Matrix>("gamma");
527 Real fxnpv = fxoption2->NPV();
528 Real fxspot = simMarket->fxRate("EURUSD")->value();
529
530 std::vector<string> dscKey, dscKey2, fwdKey;
531 for (Size i = 0; i < n; ++i) {
532 dscKey.push_back("DiscountCurve/EUR/" + numStr[i] + "/" + bucketStr[i]);
533 dscKey2.push_back("DiscountCurve/USD/" + numStr[i] + "/" + bucketStr[i]);
534 fwdKey.push_back("IndexCurve/EUR-EURIBOR-6M/" + numStr[i] + "/" + bucketStr[i]);
535 }
536
537 for (Size i = 0; i < n; ++i) {
538 analyticalResultsDelta["1_Swap_EUR " + dscKey[i]] = deltaDsc0[i];
539 analyticalResultsDelta["1_Swap_EUR " + fwdKey[i]] = deltaFwd0[i];
540 analyticalResultsDelta["7_FxOption_EUR_USD " + dscKey[i]] = deltaDividend2[i] * 10.0 / fxspot;
541 analyticalResultsDelta["7_FxOption_EUR_USD " + dscKey2[i]] = deltaRate2[i] * 10.0 / fxspot;
542 }
543
544 for (Size i = 0; i < n; ++i) {
545 analyticalResultsGamma["1_Swap_EUR " + dscKey[i]] = gamma0[i][i];
546 analyticalResultsGamma["1_Swap_EUR " + fwdKey[i]] = gamma0[n + i][n + i];
547 analyticalResultsGamma["7_FxOption_EUR_USD " + dscKey[i]] =
548 gamma2[n + i][n + i] * 10.0 / fxspot;
549 analyticalResultsGamma["7_FxOption_EUR_USD " + dscKey2[i]] = gamma2[i][i] * 10.0 / fxspot;
550 for (Size j = 0; j < n; ++j) {
551 if (i < j) {
552 analyticalResultsCrossGamma["1_Swap_EUR " + dscKey[i] + " " + dscKey[j]] = gamma0[i][j];
553 analyticalResultsCrossGamma["1_Swap_EUR " + fwdKey[i] + " " + fwdKey[j]] = gamma0[n + i][n + j];
554 analyticalResultsCrossGamma["7_FxOption_EUR_USD " + dscKey[i] + " " + dscKey[j]] =
555 gamma2[n + i][n + j] * 10.0 / fxspot;
556 analyticalResultsCrossGamma["7_FxOption_EUR_USD " + dscKey2[i] + " " + dscKey2[j]] =
557 gamma2[i][j] * 10.0 / fxspot;
558 }
559 analyticalResultsCrossGamma["1_Swap_EUR " + dscKey[i] + " " + fwdKey[j]] = gamma0[i][n + j];
560 analyticalResultsCrossGamma["7_FxOption_EUR_USD " + dscKey[i] + " " + dscKey2[j]] =
561 gamma2[n + i][j] * 10.0 / fxspot;
562 }
563 }
564
565
566
567
568
569 analyticalResultsDelta["7_FxOption_EUR_USD FXSpot/EURUSD/0/spot"] =
570 10.0 * (deltaSpot2 / fxspot - fxnpv / (fxspot * fxspot));
571
572
573 analyticalResultsGamma["7_FxOption_EUR_USD FXSpot/EURUSD/0/spot"] =
574 10.0 * (2.0 * fxnpv / (fxspot * fxspot * fxspot) - 2.0 * deltaSpot2 / (fxspot * fxspot) + gammaSpot2 / fxspot);
575
576 analyticalResultsDelta["7_FxOption_EUR_USD FXVolatility/EURUSD/0/5Y/ATM"] =
577 vega2.front() * 10.0 / fxspot;
578
579
580 QuantLib::ext::shared_ptr<SensitivityAnalysis> sa = QuantLib::ext::make_shared<SensitivityAnalysis>(
582 sa->generateSensitivities();
583 map<pair<string, string>, Real> deltaMap;
584 map<pair<string, string>, Real> gammaMap;
585
586 auto sensiCube = sa->sensiCube();
587 for (const auto& tradeId : portfolio->ids()) {
588 for (const auto& f : sensiCube->factors()) {
589 string des = sensiCube->factorDescription(f);
590 deltaMap[make_pair(tradeId, des)] = sensiCube->delta(tradeId, f);
591 gammaMap[make_pair(tradeId, des)] = sensiCube->gamma(tradeId, f);
592 }
593 }
594
595 std::vector<ore::analytics::SensitivityScenarioGenerator::ScenarioDescription> scenDesc =
596 sa->scenarioGenerator()->scenarioDescriptions();
597 Real shiftSize = 1E-5;
598
599
600 BOOST_TEST_MESSAGE("Checking deltas...");
601 Size foundDeltas = 0, zeroDeltas = 0;
602 for (auto const& x : deltaMap) {
603 string key = x.first.first + " " + x.first.second;
604 Real scaledResult = x.second / shiftSize;
605 if (analyticalResultsDelta.count(key) > 0) {
606 if (!
check(analyticalResultsDelta.at(key), scaledResult))
607 BOOST_ERROR("Sensitivity analysis result " << key << " (" << scaledResult
608 << ") could not be verified against analytic result ("
609 << analyticalResultsDelta.at(key) << ")");
610 ++foundDeltas;
611 } else {
613 BOOST_ERROR("Sensitivity analysis result " << key << " (" << scaledResult << ") expected to be zero");
614 ++zeroDeltas;
615 }
616 }
617 if (foundDeltas != analyticalResultsDelta.size())
618 BOOST_ERROR("Mismatch between number of analytical results for delta ("
619 << analyticalResultsDelta.size() << ") and sensitivity results (" << foundDeltas << ")");
620 BOOST_TEST_MESSAGE("Checked " << foundDeltas << " deltas against analytical values (and " << zeroDeltas
621 << " deal-unrelated deltas for zero).");
622
623
624 BOOST_TEST_MESSAGE("Checking gammas...");
625 Size foundGammas = 0, zeroGammas = 0;
626 for (auto const& x : gammaMap) {
627 string key = x.first.first + " " + x.first.second;
628 Real scaledResult = x.second / (shiftSize * shiftSize);
629 if (analyticalResultsGamma.count(key) > 0) {
630 if (!
check(analyticalResultsGamma.at(key), scaledResult))
631 BOOST_ERROR("Sensitivity analysis result " << key << " (" << scaledResult
632 << ") could not be verified against analytic result ("
633 << analyticalResultsGamma.at(key) << ")");
634 ++foundGammas;
635 } else {
636
637
638 if (!
close_enough(x.second, 0.0) && key !=
"5_Swaption_EUR SwaptionVolatility/EUR/47/10Y/10Y/ATM" &&
639 key != "7_FxOption_EUR_USD FXVolatility/EURUSD/0/5Y/ATM")
640 BOOST_ERROR("Sensitivity analysis result " << key << " (" << scaledResult << ") expected to be zero");
641 ++zeroGammas;
642 }
643 }
644 if (foundGammas != analyticalResultsGamma.size())
645 BOOST_ERROR("Mismatch between number of analytical results for gamma ("
646 << analyticalResultsGamma.size() << ") and sensitivity results (" << foundGammas << ")");
647 BOOST_TEST_MESSAGE("Checked " << foundGammas << " gammas against analytical values (and " << zeroGammas
648 << " deal-unrelated gammas for zero).");
649
650
651 BOOST_TEST_MESSAGE("Checking cross-gammas...");
652 Size foundCrossGammas = 0, zeroCrossGammas = 0;
653 for (const auto& [tradeId, trade] : portfolio->trades()) {
654 for (auto const& s : scenDesc) {
655 if (s.type() == ShiftScenarioGenerator::ScenarioDescription::Type::Cross) {
656 string key = tradeId + " " + s.factor1() + " " + s.factor2();
657 Real crossGamma = sa->sensiCube()->crossGamma(tradeId, make_pair(s.key1(), s.key2()));
658 Real scaledResult = crossGamma / (shiftSize * shiftSize);
659
660 if (analyticalResultsCrossGamma.count(key) > 0) {
661 if (!
check(analyticalResultsCrossGamma.at(key), scaledResult))
662 BOOST_ERROR("Sensitivity analysis result "
663 << key << " (" << scaledResult
664 << ") could not be verified against analytic result ("
665 << analyticalResultsCrossGamma.at(key) << ")");
666 ++foundCrossGammas;
667 } else {
668 if (!
check(crossGamma, 0.0))
669 BOOST_ERROR("Sensitivity analysis result " << key << " (" << scaledResult
670 << ") expected to be zero");
671 ++zeroCrossGammas;
672 }
673 }
674 }
675 }
676 if (foundCrossGammas != analyticalResultsCrossGamma.size())
677 BOOST_ERROR("Mismatch between number of analytical results for gamma ("
678 << analyticalResultsCrossGamma.size() << ") and sensitivity results (" << foundCrossGammas << ")");
679 BOOST_TEST_MESSAGE("Checked " << foundCrossGammas << " cross gammas against analytical values (and "
680 << zeroCrossGammas << " deal-unrelated cross gammas for zero).");
681
682
683
684
685
686
687 ObservationMode::instance().setMode(backupMode);
688 IndexManager::instance().clearHistories();
689
690 BOOST_CHECK(true);
691}
static const string defaultConfiguration
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
QuantLib::ext::shared_ptr< Trade > buildFxOption(string id, string longShort, string putCall, Size expiry, string boughtCcy, Real boughtAmount, string soldCcy, Real soldAmount, Real premium, string premiumCcy, string premiumDate)
QuantLib::ext::shared_ptr< Trade > buildSwap(string id, string ccy, bool isPayer, Real notional, int start, Size term, Real rate, Real spread, string fixedFreq, string fixedDC, string floatFreq, string floatDC, string index, Calendar calendar, Natural spotDays, bool spotStartLag)
QuantLib::ext::shared_ptr< SensitivityScenarioData > setupSensitivityScenarioData5(bool parConversion)
QuantLib::ext::shared_ptr< analytics::ScenarioSimMarketParameters > setupSimMarketData5()