Build a volatility surface from a collection of expiry and strike delta pairs.
770 {
771
772 using boost::adaptors::transformed;
773 using boost::algorithm::join;
774
775 DLOG(
"EquityVolCurve: start building 2-D volatility delta strike surface");
776
778 "EquityVolCurve: only quote type"
779 << " RATE_LNVOL is currently supported for a 2-D volatility delta strike surface.");
780
781
782 vector<Real> putDeltas = parseVectorOfValues<Real>(vdsc.putDeltas(), &parseReal);
783 sort(putDeltas.begin(), putDeltas.end(), [](Real x, Real y) { return !close(x, y) && x < y; });
784 QL_REQUIRE(adjacent_find(putDeltas.begin(), putDeltas.end(), [](Real x, Real y) { return close(x, y); }) ==
785 putDeltas.end(),
786 "EquityVolCurve: The configured put deltas contain duplicates");
787 DLOG(
"EquityVolCurve: Parsed " << putDeltas.size() <<
" unique configured put deltas");
788 DLOG(
"EquityVolCurve: Put deltas are: " << join(putDeltas | transformed([](Real d) {
return ore::data::to_string(d); }),
","));
789
790
791 vector<Real> callDeltas = parseVectorOfValues<Real>(vdsc.callDeltas(), &parseReal);
792 sort(callDeltas.begin(), callDeltas.end(), [](Real x, Real y) { return !close(x, y) && x > y; });
793 QL_REQUIRE(adjacent_find(callDeltas.begin(), callDeltas.end(), [](Real x, Real y) { return close(x, y); }) ==
794 callDeltas.end(),
795 "EquityVolCurve: The configured call deltas contain duplicates");
796 DLOG(
"EquityVolCurve: Parsed " << callDeltas.size() <<
" unique configured call deltas");
797 DLOG(
"EquityVolCurve: Call deltas are: " << join(callDeltas | transformed([](Real d) {
return ore::data::to_string(d); }),
","));
798
799
800 bool expWc = false;
801 if (find(vdsc.expiries().begin(), vdsc.expiries().end(), "*") != vdsc.expiries().end()) {
802 expWc = true;
803 QL_REQUIRE(vdsc.expiries().size() == 1, "Wild card expiry specified but more expiries also specified.");
804 DLOG(
"EquityVolCurve: Have expiry wildcard pattern " << vdsc.expiries()[0]);
805 }
806
807
808
809 map<Date, vector<Real>> surfaceData;
810
811
812 Size numStrikes = putDeltas.size() + 1 + callDeltas.size();
813
814
815 Size quotesAdded = 0;
816
817
818 DeltaVolQuote::DeltaType deltaType =
parseDeltaType(vdsc.deltaType());
819 DeltaVolQuote::AtmType atmType =
parseAtmType(vdsc.atmType());
820 boost::optional<DeltaVolQuote::DeltaType> atmDeltaType;
821 if (!vdsc.atmDeltaType().empty()) {
823 }
824
825
826 vector<QuantLib::ext::shared_ptr<BaseStrike>>
strikes;
827 for (const auto& pd : putDeltas) {
828 strikes.push_back(QuantLib::ext::make_shared<DeltaStrike>(deltaType, Option::Put, pd));
829 }
830 strikes.push_back(QuantLib::ext::make_shared<AtmStrike>(atmType, atmDeltaType));
831 for (const auto& cd : callDeltas) {
832 strikes.push_back(QuantLib::ext::make_shared<DeltaStrike>(deltaType, Option::Call, cd));
833 }
834
835
836 std::ostringstream ss;
838 << vc.ccy() << "/*";
839 Wildcard w(ss.str());
840 for (const auto& md : loader.get(w, asof)) {
841
842 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
843
844 auto q = QuantLib::ext::dynamic_pointer_cast<EquityOptionQuote>(md);
845 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to EquityOptionQuote");
846 QL_REQUIRE(q->eqName() == vc.equityId(), "EquityOptionQuote eqName '"
847 << q->eqName() << "' <> EquityVolatilityCurveConfig equityId '"
848 << vc.equityId() << "'");
849 QL_REQUIRE(q->ccy() == vc.ccy(),
850 "EquityOptionQuote ccy '" << q->ccy() << "' <> EquityVolatilityCurveConfig ccy '" << vc.ccy() << "'");
851 QL_REQUIRE(q->quoteType() == vdsc.quoteType(),
852 "EquityOptionQuote quoteType '" << q->quoteType() << "' <> VolatilityMoneynessSurfaceConfig quoteType '" << vdsc.quoteType() << "'");
853
854
855 vector<QuantLib::ext::shared_ptr<BaseStrike>>::iterator strikeIt;
856
857 if (expWc) {
858
860 [&q](QuantLib::ext::shared_ptr<BaseStrike> s) { return *s == *q->strike(); });
862 continue;
863 } else {
864
865 auto it = find(vc.quotes().begin(), vc.quotes().end(), q->name());
866 if (it == vc.quotes().end())
867 continue;
868
869
870
872 [&q](QuantLib::ext::shared_ptr<BaseStrike> s) { return *s == *q->strike(); });
873 QL_REQUIRE(strikeIt !=
strikes.end(),
874 "EquityVolCurve: The quote '"
875 << q->name()
876 << "' is in the list of configured quotes but does not match any of the configured strikes");
877 }
878
879
880 Size pos = std::distance(
strikes.begin(), strikeIt);
881
882
883 Date eDate;
884 QuantLib::ext::shared_ptr<Expiry> expiry =
parseExpiry(q->expiry());
885 if (auto expiryDate = QuantLib::ext::dynamic_pointer_cast<ExpiryDate>(expiry)) {
886 eDate = expiryDate->expiryDate();
887 } else if (auto expiryPeriod = QuantLib::ext::dynamic_pointer_cast<ExpiryPeriod>(expiry)) {
888
889 eDate =
calendar_.adjust(asof + expiryPeriod->expiryPeriod());
890 }
891
892
893 if (surfaceData.count(eDate) == 0)
894 surfaceData[eDate] = vector<Real>(numStrikes, Null<Real>());
895
896 QL_REQUIRE(surfaceData[eDate][pos] == Null<Real>(),
897 "EquityVolCurve: Quote " << q->name() << " provides a duplicate quote for the date " << io::iso_date(eDate)
898 << " and strike " << *q->strike());
899 surfaceData[eDate][pos] = q->quote()->value();
900 quotesAdded++;
901
902 TLOG(
"EquityVolCurve: Added quote " << q->name() <<
": (" << io::iso_date(eDate) <<
"," << *q->strike() <<
"," << fixed
903 << setprecision(9) << "," << q->quote()->value() << ")");
904 }
905
906 DLOG(
"EquityVolCurve: EquityVolCurve: added " << quotesAdded <<
" quotes in building delta strike surface.");
907
908
909 if (expWc) {
910
911 for (const auto& kv : surfaceData) {
912 for (Size j = 0; j < numStrikes; j++) {
913 QL_REQUIRE(kv.second[j] != Null<Real>(), "EquityVolCurve: Volatility for expiry date "
914 << io::iso_date(kv.first) << " and strike " << *strikes[j]
915 << " not found. Cannot proceed with a sparse matrix.");
916 }
917 }
918 } else {
919
920
921 QL_REQUIRE(vc.quotes().size() == quotesAdded,
922 "EquityVolCurve: Found " << quotesAdded << " quotes, but " << vc.quotes().size() << " quotes required by config.");
923 }
924
925
926 vector<Date> expiryDates;
927 Matrix vols(surfaceData.size(), numStrikes);
928 for (const auto row : surfaceData | boost::adaptors::indexed(0)) {
929 expiryDates.push_back(row.value().first);
930 copy(row.value().second.begin(), row.value().second.end(), vols.row_begin(row.index()));
931 }
932
933
934
935 transform(putDeltas.begin(), putDeltas.end(), putDeltas.begin(), [](Real pd) { return -1.0 * pd; });
936 DLOG(
"EquityVolCurve: Multiply put deltas by -1.0 before creating BlackVolatilitySurfaceDelta object.");
937 DLOG(
"EquityVolCurve: Put deltas are: " << join(putDeltas | transformed([](Real d) {
return ore::data::to_string(d); }),
","));
938
939
940
941 bool flatExtrapolation = true;
942 if (vdsc.extrapolation()) {
943
946 TLOG(
"EquityVolCurve: Strike extrapolation switched to using interpolator.");
947 flatExtrapolation = false;
949 TLOG(
"EquityVolCurve: Strike extrapolation cannot be turned off on its own so defaulting to flat.");
951 TLOG(
"EquityVolCurve: Strike extrapolation has been set to flat.");
952 } else {
953 TLOG(
"EquityVolCurve: Strike extrapolation " << strikeExtrapType <<
" not expected so default to flat.");
954 }
955
958 TLOG(
"EquityVolCurve: BlackVolatilitySurfaceDelta only supports flat volatility extrapolation in the time direction");
959 }
960 } else {
961 TLOG(
"EquityVolCurve: Extrapolation is turned off for the whole surface so the time and"
962 << " strike extrapolation settings are ignored");
963 }
964
965
966 if (vdsc.timeInterpolation() != "Linear") {
967 TLOG(
"EquityVolCurve: BlackVolatilitySurfaceDelta only supports linear time interpolation.");
968 }
969
970
972 if (vdsc.strikeInterpolation() == "Linear") {
973 im = InterpolatedSmileSection::InterpolationMethod::Linear;
974 } else if (vdsc.strikeInterpolation() == "NaturalCubic") {
975 im = InterpolatedSmileSection::InterpolationMethod::NaturalCubic;
976 } else if (vdsc.strikeInterpolation() == "FinancialCubic") {
977 im = InterpolatedSmileSection::InterpolationMethod::FinancialCubic;
978 } else if (vdsc.strikeInterpolation() == "CubicSpline") {
979 im = InterpolatedSmileSection::InterpolationMethod::CubicSpline;
980 } else {
981 im = InterpolatedSmileSection::InterpolationMethod::Linear;
982 DLOG(
"EquityVolCurve: BlackVolatilitySurfaceDelta does not support strike interpolation '" << vdsc.strikeInterpolation()
983 << "' so setting it to linear.");
984 }
985
986
987 if (!expiryDates.empty())
989
990 DLOG(
"EquityVolCurve: Creating BlackVolatilitySurfaceDelta object");
991 bool hasAtm = true;
992 vol_ = QuantLib::ext::make_shared<BlackVolatilitySurfaceDelta>(
993 asof, expiryDates, putDeltas, callDeltas, hasAtm, vols,
dayCounter_,
calendar_, eqIndex->equitySpot(),
994 eqIndex->equityForecastCurve(), eqIndex->equityDividendCurve(), deltaType, atmType, atmDeltaType, 0 * Days,
995 deltaType, atmType, atmDeltaType, im, flatExtrapolation);
996
997 DLOG(
"EquityVolCurve: Setting BlackVolatilitySurfaceDelta extrapolation to " <<
to_string(vdsc.extrapolation()));
998 vol_->enableExtrapolation(vdsc.extrapolation());
999
1000 DLOG(
"EquityVolCurve: finished building 2-D volatility delta strike surface");
1001}
QuantLib::Date maxExpiry_
QuantLib::ext::shared_ptr< BlackVolTermStructure > vol_
QuantLib::Calendar calendar_
QuantLib::DayCounter dayCounter_
DeltaVolQuote::AtmType parseAtmType(const std::string &s)
Convert text to QuantLib::DeltaVolQuote::AtmType.
DeltaVolQuote::DeltaType parseDeltaType(const std::string &s)
Convert text to QuantLib::DeltaVolQuote::DeltaType.
#define DLOG(text)
Logging Macro (Level = Debug)
#define TLOG(text)
Logging Macro (Level = Data)
std::string to_string(const LocationInfo &l)
Extrapolation parseExtrapolation(const string &s)
Parse Extrapolation from string.
QuantLib::ext::shared_ptr< Expiry > parseExpiry(const string &strExpiry)
Parse an Expiry from its string representation, strExpiry.