From bf1d88036c2a394ec047b16a6f959548b9c7ca59 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Fri, 23 Feb 2024 23:03:52 +0100 Subject: [PATCH 01/36] Bjerksund-Stensland spread engine --- ql/CMakeLists.txt | 6 + .../basket/bjerksundstenslandspreadengine.cpp | 60 +++++ .../basket/bjerksundstenslandspreadengine.hpp | 53 ++++ ql/pricingengines/basket/kirkengine.cpp | 52 +--- ql/pricingengines/basket/kirkengine.hpp | 15 +- .../basket/operatorsplittingspreadengine.cpp | 63 +++++ .../basket/operatorsplittingspreadengine.hpp | 53 ++++ .../spreadblackscholesvanillaengine.cpp | 72 +++++ .../spreadblackscholesvanillaengine.hpp | 54 ++++ test-suite/basketoption.cpp | 248 ++++++++++++++++++ 10 files changed, 625 insertions(+), 51 deletions(-) create mode 100644 ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp create mode 100644 ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp create mode 100644 ql/pricingengines/basket/operatorsplittingspreadengine.cpp create mode 100644 ql/pricingengines/basket/operatorsplittingspreadengine.hpp create mode 100644 ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp create mode 100644 ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 4647a233626..e014cf1e75d 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -674,10 +674,13 @@ set(QL_SOURCES pricingengines/barrier/fdhestondoublebarrierengine.cpp pricingengines/barrier/fdhestonrebateengine.cpp pricingengines/barrier/mcbarrierengine.cpp + pricingengines/basket/bjerksundstenslandspreadengine.cpp pricingengines/basket/fd2dblackscholesvanillaengine.cpp pricingengines/basket/kirkengine.cpp pricingengines/basket/mcamericanbasketengine.cpp pricingengines/basket/mceuropeanbasketengine.cpp + pricingengines/basket/operatorsplittingspreadengine.cpp + pricingengines/basket/spreadblackscholesvanillaengine.cpp pricingengines/basket/stulzengine.cpp pricingengines/blackcalculator.cpp pricingengines/blackformula.cpp @@ -1881,10 +1884,13 @@ set(QL_HEADERS pricingengines/barrier/fdhestondoublebarrierengine.hpp pricingengines/barrier/fdhestonrebateengine.hpp pricingengines/barrier/mcbarrierengine.hpp + pricingengines/basket/bjerksundstenslandspreadengine.hpp pricingengines/basket/fd2dblackscholesvanillaengine.hpp pricingengines/basket/kirkengine.hpp pricingengines/basket/mcamericanbasketengine.hpp pricingengines/basket/mceuropeanbasketengine.hpp + pricingengines/basket/operatorsplittingspreadengine.hpp + pricingengines/basket/spreadblackscholesvanillaengine.hpp pricingengines/basket/stulzengine.hpp pricingengines/blackcalculator.hpp pricingengines/blackformula.hpp diff --git a/ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp b/ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp new file mode 100644 index 00000000000..f6503369ff1 --- /dev/null +++ b/ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp @@ -0,0 +1,60 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include +#include + +namespace QuantLib { + + BjerksundStenslandSpreadEngine::BjerksundStenslandSpreadEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation) + : SpreadBlackScholesVanillaEngine(process1, process2, correlation) { + } + + Real BjerksundStenslandSpreadEngine::calculate( + Real k, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const { + + const Real cp = (optionType == Option::Call) ? 1 : -1; + + const Real a = f2_ + k; + const Real b = f2_/a; + + const Real sigma1 = std::sqrt(variance1); + const Real sigma2 = std::sqrt(variance2); + + const Real stdev = std::sqrt( + variance1 + b*b*variance2 - 2*rho_*b*sigma1*sigma2); + + const Real lfa = std::log(f1_/a); + + const Real d1 = + (lfa + (0.5*variance1 + 0.5*b*b*variance2 - b*rho_*sigma1*sigma2))/stdev; + const Real d2 = + (lfa + (-0.5*variance1 + variance2*b*(0.5*b - 1) + rho_*sigma1*sigma2))/stdev; + const Real d3 = (lfa + (-0.5*variance1 + 0.5*b*b*variance2))/stdev; + + const CumulativeNormalDistribution phi; + return df*cp*(f1_*phi(cp*d1) - f2_*phi(cp*d2) - k*phi(cp*d3)); + } +} + + diff --git a/ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp b/ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp new file mode 100644 index 00000000000..c810b0ffca4 --- /dev/null +++ b/ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp @@ -0,0 +1,53 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file bjerksundstenslandspreadengine.hpp + \brief Bjerksund and Stensland formulae (2006) +*/ + +#ifndef quantlib_bjerksund_stensland_spread_engine_hpp +#define quantlib_bjerksund_stensland_spread_engine_hpp + +#include + +namespace QuantLib { + + //! Pricing engine for spread option on two futures + /*! P. Bjerksund and G. Stensland, + Closed form spread option valuation, + Quantitative Finance, 14 (2014), pp. 1785–1794. + + \ingroup basketengines + */ + class BjerksundStenslandSpreadEngine : public SpreadBlackScholesVanillaEngine { + public: + BjerksundStenslandSpreadEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation); + + protected: + Real calculate( + Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const override; + }; +} + + +#endif diff --git a/ql/pricingengines/basket/kirkengine.cpp b/ql/pricingengines/basket/kirkengine.cpp index 2a5189a0a38..629f01520d4 100644 --- a/ql/pricingengines/basket/kirkengine.cpp +++ b/ql/pricingengines/basket/kirkengine.cpp @@ -17,67 +17,35 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ -#include #include #include #include -#include -#include namespace QuantLib { KirkEngine::KirkEngine(ext::shared_ptr process1, ext::shared_ptr process2, Real correlation) - : process1_(std::move(process1)), process2_(std::move(process2)), rho_(correlation) { - registerWith(process1_); - registerWith(process2_); + : SpreadBlackScholesVanillaEngine(process1, process2, correlation) { } - void KirkEngine::calculate() const { - - QL_REQUIRE(arguments_.exercise->type() == Exercise::European, - "not a European option"); - - ext::shared_ptr exercise = - ext::dynamic_pointer_cast(arguments_.exercise); - QL_REQUIRE(exercise, "not a European exercise"); - - ext::shared_ptr spreadPayoff = - ext::dynamic_pointer_cast(arguments_.payoff); - QL_REQUIRE(spreadPayoff," spread payoff expected"); - - ext::shared_ptr payoff = - ext::dynamic_pointer_cast( - spreadPayoff->basePayoff()); - QL_REQUIRE(payoff, "non-plain payoff given"); - const Real strike = payoff->strike(); + Real KirkEngine::calculate( + Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const { - const Real f1 = process1_->stateVariable()->value(); - const Real f2 = process2_->stateVariable()->value(); - - // use atm vols - const Real variance1 = process1_->blackVolatility()->blackVariance( - exercise->lastDate(), f1); - const Real variance2 = process2_->blackVolatility()->blackVariance( - exercise->lastDate(), f2); - - const DiscountFactor riskFreeDiscount = - process1_->riskFreeRate()->discount(exercise->lastDate()); - - const Real f = f1/(f2 + strike); + const Real f = f1_/(f2_ + strike); const Real v = std::sqrt(variance1 - + variance2*squared(f2/(f2+strike)) + + variance2*squared(f2_/(f2_+strike)) - 2*rho_*std::sqrt(variance1*variance2) - *(f2/(f2+strike))); + *(f2_/(f2_+strike))); BlackCalculator black( ext::make_shared( - payoff->optionType(),1.0), - f, v, riskFreeDiscount); + optionType,1.0), + f, v, df); - results_.value = (f2 + strike)*black.value(); + return (f2_ + strike)*black.value(); } } diff --git a/ql/pricingengines/basket/kirkengine.hpp b/ql/pricingengines/basket/kirkengine.hpp index 47e3a61b125..0d48abc522f 100644 --- a/ql/pricingengines/basket/kirkengine.hpp +++ b/ql/pricingengines/basket/kirkengine.hpp @@ -24,8 +24,7 @@ #ifndef quantlib_kirk_engine_hpp #define quantlib_kirk_engine_hpp -#include -#include +#include namespace QuantLib { @@ -40,19 +39,17 @@ namespace QuantLib { \test the correctness of the returned value is tested by reproducing results available in literature. */ - class KirkEngine : public BasketOption::engine { + class KirkEngine : public SpreadBlackScholesVanillaEngine { public: KirkEngine(ext::shared_ptr process1, ext::shared_ptr process2, Real correlation); - void calculate() const override; - private: - ext::shared_ptr process1_; - ext::shared_ptr process2_; - Real rho_; + protected: + Real calculate( + Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const override; }; - } diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp new file mode 100644 index 00000000000..0f6079b447f --- /dev/null +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp @@ -0,0 +1,63 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include + +#include +#include + +namespace QuantLib { + + OperatorSplittingSpreadEngine::OperatorSplittingSpreadEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation) + : SpreadBlackScholesVanillaEngine(process1, process2, correlation) { + } + + Real OperatorSplittingSpreadEngine::calculate( + Real k, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const { + + const Real vol1 = std::sqrt(variance1); + const Real vol2 = std::sqrt(variance2); + const Real sig2 = vol2*f2_/(f2_+k); + const Real sig_m = std::sqrt(variance1 +sig2*(sig2 - 2*rho_*vol1)); + + const Real d1 = (std::log(f1_) - std::log(f2_ + k))/sig_m + 0.5*sig_m; + const Real d2 = d1 - sig_m; + + const CumulativeNormalDistribution N; + const Real kirkNPV = df*(f1_*N(d1) - (f2_ + k)*N(d2)); + + const Real v = (rho_*vol1 - sig2)*vol2/(sig_m*sig_m); + const Real approx = kirkNPV + - 0.5 * sig2*sig2 * k * df * NormalDistribution()(d2) * v + *( d2*(1 - rho_*vol1/sig2) + - 0.5*sig_m * v * k / (f2_+k) + * ( d1*d2 + (1-rho_*rho_)*squared(vol1/(rho_*vol1-sig2)))); + + if (optionType == Option::Call) + return approx; + else + return approx - df*(f1_-f2_-k); + } +} + + diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.hpp b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp new file mode 100644 index 00000000000..cf06df5f33c --- /dev/null +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp @@ -0,0 +1,53 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file operatorsplittingspreadengine.hpp + \brief Analytic operator splitting approximation by Chi-Fai Lo (2015) +*/ + +#ifndef quantlib_operator_splitting_spread_engine_hpp +#define quantlib_operator_splitting_spread_engine_hpp + +#include + +namespace QuantLib { + + //! Pricing engine for spread option on two futures + /*! Chi-Fai Lo, + Pricing Spread Options by the Operator Splitting Method, + https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2429696 + + \ingroup basketengines + */ + class OperatorSplittingSpreadEngine : public SpreadBlackScholesVanillaEngine { + public: + OperatorSplittingSpreadEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation); + + protected: + Real calculate( + Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const override; + }; +} + + +#endif diff --git a/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp b/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp new file mode 100644 index 00000000000..0726329ca4f --- /dev/null +++ b/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp @@ -0,0 +1,72 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include +#include + +namespace QuantLib { + + SpreadBlackScholesVanillaEngine::SpreadBlackScholesVanillaEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation) + : process1_(std::move(process1)), process2_(std::move(process2)), rho_(correlation) { + update(); + + registerWith(process1_); + registerWith(process2_); + } + + void SpreadBlackScholesVanillaEngine::update() { + f1_ = process1_->stateVariable()->value(); + f2_ = process2_->stateVariable()->value(); + + BasketOption::engine::update(); + } + + void SpreadBlackScholesVanillaEngine::calculate() const { + QL_REQUIRE(arguments_.exercise->type() == Exercise::European, + "not a European option"); + + const ext::shared_ptr exercise = + ext::dynamic_pointer_cast(arguments_.exercise); + QL_REQUIRE(exercise, "not a European exercise"); + + const ext::shared_ptr spreadPayoff = + ext::dynamic_pointer_cast(arguments_.payoff); + QL_REQUIRE(spreadPayoff," spread payoff expected"); + + const ext::shared_ptr payoff = + ext::dynamic_pointer_cast( + spreadPayoff->basePayoff()); + QL_REQUIRE(payoff, "non-plain payoff given"); + const Real strike = payoff->strike(); + const Option::Type optionType = payoff->optionType(); + + const Real variance1 = + process1_->blackVolatility()->blackVariance(exercise->lastDate(), f1_); + const Real variance2 = + process2_->blackVolatility()->blackVariance(exercise->lastDate(), f2_); + + const DiscountFactor df = + process1_->riskFreeRate()->discount(exercise->lastDate()); + + results_.value = calculate(strike, optionType, variance1, variance2, df); + } +} diff --git a/ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp b/ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp new file mode 100644 index 00000000000..41fbdb269cf --- /dev/null +++ b/ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp @@ -0,0 +1,54 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file spreadblackscholesvanillaengine.hpp + \brief base class for 2d spread pricing engines using the Black-Scholes model. +*/ + +#ifndef quantlib_spread_black_scholes_vanilla_engine_hpp +#define quantlib_spread_black_scholes_vanilla_engine_hpp + +#include +#include + +namespace QuantLib { + + class SpreadBlackScholesVanillaEngine : public BasketOption::engine { + public: + SpreadBlackScholesVanillaEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation); + + void update() override; + void calculate() const override; + + protected: + virtual Real calculate( + Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const = 0; + + const ext::shared_ptr process1_; + const ext::shared_ptr process2_; + const Real rho_; + Real f1_, f2_; + }; +} + +#endif diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 83b98e18185..4d47e0501bd 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -24,10 +24,12 @@ #include "utilities.hpp" #include #include +#include #include #include #include #include +#include #include #include #include @@ -35,8 +37,11 @@ #include #include #include +#include #include +#include #include +#include using namespace QuantLib; using namespace boost::unit_test_framework; @@ -1053,6 +1058,249 @@ BOOST_AUTO_TEST_CASE(test2DPDEGreeks) { } } + +BOOST_AUTO_TEST_CASE(testBjerksundStenslandSpreadEngine) { + BOOST_TEST_MESSAGE("Testing Bjerksund-Stensland spread engine..."); + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(1, March, 2024); + const Date maturity = today + Period(12, Months); + + const ext::shared_ptr exercise + = ext::make_shared(maturity); + + const Real rho = 0.75; + const Real f1 = 100, f2 = 110; + const Handle r + = Handle(flatRate(today, 0.05, dc)); + const ext::shared_ptr p1 = + ext::make_shared( + Handle(ext::make_shared(f1)), r, + Handle(flatVol(today, 0.25, dc)) + ); + const ext::shared_ptr p2 = + ext::make_shared( + Handle(ext::make_shared(f2)), r, + Handle(flatVol(today, 0.35, dc)) + ); + + const ext::shared_ptr engine + = ext::make_shared(p1, p2, rho); + + const Real strike = 5; + BasketOption callOption(ext::make_shared( + ext::make_shared(Option::Call, strike)), + exercise); + + callOption.setPricingEngine(engine); + const Real callNPV = callOption.NPV(); + + // reference value was calculated with python packages pyfeng 0.2.6 + const Real expectedPutNPV = 17.850835947276213; + + BasketOption putOption(ext::make_shared( + ext::make_shared(Option::Put, strike)), + exercise); + + putOption.setPricingEngine(engine); + const Real putNPV = putOption.NPV(); + + const Real tol = QL_EPSILON*100; + Real diff = std::abs(putNPV - expectedPutNPV); + + if (diff > tol) { + BOOST_FAIL("failed to reproduce reference put price " + "using the Bjerksund-Stensland spread engine." + << std::fixed << std::setprecision(8) + << "\n calculated: " << putOption.NPV() + << "\n expected : " << expectedPutNPV + << "\n diff : " << diff + << "\n tolerance : " << tol); + } + + const DiscountFactor df = r->discount(maturity); + const Real fwd = (callNPV - putNPV)/df; + diff = std::abs(fwd - (f1 - f2 - strike)); + + if (diff > tol) { + BOOST_FAIL("failed to reproduce call-put parity " + "using the Bjerksund-Stensland spread engine." + << std::fixed << std::setprecision(8) + << "\n calculated fwd: " << fwd + << "\n expected fwd : " << f1 - f2 - strike + << "\n diff : " << diff + << "\n tolerance : " << tol); + } +} + +BOOST_AUTO_TEST_CASE(testOperatorSplittingSpreadEngine) { + BOOST_TEST_MESSAGE("Testing Operator Splitting spread engine..."); + + // Example taken from + // Chi-Fai Lo, Pricing Spread Options by the Operator Splitting Method, + // https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2429696 + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(1, March, 2025); + const Date maturity = yearFractionToDate(dc, today, 1.0); + + const Handle r + = Handle(flatRate(today, 0.05, dc)); + + const DiscountFactor df = r->discount(maturity); + const DiscountFactor dq1 = flatRate(today, 0.03, dc)->discount(maturity); + const DiscountFactor dq2 = flatRate(today, 0.02, dc)->discount(maturity); + const Real f1 = 110*dq1/df, f2 = 90*dq2/df; + + const ext::shared_ptr p1 = + ext::make_shared( + Handle(ext::make_shared(f1)), r, + Handle(flatVol(today, 0.3, dc)) + ); + const ext::shared_ptr p2 = + ext::make_shared( + Handle(ext::make_shared(f2)), r, + Handle(flatVol(today, 0.2, dc)) + ); + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Call, 20.0)), + ext::make_shared(maturity)); + + const Real testData[][2] = { + {-0.9, 18.9323}, + {-0.7, 18.0092}, + {-0.5, 17.0325}, + {-0.4, 16.5211}, + {-0.3, 15.9925}, + {-0.2, 15.4449}, + {-0.1, 14.8762}, + { 0.0, 14.284}, + { 0.1, 13.6651}, + { 0.2, 13.016}, + { 0.3, 12.3319}, + { 0.4, 11.6067}, + { 0.5, 10.8323}, + { 0.7, 9.0863}, + { 0.9, 6.9148} + }; + for (Size i = 0; i < LENGTH(testData); ++i) { + const Real rho = testData[i][0]; + const Real expected = testData[i][1]; + + const ext::shared_ptr osEngine + = ext::make_shared(p1, p2, rho); + + option.setPricingEngine(osEngine); + + const Real diff = std::abs(option.NPV() - expected); + const Real tol = 0.0001; + + if (diff > tol) { + BOOST_FAIL("failed to reproduce reference values " + "using the operator splitting spread engine." + << std::fixed << std::setprecision(5) + << "\n calculated: " << option.NPV() + << "\n expected : " << expected + << "\n diff : " << diff + << "\n tolerance : " << tol); + } + } +} + + +BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { + BOOST_TEST_MESSAGE("Testing two-dimensional PDE engine " + "vs analytical approximations..."); + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(5, February, 2024); + const Date maturity = today + Period(6, Months); + + const ext::shared_ptr s1 = ext::make_shared(100); + const ext::shared_ptr s2 = ext::make_shared(100); + + const ext::shared_ptr r = ext::make_shared(0.05); + + const ext::shared_ptr v1 = ext::make_shared(0.25); + const ext::shared_ptr v2 = ext::make_shared(0.4); + + const ext::shared_ptr p1 = + ext::make_shared( + Handle(s1), + Handle(flatRate(today, r, dc)), + Handle(flatVol(today, v1, dc)) + ); + const ext::shared_ptr p2 = + ext::make_shared( + Handle(s2), + Handle(flatRate(today, r, dc)), + Handle(flatVol(today, v2, dc)) + ); + + const Real strike = 5; + + IncrementalStatistics statKirk, statBS2014, statOs; + + for (Option::Type type: {Option::Call, Option::Put}) { + BasketOption option( + ext::make_shared( + ext::make_shared(type, strike)), + ext::make_shared(maturity)); + + for (Real rho: {-0.75, 0.0, 0.9}) { + const ext::shared_ptr kirkEngine + = ext::make_shared(p1, p2, rho); + + const ext::shared_ptr bs2014Engine + = ext::make_shared(p1, p2, rho); + + const ext::shared_ptr osEngine + = ext::make_shared(p1, p2, rho); + + const ext::shared_ptr fdEngine + = ext::make_shared( + p1, p2, rho, 50, 50, 15); + + for (Real rate: {0.0, 0.05, 0.2}) { + r->setValue(rate); + for (Real spot: {75, 90, 100, 105, 175}) { + s2->setValue(spot); + + option.setPricingEngine(fdEngine); + const Real fdNPV = option.NPV(); + + option.setPricingEngine(kirkEngine); + statKirk.add(option.NPV() - fdNPV); + + option.setPricingEngine(bs2014Engine); + statBS2014.add(option.NPV() - fdNPV); + + option.setPricingEngine(osEngine); + statOs.add(option.NPV() - fdNPV); + } + } + } + } + + + if (statKirk.standardDeviation() > 0.025) { + BOOST_FAIL("failed to reproduce PDE spread option prices " + "with Kirk engine." + << std::fixed << std::setprecision(5) + << "\n stdev: " << option.NPV() + << "\n expected : " << expected + << "\n diff : " << diff + << "\n tolerance : " << tol); + + } + std::cout << std::setprecision(18) + << statKirk.standardDeviation() << " " + << statBS2014.standardDeviation() << " " + << statOs.standardDeviation() << std::endl; +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From d295a28d123577f34c45d9c32d647319c4352c1e Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Fri, 23 Feb 2024 23:17:00 +0100 Subject: [PATCH 02/36] fixed syntax error --- test-suite/basketoption.cpp | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 4d47e0501bd..2a86c76ce82 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1285,15 +1285,25 @@ BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { } - if (statKirk.standardDeviation() > 0.025) { - BOOST_FAIL("failed to reproduce PDE spread option prices " - "with Kirk engine." + if (statKirk.standardDeviation() > 0.03) { + BOOST_FAIL("failed to reproduce PDE spread option prices with Kirk engine." << std::fixed << std::setprecision(5) - << "\n stdev: " << option.NPV() - << "\n expected : " << expected - << "\n diff : " << diff - << "\n tolerance : " << tol); - + << "\n stdev : " << statKirk.standardDeviation() + << "\n tolerance : " << 0.03); + } + if (statBS2014.standardDeviation() > 0.02) { + BOOST_FAIL("failed to reproduce PDE spread option prices" + " with Bjerksund-Stensland engine." + << std::fixed << std::setprecision(5) + << "\n stdev : " << statBS2014.standardDeviation() + << "\n tolerance : " << 0.02); + } + if (statOs.standardDeviation() > 0.02) { + BOOST_FAIL("failed to reproduce PDE spread option prices" + " with Operator-Splitting engine." + << std::fixed << std::setprecision(5) + << "\n stdev : " << statOs.standardDeviation() + << "\n tolerance : " << 0.02); } std::cout << std::setprecision(18) << statKirk.standardDeviation() << " " From 6f0f78b3ff5b7e8c3de5bac2287a690e974e7383 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 1 Apr 2024 14:25:25 +0200 Subject: [PATCH 03/36] first light --- ql/CMakeLists.txt | 6 + .../matrixutilities/choleskydecomposition.cpp | 22 ++ .../matrixutilities/choleskydecomposition.hpp | 2 + ql/math/matrixutilities/pseudosqrt.cpp | 27 +++ ql/math/matrixutilities/pseudosqrt.hpp | 2 +- .../operators/fdmndimblackscholesop.cpp | 141 +++++++++++ .../operators/fdmndimblackscholesop.hpp | 67 ++++++ .../basket/denglizhouspreadengine.cpp | 141 +++++++++++ .../basket/denglizhouspreadengine.hpp | 61 +++++ .../fdndimblackscholesvanillaengine.cpp | 119 +++++++++ .../fdndimblackscholesvanillaengine.hpp | 67 ++++++ .../spreadblackscholesvanillaengine.cpp | 5 +- test-suite/basketoption.cpp | 225 +++++++++++++++++- test-suite/matrices.cpp | 60 ++++- 14 files changed, 933 insertions(+), 12 deletions(-) create mode 100644 ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp create mode 100644 ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp create mode 100644 ql/pricingengines/basket/denglizhouspreadengine.cpp create mode 100644 ql/pricingengines/basket/denglizhouspreadengine.hpp create mode 100644 ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp create mode 100644 ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index e014cf1e75d..9093dfb074b 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -447,6 +447,7 @@ set(QL_SOURCES methods/finitedifferences/meshers/fdmsimpleprocess1dmesher.cpp methods/finitedifferences/meshers/uniformgridmesher.cpp methods/finitedifferences/operators/fdm2dblackscholesop.cpp + methods/finitedifferences/operators/fdmndimblackscholesop.cpp methods/finitedifferences/operators/fdmbatesop.cpp methods/finitedifferences/operators/fdmblackscholesfwdop.cpp methods/finitedifferences/operators/fdmblackscholesop.cpp @@ -675,7 +676,9 @@ set(QL_SOURCES pricingengines/barrier/fdhestonrebateengine.cpp pricingengines/barrier/mcbarrierengine.cpp pricingengines/basket/bjerksundstenslandspreadengine.cpp + pricingengines/basket/denglizhouspreadengine.cpp pricingengines/basket/fd2dblackscholesvanillaengine.cpp + pricingengines/basket/fdndimblackscholesvanillaengine.cpp pricingengines/basket/kirkengine.cpp pricingengines/basket/mcamericanbasketengine.cpp pricingengines/basket/mceuropeanbasketengine.cpp @@ -1592,6 +1595,7 @@ set(QL_HEADERS methods/finitedifferences/meshers/uniformgridmesher.hpp methods/finitedifferences/mixedscheme.hpp methods/finitedifferences/operators/fdm2dblackscholesop.hpp + methods/finitedifferences/operators/fdmndimblackscholesop.hpp methods/finitedifferences/operators/fdmbatesop.hpp methods/finitedifferences/operators/fdmblackscholesfwdop.hpp methods/finitedifferences/operators/fdmblackscholesop.hpp @@ -1885,7 +1889,9 @@ set(QL_HEADERS pricingengines/barrier/fdhestonrebateengine.hpp pricingengines/barrier/mcbarrierengine.hpp pricingengines/basket/bjerksundstenslandspreadengine.hpp + pricingengines/basket/denglizhouspreadengine.hpp pricingengines/basket/fd2dblackscholesvanillaengine.hpp + pricingengines/basket/fdndimblackscholesvanillaengine.hpp pricingengines/basket/kirkengine.hpp pricingengines/basket/mcamericanbasketengine.hpp pricingengines/basket/mceuropeanbasketengine.hpp diff --git a/ql/math/matrixutilities/choleskydecomposition.cpp b/ql/math/matrixutilities/choleskydecomposition.cpp index 3ee08024257..34ecd203a9e 100644 --- a/ql/math/matrixutilities/choleskydecomposition.cpp +++ b/ql/math/matrixutilities/choleskydecomposition.cpp @@ -3,6 +3,7 @@ /* Copyright (C) 2003, 2004 Ferdinando Ametrano Copyright (C) 2016 Peter Caspers + Copyright (C) 2024 Klaus Spanderen This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - http://quantlib.org/ @@ -61,4 +62,25 @@ namespace QuantLib { } return result; } + + Array CholeskySolveFor(const Matrix& L, const Array& b) { + const Size n = b.size(); + + QL_REQUIRE(L.columns() == n && L.rows() == n, + "Size of input matrix and vector does not match."); + + Array x(n); + for (Size i=0; i < n; ++i) { + x[i] = -std::inner_product(L.row_begin(i), L.row_begin(i)+i, x.begin(), -b[i]); + x[i] /= L[i][i]; + } + + for (Integer i=n-1; i >=0; --i) { + x[i] = -std::inner_product( + L.column_begin(i)+i+1, L.column_end(i), x.begin()+i+1, -x[i]); + x[i] /= L[i][i]; + } + + return x; + } } diff --git a/ql/math/matrixutilities/choleskydecomposition.hpp b/ql/math/matrixutilities/choleskydecomposition.hpp index 893cb4dd0c7..3f826f9af03 100644 --- a/ql/math/matrixutilities/choleskydecomposition.hpp +++ b/ql/math/matrixutilities/choleskydecomposition.hpp @@ -2,6 +2,7 @@ /* Copyright (C) 2003, 2004 Ferdinando Ametrano + Copyright (C) 2024 Klaus Spanderen This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - http://quantlib.org/ @@ -30,6 +31,7 @@ namespace QuantLib { /*! \relates Matrix */ Matrix CholeskyDecomposition(const Matrix& m, bool flexible = false); + Array CholeskySolveFor(const Matrix& L, const Array& b); } diff --git a/ql/math/matrixutilities/pseudosqrt.cpp b/ql/math/matrixutilities/pseudosqrt.cpp index 0cf0986996b..5c1c276fa66 100644 --- a/ql/math/matrixutilities/pseudosqrt.cpp +++ b/ql/math/matrixutilities/pseudosqrt.cpp @@ -416,6 +416,33 @@ namespace QuantLib { result = CholeskyDecomposition(result, true); } break; + case SalvagingAlgorithm::Principal: { + QL_REQUIRE(jd.eigenvalues().back()>=-10*QL_EPSILON, + "negative eigenvalue(s) (" + << std::scientific << jd.eigenvalues().back() + << ")"); + + Array sqrtEigenvalues(size); + std::transform( + jd.eigenvalues().begin(), jd.eigenvalues().end(), + sqrtEigenvalues.begin(), + [](Real lambda) -> Real { + return std::sqrt(std::max(lambda, 0.0)); + } + ); + + for (Size i=0; i < size; ++i) + std::transform( + sqrtEigenvalues.begin(), sqrtEigenvalues.end(), + jd.eigenvectors().row_begin(i), + diagonal.column_begin(i), + std::multiplies() + ); + + result = jd.eigenvectors()*diagonal; + result = 0.5*(result + transpose(result)); + } + break; default: QL_FAIL("unknown salvaging algorithm"); } diff --git a/ql/math/matrixutilities/pseudosqrt.hpp b/ql/math/matrixutilities/pseudosqrt.hpp index f5a1537df21..50cb8847793 100644 --- a/ql/math/matrixutilities/pseudosqrt.hpp +++ b/ql/math/matrixutilities/pseudosqrt.hpp @@ -30,7 +30,7 @@ namespace QuantLib { //! algorithm used for matricial pseudo square root struct SalvagingAlgorithm { - enum Type { None, Spectral, Hypersphere, LowerDiagonal, Higham }; + enum Type { None, Spectral, Hypersphere, LowerDiagonal, Higham, Principal }; }; //! Returns the pseudo square root of a real symmetric matrix diff --git a/ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp b/ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp new file mode 100644 index 00000000000..d89684d3688 --- /dev/null +++ b/ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp @@ -0,0 +1,141 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + + +/*! \file fdmndimblackscholesop.cpp +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace QuantLib { + + FdmndimBlackScholesOp::FdmndimBlackScholesOp( + ext::shared_ptr mesher, + std::vector > processes, + Matrix rho, + Time maturity) + : mesher_(std::move(mesher)), + processes_(std::move(processes)), + currentForwardRate_(Null()) { + + QL_REQUIRE(!processes_.empty(), "no Black-Scholes process is given."); + QL_REQUIRE(rho.size1() == rho.size2() + && rho.size1() == processes_.size(), + "correlation matrix has the wrong size."); + + for (Size direction = 0; direction < processes_.size(); ++direction) { + const auto process = processes_[direction]; + ops_.push_back( + ext::make_shared( + mesher_, process, process->x0(), false, -Null(), direction + ) + ); + } + + for (Size i=1; i < processes_.size(); ++i) { + const auto p1 = processes_[i]; + const Volatility v1 + = p1->blackVolatility()->blackVol(maturity, p1->x0(), true); + + for (Size j=0; j < i; ++j) { + const auto p2 = processes_[j]; + const Volatility v2 + = p2->blackVolatility()->blackVol(maturity, p2->x0(), true); + + corrMaps_.emplace_back( + new NinePointLinearOp( + SecondOrderMixedDerivativeOp(i, j, mesher_) + .mult(Array(mesher_->layout()->size(), v1*v2*rho[i][j])) + ) + ); + } + } + } + + Size FdmndimBlackScholesOp::size() const { + return processes_.size(); + } + + void FdmndimBlackScholesOp::setTime(Time t1, Time t2) { + for (auto& op: ops_) + op->setTime(t1, t2); + + currentForwardRate_ = 0.0; + for (Size i=1; i < processes_.size(); ++i) + currentForwardRate_ += + processes_[i]->riskFreeRate()->forwardRate(t1, t2, Continuous).rate(); + } + + Array FdmndimBlackScholesOp::apply(const Array& x) const { + Array y = apply_mixed(x); + for (const auto& op: ops_) + y += op->apply(x); + + return y; + } + + Array FdmndimBlackScholesOp::apply_mixed(const Array& x) const { + Array y = currentForwardRate_*x; + for (const auto& m: corrMaps_) + y += m->apply(x); + + return y; + } + + Array FdmndimBlackScholesOp::apply_direction(Size direction, const Array& x) const { + return ops_[direction]->apply(x); + } + + Array FdmndimBlackScholesOp::solve_splitting( + Size direction, const Array& x, Real s) const { + + return ops_[direction]->solve_splitting(direction, x, s); + } + + Array FdmndimBlackScholesOp::preconditioner(const Array& r, Real dt) const { + return solve_splitting(0, r, dt); + } + + std::vector FdmndimBlackScholesOp::toMatrixDecomp() const { + std::vector retVal; + + for (const auto& op: ops_) + retVal.push_back(op->toMatrix()); + + SparseMatrix mixed = + currentForwardRate_*boost::numeric::ublas::identity_matrix( + mesher_->layout()->size()); + for (const auto& m: corrMaps_) + mixed += m->toMatrix(); + + retVal.push_back(mixed); + + return retVal; + } +} diff --git a/ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp b/ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp new file mode 100644 index 00000000000..f35771203f6 --- /dev/null +++ b/ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp @@ -0,0 +1,67 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + + +/*! \file fdmndimblackscholesop.hpp +*/ + +#ifndef quantlib_fdm_ndim_black_scholes_op_hpp +#define quantlib_fdm_ndim_black_scholes_op_hpp + +#include +#include +#include + +namespace QuantLib { + + class FdmMesher; + class FdmBlackScholesOp; + class LocalVolTermStructure; + class GeneralizedBlackScholesProcess; + + class FdmndimBlackScholesOp : public FdmLinearOpComposite { + public: + FdmndimBlackScholesOp( + ext::shared_ptr mesher, + std::vector > processes, + Matrix correlation, + Time maturity); + + Size size() const override; + void setTime(Time t1, Time t2) override; + Array apply(const Array& x) const override; + Array apply_mixed(const Array& x) const override; + + Array apply_direction(Size direction, const Array& x) const override; + + Array solve_splitting(Size direction, const Array& x, Real s) const override; + Array preconditioner(const Array& r, Real s) const override; + + std::vector toMatrixDecomp() const override; + + private: + const ext::shared_ptr mesher_; + const std::vector > processes_; + + Real currentForwardRate_; + std::vector > ops_; + std::vector > corrMaps_; + }; +} +#endif diff --git a/ql/pricingengines/basket/denglizhouspreadengine.cpp b/ql/pricingengines/basket/denglizhouspreadengine.cpp new file mode 100644 index 00000000000..d5052d92a92 --- /dev/null +++ b/ql/pricingengines/basket/denglizhouspreadengine.cpp @@ -0,0 +1,141 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include +#include +#include +#include +#include +#include "ql/pricingengines/basket/denglizhouspreadengine.hpp" + +#include + +namespace QuantLib { + + DengLiZhouSpreadEngine::DengLiZhouSpreadEngine( + std::vector > processes, + Matrix rho) + : processes_(std::move(processes)), + rho_(std::move(rho)) { + + QL_REQUIRE(!processes_.empty(), "No Black-Scholes process is given."); + QL_REQUIRE(processes_.size() == rho_.size1() && rho_.size1() == rho_.size2(), + "process and correlation matrix must have the same size."); + + for (Size i=0; i < processes_.size(); ++i) + registerWith(processes_[i]); + } + + void DengLiZhouSpreadEngine::calculate() const { + const ext::shared_ptr exercise = + ext::dynamic_pointer_cast(arguments_.exercise); + QL_REQUIRE(exercise, "not an European exercise"); + + const ext::shared_ptr avgPayoff = + ext::dynamic_pointer_cast(arguments_.payoff); + QL_REQUIRE(avgPayoff, " average basket payoff expected"); + + const ext::shared_ptr payoff = + ext::dynamic_pointer_cast(avgPayoff->basePayoff()); + QL_REQUIRE(payoff, "non-plain vanilla payoff given"); + + const Real strike = payoff->strike(); + const Option::Type optionType = payoff->optionType(); + + const Date maturityDate = exercise->lastDate(); + const Time T = processes_[0]->time(maturityDate); + + const auto extractProcesses = + [this](const std::function& f) + -> Array { + + Array x(processes_.size()); + for (Size i=0; i < x.size(); ++i) + x[i] = f(processes_[i]); + + return x; + }; + + const Array dr = extractProcesses( + [maturityDate](const auto& p) -> DiscountFactor { + return p->riskFreeRate()->discount(maturityDate); + } + ); + + QL_REQUIRE( + std::equal( + dr.begin()+1, dr.end(), dr.begin(), + std::pointer_to_binary_function(close_enough) + ), + "interest rates need to be the same for all underlyings" + ); + + const Array s = extractProcesses([](const auto& p) -> Real { return p->x0(); }); + + const Array dq = extractProcesses( + [maturityDate](const auto& p) -> DiscountFactor { + return p->dividendYield()->discount(maturityDate); + } + ); + const Array v = extractProcesses( + [maturityDate](const auto& p) -> Volatility { + return p->blackVolatility()->blackVariance(maturityDate, p->x0()); + } + ); + + results_.value = + DengLiZhouSpreadEngine::calculate_vanilla_call(s, dr, dq, v, rho_, strike); + } + + Real DengLiZhouSpreadEngine::calculate_vanilla_call( + const Array& s, const Array& dr, const Array& dq, + const Array& v, const Matrix& rho, Real K) { + + const Array mu = Log(s*dq/dr[0]) - 0.5 * v; + const Array nu = Sqrt(v); + + const Real R = std::accumulate( + mu.begin()+1, mu.end(), Real(0), + [](Real a, Real b) -> Real { return a + std::exp(b); } + ); + + const Size N = s.size()-1; + + Matrix sig11(N, N); + for (Size i=0; i < N; ++i) + std::copy(rho.row_begin(i+1)+1, rho.row_end(i+1), sig11.row_begin(i)); + + const Matrix c = CholeskyDecomposition(sig11, true); + + + const Matrix sq_sig11 = pseudoSqrt(sig11, SalvagingAlgorithm::Principal); + + //const Real a = -0.5/std::sqrt( sig11) + Matrix E(N, N); + for (Size i=1; i <= N; ++i) + for (Size j=i; j <= N; ++j) + E(i-1, j-1) = E(j-1, i-1) = + - nu[i]*nu[j]*std::exp(mu[i]+mu[j])/(nu[0]*squared(R + K)) + + ((i==j)? squared(nu[j])*std::exp(mu[j])/(nu[0]*(R+K)) : 0.0); + + std::cout << std::setprecision(16) << E << std::endl + << sq_sig11 << std::endl; + return 1.0; + } +} diff --git a/ql/pricingengines/basket/denglizhouspreadengine.hpp b/ql/pricingengines/basket/denglizhouspreadengine.hpp new file mode 100644 index 00000000000..94b768bdbd3 --- /dev/null +++ b/ql/pricingengines/basket/denglizhouspreadengine.hpp @@ -0,0 +1,61 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file denglizhoubasketengine.hpp + \brief Deng, Li and Zhou: Closed-Form Approximation for Spread option pricing +*/ + +#ifndef quantlib_deng_li_zhou_spread_engine_hpp +#define quantlib_deng_li_zhou_spread_engine_hpp + +#include +#include + +namespace QuantLib { + + //! Pricing engine for basket option on two futures + /*! This class implements formulae from + "Multi-asset Spread Option Pricing and Hedging", + S. Deng, M. Li, J.Zhou, + https://mpra.ub.uni-muenchen.de/8259/1/MPRA_paper_8259.pdf + Typo in formula (37) for J^2 is corrected. + \ingroup basketengines + + \test the correctness of the returned value is tested by + reproducing results available in literature. + */ + class DengLiZhouSpreadEngine : public BasketOption::engine { + public: + DengLiZhouSpreadEngine( + std::vector > processes, + Matrix rho); + + void calculate() const override; + + static Real calculate_vanilla_call( + const Array& s, const Array& dr, const Array& dq, + const Array& v, const Matrix& rho, Time T); + + private: + const std::vector > processes_; + const Matrix rho_; + }; +} + +#endif diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp new file mode 100644 index 00000000000..242b9d65b34 --- /dev/null +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -0,0 +1,119 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +namespace QuantLib { + + FdndimBlackScholesVanillaEngine::FdndimBlackScholesVanillaEngine( + std::vector > processes, + Matrix correlation, + std::vector xGrids, + Size tGrid, Size dampingSteps, + const FdmSchemeDesc& schemeDesc, + bool localVol, + Real illegalLocalVolOverwrite) + : processes_(std::move(processes)), + correlation_(std::move(correlation)), + xGrids_(std::move(xGrids)), + tGrid_(tGrid), + dampingSteps_(dampingSteps), + schemeDesc_(schemeDesc), + localVol_(localVol), + illegalLocalVolOverwrite_(illegalLocalVolOverwrite) { + + QL_REQUIRE(!processes_.empty(), "no Black-Scholes process is given."); + QL_REQUIRE(correlation_.size1() == correlation_.size2() + && correlation_.size1() == processes_.size(), + "correlation matrix has the wrong size."); + QL_REQUIRE(xGrids_.size() == processes_.size(), + "wrong number of xGrids is given."); + } + + + void FdndimBlackScholesVanillaEngine::calculate() const { + const Time maturity = processes_[0]->time(arguments_.exercise->lastDate()); + + std::vector > meshers; + for (Size i=0; i < processes_.size(); ++i) { + const auto process = processes_[i]; + + meshers.push_back( + ext::make_shared( + xGrids_[i], process, maturity, process->x0(), + Null(), Null(), 0.0001, 1.5, + std::pair(process->x0(), 0.1) + ) + ); + } + const auto mesher = ext::make_shared(meshers); + + const auto payoff + = ext::dynamic_pointer_cast(arguments_.payoff); + const auto calculator + = ext::make_shared(payoff, mesher); + + const auto conditions + = FdmStepConditionComposite::vanillaComposite( + DividendSchedule(), arguments_.exercise, + mesher, calculator, + processes_[0]->riskFreeRate()->referenceDate(), + processes_[0]->riskFreeRate()->dayCounter()); + + const FdmBoundaryConditionSet boundaries; + const FdmSolverDesc solverDesc + = { mesher, boundaries, conditions, calculator, + maturity, tGrid_, dampingSteps_ }; + + const auto op = ext::make_shared( + mesher, processes_, correlation_, maturity + ); + + std::vector logX; + for (const auto& p: processes_) + logX.push_back(std::log(p->x0())); + + switch(processes_.size()) { + #define PDE_MAX_SUPPORTED_DIM 6 + + #define BOOST_PP_LOCAL_MACRO(n) \ + case n : \ + results_.value = ext::make_shared>( \ + solverDesc, schemeDesc_, op)->interpolateAt(logX); \ + break; + #define BOOST_PP_LOCAL_LIMITS (1, PDE_MAX_SUPPORTED_DIM) + #include BOOST_PP_LOCAL_ITERATE() + default: + QL_FAIL("This engine does not support " << processes_.size() << " underlyings. " + << "Max number of underlyings is " << PDE_MAX_SUPPORTED_DIM << ". " + << "Change preprocessor constant PDE_MAX_SUPPORTED_DIM and recompile " + << "if a large number of underlyings is needed."); + } + } +} diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp new file mode 100644 index 00000000000..f973641c49a --- /dev/null +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp @@ -0,0 +1,67 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file fdndimblackscholesvanillaengine.hpp + \brief Finite-Differences n-dimensional Black-Scholes vanilla option engine +*/ + +#ifndef quantlib_fd_ndim_black_scholes_vanilla_engine_hpp +#define quantlib_fd_ndim_black_scholes_vanilla_engine_hpp + +#include +#include +#include +#include +#include + +namespace QuantLib { + + //! n-dimensional dimensional finite-differences Black Scholes vanilla option engine + + /*! \ingroup basketengines + + \test the correctness of the returned value is tested by + reproducing results available in web/literature + and comparison with the PyFENG python package. + */ + class FdndimBlackScholesVanillaEngine : public BasketOption::engine { + public: + FdndimBlackScholesVanillaEngine( + std::vector > processes, + Matrix correlation, + std::vector xGrids, + Size tGrid = 50, Size dampingSteps = 0, + const FdmSchemeDesc& schemeDesc = FdmSchemeDesc::Hundsdorfer(), + bool localVol = false, + Real illegalLocalVolOverwrite = -Null()); + + void calculate() const override; + + private: + const std::vector > processes_; + const Matrix correlation_; + const std::vector xGrids_; + const Size tGrid_, dampingSteps_; + const FdmSchemeDesc schemeDesc_; + const bool localVol_; + const Real illegalLocalVolOverwrite_; + }; +} + +#endif diff --git a/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp b/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp index 0726329ca4f..ac1dfc58711 100644 --- a/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp @@ -41,12 +41,9 @@ namespace QuantLib { } void SpreadBlackScholesVanillaEngine::calculate() const { - QL_REQUIRE(arguments_.exercise->type() == Exercise::European, - "not a European option"); - const ext::shared_ptr exercise = ext::dynamic_pointer_cast(arguments_.exercise); - QL_REQUIRE(exercise, "not a European exercise"); + QL_REQUIRE(exercise, "not an European exercise"); const ext::shared_ptr spreadPayoff = ext::dynamic_pointer_cast(arguments_.payoff); diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 2a86c76ce82..cca9226571f 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -26,10 +26,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -1095,7 +1097,7 @@ BOOST_AUTO_TEST_CASE(testBjerksundStenslandSpreadEngine) { callOption.setPricingEngine(engine); const Real callNPV = callOption.NPV(); - // reference value was calculated with python packages pyfeng 0.2.6 + // reference value was calculated with python packages PyFENG 0.2.6 const Real expectedPutNPV = 17.850835947276213; BasketOption putOption(ext::make_shared( @@ -1305,12 +1307,225 @@ BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { << "\n stdev : " << statOs.standardDeviation() << "\n tolerance : " << 0.02); } - std::cout << std::setprecision(18) - << statKirk.standardDeviation() << " " - << statBS2014.standardDeviation() << " " - << statOs.standardDeviation() << std::endl; } +BOOST_AUTO_TEST_CASE(testNdimPDEvs2dimPDE) { + BOOST_TEST_MESSAGE("Testing n-dimensional PDE engine vs two dimensional engine..."); + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(25, February, 2024); + const Date maturity = today + Period(6, Months); + + const ext::shared_ptr s1 = ext::make_shared(110); + const ext::shared_ptr s2 = ext::make_shared(100); + + const Handle rTS + = Handle(flatRate(today, 0.1, dc)); + + const ext::shared_ptr v1 = ext::make_shared(0.5); + const ext::shared_ptr v2 = ext::make_shared(0.3); + + const ext::shared_ptr p1 = + ext::make_shared( + Handle(s1), rTS, + Handle(flatVol(today, v1, dc)) + ); + + const Handle qTS + = Handle(flatRate(today, 0.075, dc)); + + const ext::shared_ptr p2 = + ext::make_shared( + Handle(s2), qTS, rTS, + Handle(flatVol(today, v2, dc)) + ); + + const Real rho = 0.75; + + const ext::shared_ptr twoDimEngine + = ext::make_shared(p1, p2, rho, 25, 25, 15); + + const ext::shared_ptr nDimEngine + = ext::make_shared( + std::vector >({p1, p2}), + Matrix({{1, rho}, {rho, 1}}), + std::vector({25, 25}), 15 + ); + + const Real tol = 1e-4; + for (const auto& exercise: std::vector >( + { ext::make_shared(maturity), + ext::make_shared(today, maturity)})) { + + for (Option::Type type: {Option::Call, Option::Put}) { + BasketOption option( + ext::make_shared( + ext::make_shared(type, 5) + ), + exercise + ); + + option.setPricingEngine(twoDimEngine); + const Real sb2dNPV = option.NPV(); + + option.setPricingEngine(nDimEngine); + const Real sbndNPV = option.NPV(); + + if (std::abs(sb2dNPV - sbndNPV) > tol) { + BOOST_FAIL("failed to reproduce spread option prices" + " with multidimensional PDE engine." + << std::fixed << std::setprecision(5) + << "\n calculated: " << sbndNPV + << "\n expected: " << sb2dNPV + << "\n diff: " << std::abs(sb2dNPV - sbndNPV) + << "\n tolerance : " << tol); + + } + + BasketOption avgOption( + ext::make_shared( + ext::make_shared(type, 200), Array({1.5, 0.5}) + ), + exercise + ); + avgOption.setPricingEngine(twoDimEngine); + const Real avg2dNPV = avgOption.NPV(); + + avgOption.setPricingEngine(nDimEngine); + const Real avgndNPV = avgOption.NPV(); + std::cout << avg2dNPV << " " << avgndNPV << std::endl; + + if (std::abs(avg2dNPV - avgndNPV) > tol) { + BOOST_FAIL("failed to reproduce average option prices" + " with multidimensional PDE engine." + << std::fixed << std::setprecision(5) + << "\n calculated: " << avgndNPV + << "\n expected: " << avg2dNPV + << "\n diff: " << std::abs(avg2dNPV - avgndNPV) + << "\n tolerance : " << tol); + } + } + } +} + +BOOST_AUTO_TEST_CASE(testNdimPDEinDifferentDims) { + BOOST_TEST_MESSAGE("Testing n-dimensional PDE engine in different dimensions..."); + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(25, February, 2024); + const Date maturity = today + Period(6, Months); + + const std::vector underlyings({100, 50, 75, 120}); + const std::vector volatilities({0.3, 0.2, 0.6, 0.4}); + + Real strike = 5.0; + + const Handle rTS = Handle( + flatRate(today, 0.05, dc)); + const ext::shared_ptr exercise + = ext::make_shared(maturity); + + const std::vector expected = {7.38391, 9.84887, 19.96825, 30.87275}; + + std::vector > processes; + for (Size d=1; d <= 4; ++d) { + processes.push_back( + ext::make_shared( + Handle(ext::make_shared(underlyings[d-1])), + rTS, + Handle(flatVol(today, volatilities[d-1], dc)) + ) + ); + + strike += underlyings[d-1]; + + Matrix rho(d, d); + for (Size i=0; i < d; ++i) + for (Size j=0; j < d; ++j) + rho(i, j) = std::exp(-0.5*std::abs(Real(i-j))); + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Call, strike), + Array(d, 1.0) + ), + exercise + ); + + option.setPricingEngine( + ext::make_shared( + processes, rho, std::vector(d, 20), 7 + ) + ); + + const Real calculated = option.NPV(); + const Real diff = std::abs(calculated - expected[d-1]); + const Real tol = 0.047; + + if (diff > tol) { + BOOST_FAIL("failed to reproduce precalculated " << d << "-dim option price" + << std::fixed << std::setprecision(5) + << "\n calculated: " << calculated + << "\n expected: " << expected[d-1] + << "\n diff: " << diff + << "\n tolerance : " << tol); + } + } +} + +BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { + const DayCounter dc = Actual365Fixed(); + const Date today = Date(25, March, 2024); + const Date maturity = today + Period(6, Months); + + const std::vector underlyings({200, 50, 55, 110}); + const std::vector volatilities({0.3, 0.2, 0.6, 0.4}); + const std::vector q({0.04, 0.075, 0.05, 0.08}); + + const Handle rTS = Handle( + flatRate(today, 0.05, dc)); + const ext::shared_ptr exercise = ext::make_shared(maturity); + + std::vector > processes; + for (Size d=0; d < 4; ++d) + processes.push_back( + ext::make_shared( + Handle(ext::make_shared(underlyings[d])), + Handle(flatRate(today, q[d], dc)), rTS, + Handle(flatVol(today, volatilities[d], dc)) + ) + ); + + Matrix rho(4, 4); + for (Size i=0; i < 4; ++i) + for (Size j=i; j < 4; ++j) + rho[i][j] = rho[j][i] = + std::exp(-0.5*std::abs(Real(i)-Real(j)) - ((i!=j) ? 0.02*(i+j): 0.0)); + + const Real strike = 5.0; + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Call, strike), + Array({1.0, -1.0, -1.0, -1.0}) + ), + exercise + ); + + option.setPricingEngine( + ext::make_shared(processes, rho) + ); + std::cout << option.NPV() << std::endl; + + option.setPricingEngine( + ext::make_shared( + processes, rho, std::vector(4, 20), 10 + ) + ); + std::cout << option.NPV() << std::endl; +} + + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/matrices.cpp b/test-suite/matrices.cpp index de193b8284f..ee38acc0f1b 100644 --- a/test-suite/matrices.cpp +++ b/test-suite/matrices.cpp @@ -775,15 +775,20 @@ BOOST_AUTO_TEST_CASE(testSparseMatrixMemory) { } -#define QL_CHECK_CLOSE_MATRIX(actual, expected) \ +#define QL_CHECK_CLOSE_MATRIX_TOL(actual, expected, tol) \ BOOST_REQUIRE(actual.rows() == expected.rows() && \ actual.columns() == expected.columns()); \ for (auto i = 0u; i < actual.rows(); i++) { \ for (auto j = 0u; j < actual.columns(); j++) { \ - QL_CHECK_CLOSE(actual(i, j), expected(i, j), 100 * QL_EPSILON); \ + QL_CHECK_CLOSE(actual(i, j), expected(i, j), tol); \ } \ } \ + +#define QL_CHECK_CLOSE_MATRIX(actual, expected) \ + QL_CHECK_CLOSE_MATRIX_TOL(actual, expected, 100 * QL_EPSILON) \ + + BOOST_AUTO_TEST_CASE(testOperators) { BOOST_TEST_MESSAGE("Testing matrix operators..."); @@ -842,6 +847,57 @@ BOOST_AUTO_TEST_CASE(testOperators) { QL_CHECK_CLOSE_MATRIX(rvalue_real_quotient, scalar_quotient); } +namespace MatrixTests { + Matrix createTestCorrelationMatrix(Size n) { + Matrix rho(n, n); + for (Size i=0; i < n; ++i) + for (Size j=i; j < n; ++j) + rho[i][j] = rho[j][i] = + std::exp(-0.1*std::abs(Real(i)-Real(j)) - ((i!=j) ? 0.02*(i+j): 0.0)); + + return rho; + } +} + +BOOST_AUTO_TEST_CASE(testPrincipalMatrixSqrt) { + BOOST_TEST_MESSAGE("Testing principal matrix pseudo sqrt..."); + + std::vector dims = {1, 4, 10, 40}; + for (auto n: dims) { + const Matrix rho = MatrixTests::createTestCorrelationMatrix(n); + const Matrix sqrtRho = pseudoSqrt(rho, SalvagingAlgorithm::Principal); + + // matrix is symmetric + QL_CHECK_CLOSE_MATRIX_TOL(sqrtRho, transpose(sqrtRho), 1e3*QL_EPSILON); + + // matrix is square root of original matrix + QL_CHECK_CLOSE_MATRIX_TOL((sqrtRho*sqrtRho), rho, 1e5*QL_EPSILON); + } +} + + +BOOST_AUTO_TEST_CASE(testCholeskySolverFor) { + BOOST_TEST_MESSAGE("Testing CholeskySolverFor..."); + + MersenneTwisterUniformRng rng(1234); + + std::vector dims = {1, 4, 10, 25, 50}; + for (auto n: dims) { + + Array b(n); + for (Size i=0; i < n; ++i) + b[i] = rng.nextReal(); + + const Matrix rho = MatrixTests::createTestCorrelationMatrix(n); + const Array x = CholeskySolveFor(CholeskyDecomposition(rho), b); + + const Array diff = Abs(rho*x - b); + + BOOST_CHECK_SMALL(std::sqrt(DotProduct(diff, diff)), 20*std::sqrt(n)*QL_EPSILON); + } +} + + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From a6281431f26de47f02dfed844fa276823d6e7456 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 22 Apr 2024 19:58:02 +0200 Subject: [PATCH 04/36] Merge remote-tracking branch 'origin/spread_option' into spread_option --- .../basket/denglizhouspreadengine.cpp | 66 +++++++++++++++---- test-suite/matrices.cpp | 14 ++++ 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/ql/pricingengines/basket/denglizhouspreadengine.cpp b/ql/pricingengines/basket/denglizhouspreadengine.cpp index d5052d92a92..4aa704feda3 100644 --- a/ql/pricingengines/basket/denglizhouspreadengine.cpp +++ b/ql/pricingengines/basket/denglizhouspreadengine.cpp @@ -66,8 +66,7 @@ namespace QuantLib { -> Array { Array x(processes_.size()); - for (Size i=0; i < x.size(); ++i) - x[i] = f(processes_[i]); + std::transform(processes_.begin(), processes_.end(), x.begin(), f); return x; }; @@ -100,14 +99,14 @@ namespace QuantLib { ); results_.value = - DengLiZhouSpreadEngine::calculate_vanilla_call(s, dr, dq, v, rho_, strike); + DengLiZhouSpreadEngine::calculate_vanilla_call(Log(s), dr, dq, v, rho_, strike); } Real DengLiZhouSpreadEngine::calculate_vanilla_call( - const Array& s, const Array& dr, const Array& dq, + const Array& x, const Array& dr, const Array& dq, const Array& v, const Matrix& rho, Real K) { - const Array mu = Log(s*dq/dr[0]) - 0.5 * v; + const Array mu = x + Log(dq/dr[0]) - 0.5 * v; const Array nu = Sqrt(v); const Real R = std::accumulate( @@ -115,27 +114,68 @@ namespace QuantLib { [](Real a, Real b) -> Real { return a + std::exp(b); } ); - const Size N = s.size()-1; + const Size N = x.size()-1; Matrix sig11(N, N); for (Size i=0; i < N; ++i) std::copy(rho.row_begin(i+1)+1, rho.row_end(i+1), sig11.row_begin(i)); + const Array sig10(rho.row_begin(0)+1, rho.row_end(0)); - const Matrix c = CholeskyDecomposition(sig11, true); + const Matrix sqSig11 = pseudoSqrt(sig11, SalvagingAlgorithm::Principal); + const Array sig11Inv10 = CholeskySolveFor(CholeskyDecomposition(sig11), sig10); - const Matrix sq_sig11 = pseudoSqrt(sig11, SalvagingAlgorithm::Principal); + const Real sqSig_xy = std::sqrt(1.0 - DotProduct(sig10, sig11Inv10)); - //const Real a = -0.5/std::sqrt( sig11) + const Real a = -0.5/sqSig_xy; Matrix E(N, N); for (Size i=1; i <= N; ++i) for (Size j=i; j <= N; ++j) E(i-1, j-1) = E(j-1, i-1) = - - nu[i]*nu[j]*std::exp(mu[i]+mu[j])/(nu[0]*squared(R + K)) - + ((i==j)? squared(nu[j])*std::exp(mu[j])/(nu[0]*(R+K)) : 0.0); + a*(((i==j)? squared(nu[j])*std::exp(mu[j])/(nu[0]*(R+K)) : 0.0) + -nu[i]*nu[j]*std::exp(mu[i]+mu[j])/(nu[0]*squared(R + K)) ); + + const Matrix F = sqSig11*E*sqSig11; + + Real trF(0), trF2(0); + for (Size i=0; i < N; ++i) { + trF += F[i][i]; + trF2 += squared(F[i][i]) + + 2.0*std::accumulate( + F.row_begin(i)+i+1, F.row_end(i), Real(0), + [](Real a, Real b) -> Real {return a+b*b;} + ); + } + + const Real c = -(std::log(R + K) - mu[0])/(nu[0]*sqSig_xy); + + const Array d = (sig11Inv10 + - Exp(Array(mu.begin()+1, mu.end()))*Array(nu.begin()+1,nu.end())/(nu[0]*(R+K)))/sqSig_xy; + + const Array Esig10 = E*sig10; + const Matrix Esig11 = E*sig11; + const Array sig11d = sig11*d; + + Array C(N+2); + C[0] = c + trF + nu[0]*sqSig_xy + nu[0]*DotProduct(sig10, d) + + squared(nu[0])*DotProduct(sig10, Esig10); + C[N+1] = c + trF; + + for (Size k=1; k < N+1; ++k) + C[k] = c + trF + nu[k]*sig11d[k-1] + squared(nu[k]) + * std::inner_product(sig11.row_begin(k-1), sig11.row_end(k-1), + Esig11.column_begin(k-1), 0.0); + + std::vector D(N+2); + D[0] = sqSig11*(d + 2*nu[0]*Esig10); + D[N+1] = sqSig11*d; + for (Size k=1; k < N+1; ++k) + D[k] = sqSig11*(d + 2*nu[k]*Array(Esig11.column_begin(k-1), Esig11.column_end(k-1))); + + std::cout << std::setprecision(16); + for (Size i=0; i < N+2; ++i) + std::cout << D[i] << std::endl; - std::cout << std::setprecision(16) << E << std::endl - << sq_sig11 << std::endl; return 1.0; } } diff --git a/test-suite/matrices.cpp b/test-suite/matrices.cpp index ee38acc0f1b..54e9937ba1b 100644 --- a/test-suite/matrices.cpp +++ b/test-suite/matrices.cpp @@ -898,6 +898,20 @@ BOOST_AUTO_TEST_CASE(testCholeskySolverFor) { } +BOOST_AUTO_TEST_CASE(testCholeskySolverForIncomplete) { + BOOST_TEST_MESSAGE("Testing CholeskySolverFor with incomplete matrix..."); + + const Size n = 4; + + Matrix rho(n, n, 0.0); + rho[0][0] = rho[1][1] = Real(1); + rho[0][1] = rho[1][0] = 0.9; + + const Matrix L = CholeskyDecomposition(rho, true); + QL_CHECK_CLOSE_MATRIX((L*transpose(L)), rho); +} + + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From e525781aa51d844ab196b3035e0422d51cf767fe Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sun, 2 Jun 2024 10:35:38 +0200 Subject: [PATCH 05/36] first version ready --- ql/instruments/basketoption.hpp | 2 + .../basket/denglizhouspreadengine.cpp | 197 +++++++++++++++--- .../basket/denglizhouspreadengine.hpp | 26 ++- test-suite/basketoption.cpp | 89 ++++++-- 4 files changed, 267 insertions(+), 47 deletions(-) diff --git a/ql/instruments/basketoption.hpp b/ql/instruments/basketoption.hpp index 0cac3b88109..7d20e8aa1d4 100644 --- a/ql/instruments/basketoption.hpp +++ b/ql/instruments/basketoption.hpp @@ -80,6 +80,8 @@ namespace QuantLib { a.begin(), Real(0.0)); } + Array weights() const { return weights_; } + private: Array weights_; }; diff --git a/ql/pricingengines/basket/denglizhouspreadengine.cpp b/ql/pricingengines/basket/denglizhouspreadengine.cpp index 4aa704feda3..97c032badc4 100644 --- a/ql/pricingengines/basket/denglizhouspreadengine.cpp +++ b/ql/pricingengines/basket/denglizhouspreadengine.cpp @@ -20,25 +20,25 @@ #include #include #include +#include #include #include #include "ql/pricingengines/basket/denglizhouspreadengine.hpp" -#include - namespace QuantLib { DengLiZhouSpreadEngine::DengLiZhouSpreadEngine( std::vector > processes, Matrix rho) - : processes_(std::move(processes)), + : n_(processes.size()), + processes_(std::move(processes)), rho_(std::move(rho)) { - QL_REQUIRE(!processes_.empty(), "No Black-Scholes process is given."); - QL_REQUIRE(processes_.size() == rho_.size1() && rho_.size1() == rho_.size2(), + QL_REQUIRE(n_ > 0, "No Black-Scholes process is given."); + QL_REQUIRE(n_ == rho_.size1() && rho_.size1() == rho_.size2(), "process and correlation matrix must have the same size."); - for (Size i=0; i < processes_.size(); ++i) + for (Size i=0; i < n_; ++i) registerWith(processes_[i]); } @@ -46,26 +46,13 @@ namespace QuantLib { const ext::shared_ptr exercise = ext::dynamic_pointer_cast(arguments_.exercise); QL_REQUIRE(exercise, "not an European exercise"); - - const ext::shared_ptr avgPayoff = - ext::dynamic_pointer_cast(arguments_.payoff); - QL_REQUIRE(avgPayoff, " average basket payoff expected"); - - const ext::shared_ptr payoff = - ext::dynamic_pointer_cast(avgPayoff->basePayoff()); - QL_REQUIRE(payoff, "non-plain vanilla payoff given"); - - const Real strike = payoff->strike(); - const Option::Type optionType = payoff->optionType(); - const Date maturityDate = exercise->lastDate(); - const Time T = processes_[0]->time(maturityDate); const auto extractProcesses = [this](const std::function& f) -> Array { - Array x(processes_.size()); + Array x(n_); std::transform(processes_.begin(), processes_.end(), x.begin(), f); return x; @@ -84,6 +71,7 @@ namespace QuantLib { ), "interest rates need to be the same for all underlyings" ); + const DiscountFactor dr0 = dr[0]; const Array s = extractProcesses([](const auto& p) -> Real { return p->x0(); }); @@ -98,15 +86,155 @@ namespace QuantLib { } ); - results_.value = - DengLiZhouSpreadEngine::calculate_vanilla_call(Log(s), dr, dq, v, rho_, strike); + const ext::shared_ptr avgPayoff = + ext::dynamic_pointer_cast(arguments_.payoff); + QL_REQUIRE(avgPayoff, "average basket payoff expected"); + + // sort assets by their weight + const Array weights = avgPayoff->weights(); + QL_REQUIRE(n_ == weights.size() && n_ > 1, + "wrong number of weights arguments in payoff"); + + std::vector< std::tuple > p; + p.reserve(n_); + + for (Size i=0; i < n_; ++i) + p.emplace_back(std::make_tuple(weights[i], i, s[i], dq[i], v[i])); + + const ext::shared_ptr payoff = + ext::dynamic_pointer_cast(avgPayoff->basePayoff()); + QL_REQUIRE(payoff, "non-plain vanilla payoff given"); + + Matrix rho; + if (payoff->strike() < 0.0) { + p.emplace_back(std::make_tuple(1.0, n_, -payoff->strike(), dr0, 0.0)); + rho = Matrix(n_+1, n_+1); + for (Size i=0; i < n_; ++i) { + std::copy(rho_.row_begin(i), rho_.row_end(i), rho.row_begin(i)); + rho[n_][i] = rho[i][n_] = 0.0; + } + rho[n_][n_] = 1.0; + } + else + rho = rho_; + + const Real strike = std::max(0.0, payoff->strike()); + + // positive weights first + std::sort(p.begin(), p.end(), std::greater<>()); + + const Size M = std::distance( + p.begin(), + std::lower_bound(p.begin(), p.end(), Real(0), + [](const auto& p, const Real& value) -> bool { return std::get<0>(p) > value;} ) + ); + + QL_REQUIRE(M > 0, "at least one positive asset weight must be given"); + QL_REQUIRE(M < p.size(), "at least one negative asset weight must be given"); + + const Size N = p.size() - M; + + Matrix nRho(N+1, N+1); + Array _s(N+1), _dq(N+1), _v(N+1); + + if (M > 1) { + Array F(M), vol(M); + for (Size i=0; i < M; ++i) { + vol[i] = std::sqrt(std::get<4>(p[i])); + F[i] = std::get<0>(p[i])*std::get<2>(p[i])*std::get<3>(p[i])/dr0; + } + + const Real S0 = std::accumulate( + p.begin(), p.begin()+M, Real(0.0), + [](const Real& value, const auto& p) -> Real { + return value + std::get<0>(p)*std::get<2>(p); + }); + const Real F0 = std::accumulate(F.begin(), F.end(), 0.0); + const DiscountFactor dq_S0 = F0/S0*dr0; + + Real v_s = 0.0; + for (Size i=0; i < M; ++i) + for (Size j=0; j < M; ++j) + v_s += vol[i]*vol[j]*F[i]*F[j] + *rho[std::get<1>(p[i])][std::get<1>(p[j])]; + + v_s /= F0*F0; + _s[0] = S0; _dq[0] = dq_S0; _v[0] = v_s; + + nRho[0][0] = 1.0; + + for (Size i=0; i < N; ++i) { + Real rhoHat = 0.0; + for (Size j=0; j < M; ++j) + rhoHat += rho[std::get<1>(p[M+i])][std::get<1>(p[j])]*vol[j]*F[j]; + + nRho[i+1][0] = nRho[0][i+1] + = std::min(1.0, std::max(-1.0, rhoHat/(std::sqrt(v_s)*F0))); + } + } + else { + _s[0] = std::abs(std::get<0>(p[0])*std::get<2>(p[0])); + _dq[0] = std::get<3>(p[0]); + _v[0] = std::get<4>(p[0]); + for (Size i=0; i < N+1; ++i) + nRho[0][i] = nRho[i][0] = rho[std::get<1>(p[i])][std::get<1>(p[0])]; + } + + for (Size i=0; i < N; ++i) { + _s[i+1] = std::abs(std::get<0>(p[M+i])*std::get<2>(p[M+i])); + _dq[i+1] = std::get<3>(p[M+i]); + _v[i+1] = std::get<4>(p[M+i]); + + const Size idx = std::get<1>(p[M+i]); + for (Size j=0; j < N; ++j) + nRho[i+1][j+1] = rho[idx][std::get<1>(p[M+j])]; + } + + //std::cout << N << std::endl << _s << std::endl << _dq << std::endl + // << _v << std::endl << nRho << std::endl << strike << std::endl << std::endl; + + const Real callValue + = DengLiZhouSpreadEngine::calculate_vanilla_call(Log(_s), dr0, _dq, _v, nRho, strike); + + if (payoff->optionType() == Option::Call) + results_.value = std::max(0.0, callValue); + else { + const Real fwd = _s[0]*_dq[0] - dr[0]*strike + - std::inner_product(_s.begin()+1, _s.end(), _dq.begin()+1, 0.0); + results_.value = std::max(0.0, callValue - fwd); + } + } + + Real DengLiZhouSpreadEngine::I(Real u, Real tF2, const Matrix& D, const Matrix& DF, Size i) { + const Real psi = 1.0/ + (1.0 + std::inner_product( + D.row_begin(i), D.row_end(i), D.row_begin(i), 0.0)); + const Real sqrtPsi = std::sqrt(psi); + + const Real n_uSqrtPsi = NormalDistribution()(u*sqrtPsi); + const Real J_0 = CumulativeNormalDistribution()(u*sqrtPsi); + + const Real vFv = std::inner_product( + DF.row_begin(i), DF.row_end(i), D.row_begin(i), 0.0); + const Real J_1 = psi*sqrtPsi*(psi*u*u - 1.0) * vFv * n_uSqrtPsi; + + const Real vFFv = std::inner_product( + DF.row_begin(i), DF.row_end(i), DF.row_begin(i), 0.0); + const Real J_2 = u*psi*sqrtPsi*n_uSqrtPsi*( + 2 * tF2 + + vFv*vFv*(squared(squared(psi*u)) + - 10.0*psi*psi*psi*u*u + 15*psi*psi) + + vFFv * (4*psi*psi*u*u - 12*psi) + ); + + return J_0 + J_1 - 0.5*J_2; } Real DengLiZhouSpreadEngine::calculate_vanilla_call( - const Array& x, const Array& dr, const Array& dq, + const Array& x, DiscountFactor dr, const Array& dq, const Array& v, const Matrix& rho, Real K) { - const Array mu = x + Log(dq/dr[0]) - 0.5 * v; + const Array mu = x + Log(dq/dr) - 0.5 * v; const Array nu = Sqrt(v); const Real R = std::accumulate( @@ -122,10 +250,11 @@ namespace QuantLib { const Array sig10(rho.row_begin(0)+1, rho.row_end(0)); const Matrix sqSig11 = pseudoSqrt(sig11, SalvagingAlgorithm::Principal); - const Array sig11Inv10 = CholeskySolveFor(CholeskyDecomposition(sig11), sig10); - const Real sqSig_xy = std::sqrt(1.0 - DotProduct(sig10, sig11Inv10)); + const Real sig_xy = 1.0 - DotProduct(sig10, sig11Inv10); + QL_REQUIRE(sig_xy > 0.0, "approximation loses validity"); + const Real sqSig_xy = std::sqrt(sig_xy); const Real a = -0.5/sqSig_xy; Matrix E(N, N); @@ -172,10 +301,18 @@ namespace QuantLib { for (Size k=1; k < N+1; ++k) D[k] = sqSig11*(d + 2*nu[k]*Array(Esig11.column_begin(k-1), Esig11.column_end(k-1))); - std::cout << std::setprecision(16); - for (Size i=0; i < N+2; ++i) - std::cout << D[i] << std::endl; + Matrix DM(N+2, N); + for (Size k=0; k < N+2; ++k) + std::copy(D[k].begin(), D[k].end(), DM.row_begin(k)); + + const Matrix DF = DM*F; + + Real npv = dr*std::exp(mu[0] + 0.5*squared(nu[0])) * I(C[0], trF2, DM, DF, 0) + - K*dr*I(C.back(), trF2, DM, DF, N+1); + + for (Size k=1; k <= N; ++k) + npv -= dr*std::exp(mu[k] + 0.5*squared(nu[k])) * I(C[k], trF2, DM, DF, k); - return 1.0; + return npv; } } diff --git a/ql/pricingengines/basket/denglizhouspreadengine.hpp b/ql/pricingengines/basket/denglizhouspreadengine.hpp index 94b768bdbd3..c9881730943 100644 --- a/ql/pricingengines/basket/denglizhouspreadengine.hpp +++ b/ql/pricingengines/basket/denglizhouspreadengine.hpp @@ -30,11 +30,24 @@ namespace QuantLib { //! Pricing engine for basket option on two futures - /*! This class implements formulae from + /*! This class implements the pricing formula from "Multi-asset Spread Option Pricing and Hedging", - S. Deng, M. Li, J.Zhou, + S. Deng, M. Li, J.Zhou, 2008 https://mpra.ub.uni-muenchen.de/8259/1/MPRA_paper_8259.pdf - Typo in formula (37) for J^2 is corrected. + + The Typo in formula (37) for J^2 is corrected + + This pricing formula works only if exactly one asset weight is positive. + If more than one weight is positive then a mapping of the sum of correlated + log-normal processes onto one log-normal process has to has be carried out. + This implementation is using + + "WKB Approximation for the Sum of Two Correlated Lognormal Random Variables", + C.F. Lo 2013 + https://www.m-hikari.com/ams/ams-2013/ams-125-128-2013/loAMS125-128-2013.pdf + + for this task. + \ingroup basketengines \test the correctness of the returned value is tested by @@ -48,11 +61,14 @@ namespace QuantLib { void calculate() const override; + private: static Real calculate_vanilla_call( - const Array& s, const Array& dr, const Array& dq, + const Array& s, DiscountFactor dr, const Array& dq, const Array& v, const Matrix& rho, Time T); - private: + static Real I(Real u, Real tF2, const Matrix& D, const Matrix& DF, Size i); + + const Size n_; const std::vector > processes_; const Matrix rho_; }; diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index cca9226571f..b4add203ae0 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1393,7 +1393,6 @@ BOOST_AUTO_TEST_CASE(testNdimPDEvs2dimPDE) { avgOption.setPricingEngine(nDimEngine); const Real avgndNPV = avgOption.NPV(); - std::cout << avg2dNPV << " " << avgndNPV << std::endl; if (std::abs(avg2dNPV - avgndNPV) > tol) { BOOST_FAIL("failed to reproduce average option prices" @@ -1478,9 +1477,9 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { const Date today = Date(25, March, 2024); const Date maturity = today + Period(6, Months); - const std::vector underlyings({200, 50, 55, 110}); - const std::vector volatilities({0.3, 0.2, 0.6, 0.4}); - const std::vector q({0.04, 0.075, 0.05, 0.08}); + const std::vector underlyings({50, 11, 55, 200}); + const std::vector volatilities({0.2, 0.6, 0.4, 0.3}); + const std::vector q({0.075, 0.05, 0.08, 0.04}); const Handle rTS = Handle( flatRate(today, 0.05, dc)); @@ -1506,23 +1505,89 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { BasketOption option( ext::make_shared( - ext::make_shared(Option::Call, strike), - Array({1.0, -1.0, -1.0, -1.0}) + ext::make_shared(Option::Put, strike), + Array({-1.0, -5.0, -2.0, 1.0}) ), exercise ); - option.setPricingEngine( - ext::make_shared(processes, rho) - ); - std::cout << option.NPV() << std::endl; + option.setPricingEngine(ext::make_shared(processes, rho)); + const Real calculated = option.NPV(); option.setPricingEngine( ext::make_shared( - processes, rho, std::vector(4, 20), 10 + processes, rho, std::vector(4, 30), 20 ) ); - std::cout << option.NPV() << std::endl; + const Real expected = option.NPV(); + const Real diff = std::abs(calculated - expected); + const Real tol = 0.03; + + if (diff > tol) { + BOOST_FAIL("failed to compare basket option prices" + << std::fixed << std::setprecision(5) + << "\n Deng-Li-Zhou: " << calculated + << "\n PDE: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); + } +} + + +BOOST_AUTO_TEST_CASE(testDengLiZhouWithNegativeStrike) { + const DayCounter dc = Actual365Fixed(); + const Date today = Date(27, May, 2024); + const Date maturity = today + Period(6, Months); + + const std::vector underlyings({220, 105, 45, 1e-12}); + const std::vector volatilities({0.4, 0.25, 0.3, 0.25}); + const std::vector q({0.04, 0.075, 0.05, 0.1}); + + const Handle rTS = Handle( + flatRate(today, 0.03, dc)); + const ext::shared_ptr exercise = ext::make_shared(maturity); + + std::vector > processes; + for (Size d=0; d < 4; ++d) + processes.push_back( + ext::make_shared( + Handle(ext::make_shared(underlyings[d])), + Handle(flatRate(today, q[d], dc)), rTS, + Handle(flatVol(today, volatilities[d], dc)) + ) + ); + + Matrix rho(4, 4, 0.0); + rho[0][1] = rho[1][0] = 0.8; + rho[0][2] = rho[2][0] =-0.2; + rho[1][2] = rho[2][1] = 0.3; + rho[0][0] = rho[1][1] = rho[2][2] = rho[3][3] = 1.0; + rho[1][3] = rho[3][1] = 0.3; + + const Real strike = -2.0; + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Call, strike), + Array({0.5, -2.0, 2.0, -0.75}) + ), + exercise + ); + + option.setPricingEngine(ext::make_shared(processes, rho)); + + const Real calculated = option.NPV(); + const Real expected = 3.34412; + const Real tol = 1e-5; + const Real diff = std::abs(calculated - expected); + + if (diff > tol) + BOOST_FAIL("failed to reproduce Deng-Li-Zhou value with negative strike" + << std::fixed << std::setprecision(5) + << "\n Deng-Li-Zhou: " << calculated + << "\n Expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); } From ed9ee5b22080d55fb837cbd05da447c54f6113aa Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Thu, 27 Jun 2024 23:05:09 +0200 Subject: [PATCH 06/36] second order approximation --- .../interpolations/chebyshevinterpolation.hpp | 6 +- .../basket/denglizhouspreadengine.hpp | 2 +- .../basket/operatorsplittingspreadengine.cpp | 136 ++++++++++++++-- .../basket/operatorsplittingspreadengine.hpp | 6 +- test-suite/basketoption.cpp | 152 ++++++++++++++++-- 5 files changed, 272 insertions(+), 30 deletions(-) diff --git a/ql/math/interpolations/chebyshevinterpolation.hpp b/ql/math/interpolations/chebyshevinterpolation.hpp index c0fd1586100..0525c06b858 100644 --- a/ql/math/interpolations/chebyshevinterpolation.hpp +++ b/ql/math/interpolations/chebyshevinterpolation.hpp @@ -30,8 +30,6 @@ namespace QuantLib { - class LagrangeInterpolation; - /*! References: S.A. Sarra: Chebyshev Interpolation: An Interactive Tour, https://www.maa.org/sites/default/files/images/upload_library/4/vol6/Sarra/Chebyshev.html @@ -47,6 +45,9 @@ namespace QuantLib { Size n, const ext::function& f, PointsType pointsType = SecondKind); + explicit ChebyshevInterpolation(const ChebyshevInterpolation&) = delete; + ChebyshevInterpolation& operator=(const ChebyshevInterpolation&) = delete; + void updateY(const Array& y); Array nodes() const; @@ -55,7 +56,6 @@ namespace QuantLib { private: const Array x_; Array y_; - ext::shared_ptr lagrangeInterp_; }; } diff --git a/ql/pricingengines/basket/denglizhouspreadengine.hpp b/ql/pricingengines/basket/denglizhouspreadengine.hpp index c9881730943..0148fca7132 100644 --- a/ql/pricingengines/basket/denglizhouspreadengine.hpp +++ b/ql/pricingengines/basket/denglizhouspreadengine.hpp @@ -35,7 +35,7 @@ namespace QuantLib { S. Deng, M. Li, J.Zhou, 2008 https://mpra.ub.uni-muenchen.de/8259/1/MPRA_paper_8259.pdf - The Typo in formula (37) for J^2 is corrected + The typo in formula (37) for J^2 is corrected This pricing formula works only if exactly one asset weight is positive. If more than one weight is positive then a mapping of the sum of correlated diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp index 0f6079b447f..74a81f42e32 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp @@ -1,7 +1,7 @@ /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* - Copyright (C) 2024 Klaus Spanderen + Copyright (C) 2024 klaus Spanderen This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - http://quantlib.org/ @@ -22,19 +22,30 @@ #include #include +#include + namespace QuantLib { OperatorSplittingSpreadEngine::OperatorSplittingSpreadEngine( ext::shared_ptr process1, ext::shared_ptr process2, - Real correlation) - : SpreadBlackScholesVanillaEngine(process1, process2, correlation) { + Real correlation, + Order order) + : SpreadBlackScholesVanillaEngine(process1, process2, correlation), + order_(order) { } Real OperatorSplittingSpreadEngine::calculate( Real k, Option::Type optionType, Real variance1, Real variance2, DiscountFactor df) const { + const auto callPutParityPrice = [this, df, k, optionType](Real callPrice) -> Real { + if (optionType == Option::Call) + return callPrice; + else + return callPrice - df*(f1_-f2_-k); + }; + const Real vol1 = std::sqrt(variance1); const Real vol2 = std::sqrt(variance2); const Real sig2 = vol2*f2_/(f2_+k); @@ -44,19 +55,112 @@ namespace QuantLib { const Real d2 = d1 - sig_m; const CumulativeNormalDistribution N; - const Real kirkNPV = df*(f1_*N(d1) - (f2_ + k)*N(d2)); - - const Real v = (rho_*vol1 - sig2)*vol2/(sig_m*sig_m); - const Real approx = kirkNPV - - 0.5 * sig2*sig2 * k * df * NormalDistribution()(d2) * v - *( d2*(1 - rho_*vol1/sig2) - - 0.5*sig_m * v * k / (f2_+k) - * ( d1*d2 + (1-rho_*rho_)*squared(vol1/(rho_*vol1-sig2)))); - - if (optionType == Option::Call) - return approx; - else - return approx - df*(f1_-f2_-k); + + const Real kirkCallNPV = df*(f1_*N(d1) - (f2_ + k)*N(d2)); + + const Real vv = (rho_*vol1 - sig2)*vol2/(sig_m*sig_m); + const Real oPlt = -sig2*sig2 * k * df * NormalDistribution()(d2) * vv + *( d2*(1 - rho_*vol1/sig2) + - 0.5*sig_m * vv * k / (f2_+k) + * ( d1*d2 + (1-rho_*rho_)*squared(vol1/(rho_*vol1-sig2)))); + + if (order_ == First) + return callPutParityPrice(kirkCallNPV + 0.5*oPlt); + + QL_REQUIRE(order_ == Second, "unknown approximation type"); + + /* + In the original paper the second order was calculated using numerical differentiation. + The following Mathematica scripts calculates the approximation to the n'th order. + + vol2Hat[R2_] := vol2*(R2 - K)/R2 + volMinusHat[R2_] := Sqrt[vol1^2 - 2*rho*vol1*vol2Hat[R2] + vol2Hat[R2]^2] + zeta1[R1_, R2_] := 1/(volMinusHat[R2]*Sqrt[t])*(Log[R1] + volMinusHat[R2]^2*t/2) + zeta2[R1_, R2_] := zeta1[R1, R2] - volMinusHat[R2]*Sqrt[t] + pLT[R1_, R2_] := Exp[-r*t]*R2*(R1*CDF[NormalDistribution[0, 1], zeta1[R1, R2]] + - CDF[NormalDistribution[0, 1], zeta2[R1, R2]]) + opt[R1_, R2_] := (1/2*vol2Hat[R2]^2*R2^2*D[#, {R2, 2}] + (rho*vol1 - vol2Hat[R2])*vol2Hat[R2]*R1*R2* + D[#, R1, R2] - (rho*vol1 - vol2Hat[R2])*vol2Hat[R2]*R1*D[#, R1]) & + + pStrange1[R1_, R2_] := pLT[R1, R2] + (t/2)^1/Factorial[1]*opt[R1, R2][pLT[R1, R2]] + pStrange2[R1_, R2_] := pStrange1[R1, R2] + (t/2)^2/Factorial[2]*opt[R1, R2][opt[R1, R2][pLT[R1, R2]]] + */ + + const Real R2 = f2_+k; + const Real R1 = f1_/R2; + const Real F2 = f2_; + + const Real F22 = F2*F2; + const Real F23 = F22*F2; + const Real F24 = F22*F22; + + const Real iR2 = 1.0/R2; + const Real iR22 = iR2*iR2; + const Real iR23 = iR22*iR2; + const Real iR24 = iR22*iR22; + const Real vol12 = vol1*vol1; + const Real vol22 = vol2*vol2; + const Real vol23 = vol22*vol2; + const Real a = vol12 - 2*F2*iR2*rho_*vol1*vol2 + F22*iR22*vol22; + const Real a2 = a*a; + const Real b = a/2+std::log(R1); + const Real b2 = b*b; + const Real c = std::sqrt(a); + const Real d = b/c; + const Real e = rho_*vol1 - F2*iR2*vol2; + const Real e2 = e*e; + const Real f = d-c; + const Real g = -2*iR2*rho_*vol1*vol2 + 2*F2*iR22*rho_*vol1*vol2 + 2*F2*iR22*vol22 - 2*F22*iR23*vol22; + const Real h = rho_*rho_; + const Real j = 1-h; + const Real iat = 1/c; + const Real l = b*iat - c; + const Real m = f*(1 - (R2*rho_*vol1)/(F2*vol2)) - (e*iR2*k*(d*l + (j*vol12)/(e*e))*vol2)/(2.*c); + const Real n = (iat*(1 - (R2*rho_*vol1)/(F2*vol2)))/R1 - (e*iR2*k*((f*iat)/R1 + b/(a*R1))*vol2)/(2.*c); + const Real o = df*std::exp(-0.5*f*f); + const Real p = d*l + (j*vol12)/(e*e); + const Real q = (-2*j*vol12*(-(iR2*vol2) + F2*iR22*vol2))/(e*e*e); + const Real s = q - (b2*g)/(2.*a2) - (b*f*g)/(2.*a*c) + (f*g)/(2.*c); + const Real u = f*(-((rho_*vol1)/(F2*vol2)) + (R2*rho_*vol1)/(F22*vol2)); + const Real v = -0.5*(b*g*(1 - (R2*rho_*vol1)/(F2*vol2)))/(a*c); + const Real w = (3*g*g)/(4.*a2*c) - (4*iR22*rho_*vol1*vol2 - 4*F2*iR23*rho_*vol1*vol2 + + 2*iR22*vol22 - 8*F2*iR23*vol22 + 6*F22*iR24*vol22)/(2.*a*c); + const Real x = u + v + (e*g*iR2*k*p*vol2)/(4.*a*c) + (e*iR22*k*p*vol2)/(2.*c) - + (e*iR2*k*s*vol2)/(2.*c) - (iR2*k*p*vol2*(-(iR2*vol2) + F2*iR22*vol2))/(2.*c); + const Real y = (4*iR22 - 4*F2*iR23)*rho_*vol1*vol2 + (2*iR22 - 8*F2*iR23 + 6*F22*iR24)*vol22; + const Real z = 4*iR22*rho_*vol1*vol2 - 4*F2*iR23*rho_*vol1*vol2 + 2*iR22*vol22 - + 8*F2*iR23*vol22 + 6*F22*iR24*vol22; + + const Real ooPlt = (k*o*vol23*(-2*c*b2*e2*e*(-1 + f*f)*F23*F24*g*g*iR22*m*vol23 + + 2*b2*e2*e2*F23*F24*g*g*iR2*iR22*k*vol22*vol22 + 2*a*b*e2*e*F23*F22*g*iR22*vol2*(-8* + e2*F2*iR2*k*vol22 + 7*f*F22*g*m*vol22) - a*c*e2*e*F23*F22*g*iR22*vol2*(4*e*F2* + vol2*(-2*b*(-1 + f*f)*m + e*f*iR2*k*vol2) + F22*g*(16*m + e*(2*f + 3*b*iat)*iR2*k*vol2)*vol22) - + 4*a2*a*c*e2*(e2*F22*vol2*(4*F22*iat*iR22*R2*rho_*vol1 + 8*F23*iR22*n*R1*vol2 - + 4*F24*3*iR23*n*R1*vol2 - F23*iR22*(4*iat*rho_*vol1 + F22*iR2*k*p*vol23*w)) + + 4*F23*F22*vol22*vol22*(iR22*(-2*F2*iR2 + 3*F22*iR22)*m + F22*(2*iR2 - 3*F2*iR22)*iR23*m + + F22*iR22*(-iR2 + F2*iR22)*x) + 2*e*F22*(2*F24*F2*iR24*n*R1*vol23 + + 2*f*F2*F22*iR22*rho_*vol1*vol22 - 2*f*F22*iR22*R2*rho_*vol1*vol22 - + b*F24*iR22*R2*rho_*vol1*vol22*w - 2*F24*vol2*(iR23*n*R1*vol22 + 4*iR23*m*vol22 - 2*iR22*vol22*x) + + F23*(2*iR22*m*vol23 + 6*F22*iR24*m*vol23 + b*F22*iR22*vol23*w - 4*F22*iR23*vol23*x))) + + 2*a2*c*e2*F23*F22*vol2*(8*F22*g*iR22*(-iR2 + F2*iR22)*m*vol23 + + e2*iR22*vol2*(8*F2*g*n*R1 + b*F22*iat*iR2*k*vol22*(y - z)) + + 4*e*vol22*(4*F2*g*iR22*m + F22*(-4*g*iR23*m + 2*g*iR22*x + iR22*m*z))) + + 2*a2*a*F22*(-4*e2*e2*e*f*F24*iat*iR24*k*vol23 + 8*e*F2*F24*iR23*(-iR22 + F2*iR23)*j*k*vol12*vol23*vol22 + + 12*F2*F24*iR23*squared(iR2 - F2*iR22)*j*k*vol12*vol23*vol23 + + e2*e2*F2*vol22*(2*F24*iR22*k*vol22*(2*(iR23*p - iR22*s) + b2*iat*iR2*w) + f*(4*F22*iR22*(4*m + + F22*iat*iR2*iR22*k*vol22) - 4*F23*(6*iR23*m + iat*iR24*k*vol22 - 2*iR22*x) + + F24*iR23*k*vol22*(2*b*w + iat*y))) - 2*e2*e*F22*iR22*(4*f*F22*(iR2 - F2*iR22)*m*vol23 + + F22*vol22*(F2*vol2*(2*k*(F2*iR24*p + F2*iR24*p + iR22*s - iR23*(2*p + F2*s))*vol22 + y - z) + + R2*rho_*vol1*(-y + z)))) - 2*a2*e2*F23*(2*e2*e*F23*iR22*k*(2*b*iR22 + g*(-1 + f*iat)*iR2)*vol23 + + 4*b*f*F22*F22*g*iR22*(-iR2 + F2*iR22)*m*vol22*vol22 + + 2*e2*F22*iR22*vol2*(2*b*F2*iR2*(iR2 - F2*iR22)*k*vol23 + g*(2*R2*rho_*vol1 + 2*F2*(-1 + 3*f*m + + b*f*n*R1)*vol2 + F22*k*(-(iR22*p) + iR2*s)*vol23)) + e*vol22*(F2*F22*g*iR22*(g*R2*rho_*vol1 + F2*g*(-1 + f*m)*vol2 + + 2*F2*iR2*(-iR2 + F2*iR22)*k*p*vol23) + 2*b*(2*F2*F22*g*iR22*rho_*vol1 - 2*F22*g*iR22*R2*rho_*vol1 + + 4*f*F23*g*iR22*m*vol2 + f*F24*vol2*(-4*g*iR23*m + 2*g*iR22*x + + iR22*m*z))))))/(16.*a2*a2*c*e2*F23*M_SQRT2*M_SQRTPI*vol2); + + + return callPutParityPrice(kirkCallNPV + 0.5*oPlt + 0.125*ooPlt); } } diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.hpp b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp index cf06df5f33c..461874c5336 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.hpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp @@ -37,15 +37,19 @@ namespace QuantLib { */ class OperatorSplittingSpreadEngine : public SpreadBlackScholesVanillaEngine { public: + enum Order {First, Second}; OperatorSplittingSpreadEngine( ext::shared_ptr process1, ext::shared_ptr process2, - Real correlation); + Real correlation, + Order order = Second); protected: Real calculate( Real strike, Option::Type optionType, Real variance1, Real variance2, DiscountFactor df) const override; + + const Order order_; }; } diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index b4add203ae0..f9b232f4221 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1136,7 +1136,7 @@ BOOST_AUTO_TEST_CASE(testBjerksundStenslandSpreadEngine) { } BOOST_AUTO_TEST_CASE(testOperatorSplittingSpreadEngine) { - BOOST_TEST_MESSAGE("Testing Operator Splitting spread engine..."); + BOOST_TEST_MESSAGE("Testing Strang Operator Splitting spread engine..."); // Example taken from // Chi-Fai Lo, Pricing Spread Options by the Operator Splitting Method, @@ -1212,6 +1212,120 @@ BOOST_AUTO_TEST_CASE(testOperatorSplittingSpreadEngine) { } +BOOST_AUTO_TEST_CASE(testStrangSplittingSpreadEngineVsMathematica) { + BOOST_TEST_MESSAGE("Testing Strang Operator Splitting spread engine" + "vs Mathematica results..."); + + // Example taken from + // Chi-Fai Lo, Pricing Spread Options by the Operator Splitting Method, + // https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2429696 + // Reference results have been calculated with a Mathematica script. + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(27, May, 2024); + + const auto rTS = Handle(flatRate(today, 0.05, dc)); + const auto volTS2 = Handle(flatVol(today, 0.2, dc)); + + struct TestData { + Real T, K, vol1, rho, kirkNPV, strang1, strang2; + }; + + const TestData testCases[] = { + {5.0, 20, 0.1, 0.6, 15.39520956886349, 15.39641179190707, 15.41992212706643}, + {10., 20, 0.1, 0.6, 22.91537136258191, 22.89480115264337, 22.95919510928365}, + {20., 20, 0.1, 0.6, 33.69859018569740, 33.59697949481467, 33.73582501903848}, + {1.0, 20, 0.3, 0.6, 10.9751711157804 , 10.97662152028116, 10.97661321814579}, + {2.0, 20, 0.3, 0.6, 15.68896063758723, 15.69277461480688, 15.69275497617036}, + {3.0, 20, 0.3, 0.6, 19.33110275816226, 19.33760645637910, 19.33758123861756}, + {4.0, 20, 0.3, 0.6, 22.40185479100672, 22.41113452093983, 22.41111679131122}, + {5.0, 20, 0.3, 0.6, 25.09737848235137, 25.10937922536118, 25.10938819057636}, + {1.0, 10, 0.3, 0.6, 16.10447007803242, 16.10494344785443, 16.10494658134660}, + {1.0, 40, 0.3, 0.6, 4.657519189575983, 4.657079657030094, 4.656973008981588}, + {1.0, 60, 0.3, 0.6, 1.837359067901817, 1.831230481909945, 1.831241843743509}, + {1.0, 20, 0.5, 0.6, 18.79838447214884, 18.79674735337080, 18.79654551825391}, + {1.0, 20, 0.3,-0.9, 20.17112122874686, 20.14780367419582, 20.15151348149147}, + {1.0, 20, 0.3, 0.0, 15.38036208157481, 15.37697052349819, 15.37728179978961}, + {2.0, 20, 0.3,-0.5, 25.80847626931109, 25.77323435009942, 25.77810550213640} + }; + + const Real s1 = 110.0, s2 = 90.0; + const Real tol = 100*QL_EPSILON; + + for (const auto& testCase: testCases) { + const Real rho = testCase.rho;; + const Real strike = testCase.K; + const Date maturityDate = yearFractionToDate(dc, today, testCase.T); + const auto volTS1 = Handle(flatVol(today, testCase.vol1, dc)); + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Call, strike)), + ext::make_shared(maturityDate)); + + const DiscountFactor dr = rTS->discount(maturityDate); + + const Real F1 = s1/dr; + const Real F2 = s2/dr; + + const ext::shared_ptr p1 + = ext::make_shared( + Handle(ext::make_shared(F1)), rTS, volTS1 + ); + + const ext::shared_ptr p2 + = ext::make_shared( + Handle(ext::make_shared(F2)), rTS, volTS2 + ); + + option.setPricingEngine(ext::make_shared(p1, p2, rho)); + + const Real kirkCalculated = option.NPV(); + Real diff = std::abs(testCase.kirkNPV - kirkCalculated); + if (diff > tol*testCase.kirkNPV) { + BOOST_FAIL("failed to reproduce Kirk reference values " + << std::fixed << std::setprecision(16) + << "\n calculated: " << kirkCalculated + << "\n expected : " << testCase.kirkNPV + << "\n diff : " << diff + << "\n tolerance : " << tol); + } + + option.setPricingEngine( + ext::make_shared( + p1, p2, rho, OperatorSplittingSpreadEngine::First) + ); + + const Real strang1Calculated = option.NPV(); + diff = std::abs(testCase.strang1 - strang1Calculated); + if (diff > tol*testCase.strang1) { + BOOST_FAIL("failed to reproduce Operator Splitting reference values " + << std::fixed << std::setprecision(16) + << "\n calculated: " << strang1Calculated + << "\n expected : " << testCase.strang1 + << "\n diff : " << diff + << "\n tolerance : " << tol); + } + + option.setPricingEngine( + ext::make_shared( + p1, p2, rho, OperatorSplittingSpreadEngine::Second) + ); + + const Real strang2Calculated = option.NPV(); + diff = std::abs(testCase.strang2 - strang2Calculated); + if (diff > tol*testCase.strang2) { + BOOST_FAIL("failed to reproduce Operator Splitting reference values " + << std::fixed << std::setprecision(16) + << "\n calculated: " << strang2Calculated + << "\n expected : " << testCase.strang2 + << "\n diff : " << diff + << "\n tolerance : " << tol); + } + } +} + + BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { BOOST_TEST_MESSAGE("Testing two-dimensional PDE engine " "vs analytical approximations..."); @@ -1243,7 +1357,7 @@ BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { const Real strike = 5; - IncrementalStatistics statKirk, statBS2014, statOs; + IncrementalStatistics statKirk, statBS2014, statOs1, statOs2; for (Option::Type type: {Option::Call, Option::Put}) { BasketOption option( @@ -1258,8 +1372,13 @@ BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { const ext::shared_ptr bs2014Engine = ext::make_shared(p1, p2, rho); - const ext::shared_ptr osEngine - = ext::make_shared(p1, p2, rho); + const ext::shared_ptr osEngine1 + = ext::make_shared( + p1, p2, rho, OperatorSplittingSpreadEngine::First); + + const ext::shared_ptr osEngine2 + = ext::make_shared( + p1, p2, rho, OperatorSplittingSpreadEngine::Second); const ext::shared_ptr fdEngine = ext::make_shared( @@ -1279,8 +1398,11 @@ BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { option.setPricingEngine(bs2014Engine); statBS2014.add(option.NPV() - fdNPV); - option.setPricingEngine(osEngine); - statOs.add(option.NPV() - fdNPV); + option.setPricingEngine(osEngine1); + statOs1.add(option.NPV() - fdNPV); + + option.setPricingEngine(osEngine2); + statOs2.add(option.NPV() - fdNPV); } } } @@ -1300,11 +1422,19 @@ BOOST_AUTO_TEST_CASE(testPDEvsApproximations) { << "\n stdev : " << statBS2014.standardDeviation() << "\n tolerance : " << 0.02); } - if (statOs.standardDeviation() > 0.02) { + if (statOs1.standardDeviation() > 0.02) { + BOOST_FAIL("failed to reproduce PDE spread option prices" + " with Operator-Splitting engine (first order)." + << std::fixed << std::setprecision(5) + << "\n stdev : " << statOs1.standardDeviation() + << "\n tolerance : " << 0.02); + } + + if (statOs2.standardDeviation() > 0.02) { BOOST_FAIL("failed to reproduce PDE spread option prices" - " with Operator-Splitting engine." + " with Operator-Splitting engine (second order)." << std::fixed << std::setprecision(5) - << "\n stdev : " << statOs.standardDeviation() + << "\n stdev : " << statOs2.standardDeviation() << "\n tolerance : " << 0.02); } } @@ -1473,6 +1603,8 @@ BOOST_AUTO_TEST_CASE(testNdimPDEinDifferentDims) { } BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { + BOOST_TEST_MESSAGE("Testing Deng-Li-Zhou basket engine vs PDE engine..."); + const DayCounter dc = Actual365Fixed(); const Date today = Date(25, March, 2024); const Date maturity = today + Period(6, Months); @@ -1535,6 +1667,8 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { BOOST_AUTO_TEST_CASE(testDengLiZhouWithNegativeStrike) { + BOOST_TEST_MESSAGE("Testing Deng-Li-Zhou basket engine with negative strike..."); + const DayCounter dc = Actual365Fixed(); const Date today = Date(27, May, 2024); const Date maturity = today + Period(6, Months); From ad14f6f0edfba571a5ad601799c20f665331d9c2 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 8 Jul 2024 12:34:43 +0200 Subject: [PATCH 07/36] first step for Choi engine --- ql/CMakeLists.txt | 8 +- ql/instruments/basketoption.hpp | 4 +- ...dengine.cpp => denglizhoubasketengine.cpp} | 61 +++--------- ...dengine.hpp => denglizhoubasketengine.hpp} | 8 +- .../fdndimblackscholesvanillaengine.cpp | 3 + .../basket/singlefactorbsmbasketengine.cpp | 99 +++++++++++++++++++ .../basket/singlefactorbsmbasketengine.hpp | 56 +++++++++++ .../basket/vectorbsmprocessextractor.cpp | 85 ++++++++++++++++ .../basket/vectorbsmprocessextractor.hpp | 51 ++++++++++ test-suite/basketoption.cpp | 72 +++++++++++++- 10 files changed, 388 insertions(+), 59 deletions(-) rename ql/pricingengines/basket/{denglizhouspreadengine.cpp => denglizhoubasketengine.cpp} (85%) rename ql/pricingengines/basket/{denglizhouspreadengine.hpp => denglizhoubasketengine.hpp} (93%) create mode 100644 ql/pricingengines/basket/singlefactorbsmbasketengine.cpp create mode 100644 ql/pricingengines/basket/singlefactorbsmbasketengine.hpp create mode 100644 ql/pricingengines/basket/vectorbsmprocessextractor.cpp create mode 100644 ql/pricingengines/basket/vectorbsmprocessextractor.hpp diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 47e1a7a80e3..3a026231d28 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -675,13 +675,15 @@ set(QL_SOURCES pricingengines/barrier/fdhestonrebateengine.cpp pricingengines/barrier/mcbarrierengine.cpp pricingengines/basket/bjerksundstenslandspreadengine.cpp - pricingengines/basket/denglizhouspreadengine.cpp + pricingengines/basket/vectorbsmprocessextractor.cpp + pricingengines/basket/denglizhoubasketengine.cpp pricingengines/basket/fd2dblackscholesvanillaengine.cpp pricingengines/basket/fdndimblackscholesvanillaengine.cpp pricingengines/basket/kirkengine.cpp pricingengines/basket/mcamericanbasketengine.cpp pricingengines/basket/mceuropeanbasketengine.cpp pricingengines/basket/operatorsplittingspreadengine.cpp + pricingengines/basket/singlefactorbsmbasketengine.cpp pricingengines/basket/spreadblackscholesvanillaengine.cpp pricingengines/basket/stulzengine.cpp pricingengines/blackcalculator.cpp @@ -1888,13 +1890,15 @@ set(QL_HEADERS pricingengines/barrier/fdhestonrebateengine.hpp pricingengines/barrier/mcbarrierengine.hpp pricingengines/basket/bjerksundstenslandspreadengine.hpp - pricingengines/basket/denglizhouspreadengine.hpp + pricingengines/basket/vectorbsmprocessextractor.hpp + pricingengines/basket/denglizhoubasketengine.hpp pricingengines/basket/fd2dblackscholesvanillaengine.hpp pricingengines/basket/fdndimblackscholesvanillaengine.hpp pricingengines/basket/kirkengine.hpp pricingengines/basket/mcamericanbasketengine.hpp pricingengines/basket/mceuropeanbasketengine.hpp pricingengines/basket/operatorsplittingspreadengine.hpp + pricingengines/basket/singlefactorbsmbasketengine.hpp pricingengines/basket/spreadblackscholesvanillaengine.hpp pricingengines/basket/stulzengine.hpp pricingengines/blackcalculator.hpp diff --git a/ql/instruments/basketoption.hpp b/ql/instruments/basketoption.hpp index 7d20e8aa1d4..fd0ee4bd127 100644 --- a/ql/instruments/basketoption.hpp +++ b/ql/instruments/basketoption.hpp @@ -69,8 +69,8 @@ namespace QuantLib { class AverageBasketPayoff : public BasketPayoff { public: - AverageBasketPayoff(const ext::shared_ptr& p, Array a) - : BasketPayoff(p), weights_(std::move(a)) {} + AverageBasketPayoff(const ext::shared_ptr& p, Array weights) + : BasketPayoff(p), weights_(std::move(weights)) {} AverageBasketPayoff(const ext::shared_ptr &p, Size n) : BasketPayoff(p), weights_(n, 1.0/static_cast(n)) {} diff --git a/ql/pricingengines/basket/denglizhouspreadengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp similarity index 85% rename from ql/pricingengines/basket/denglizhouspreadengine.cpp rename to ql/pricingengines/basket/denglizhoubasketengine.cpp index 97c032badc4..a782988141c 100644 --- a/ql/pricingengines/basket/denglizhouspreadengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -17,17 +17,18 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ +#include "denglizhoubasketengine.hpp" + #include #include -#include #include #include #include -#include "ql/pricingengines/basket/denglizhouspreadengine.hpp" +#include namespace QuantLib { - DengLiZhouSpreadEngine::DengLiZhouSpreadEngine( + DengLiZhouBasketEngine::DengLiZhouBasketEngine( std::vector > processes, Matrix rho) : n_(processes.size()), @@ -42,50 +43,12 @@ namespace QuantLib { registerWith(processes_[i]); } - void DengLiZhouSpreadEngine::calculate() const { + void DengLiZhouBasketEngine::calculate() const { const ext::shared_ptr exercise = ext::dynamic_pointer_cast(arguments_.exercise); QL_REQUIRE(exercise, "not an European exercise"); const Date maturityDate = exercise->lastDate(); - const auto extractProcesses = - [this](const std::function& f) - -> Array { - - Array x(n_); - std::transform(processes_.begin(), processes_.end(), x.begin(), f); - - return x; - }; - - const Array dr = extractProcesses( - [maturityDate](const auto& p) -> DiscountFactor { - return p->riskFreeRate()->discount(maturityDate); - } - ); - - QL_REQUIRE( - std::equal( - dr.begin()+1, dr.end(), dr.begin(), - std::pointer_to_binary_function(close_enough) - ), - "interest rates need to be the same for all underlyings" - ); - const DiscountFactor dr0 = dr[0]; - - const Array s = extractProcesses([](const auto& p) -> Real { return p->x0(); }); - - const Array dq = extractProcesses( - [maturityDate](const auto& p) -> DiscountFactor { - return p->dividendYield()->discount(maturityDate); - } - ); - const Array v = extractProcesses( - [maturityDate](const auto& p) -> Volatility { - return p->blackVolatility()->blackVariance(maturityDate, p->x0()); - } - ); - const ext::shared_ptr avgPayoff = ext::dynamic_pointer_cast(arguments_.payoff); QL_REQUIRE(avgPayoff, "average basket payoff expected"); @@ -95,6 +58,12 @@ namespace QuantLib { QL_REQUIRE(n_ == weights.size() && n_ > 1, "wrong number of weights arguments in payoff"); + const detail::VectorBsmProcessExtractor pExtractor(processes_); + const Array s = pExtractor.getSpot(); + const Array dq = pExtractor.getDividendYield(maturityDate); + const Array v = pExtractor.getBlackVariance(maturityDate); + const DiscountFactor dr0 = pExtractor.getInterestRate(maturityDate); + std::vector< std::tuple > p; p.reserve(n_); @@ -194,18 +163,18 @@ namespace QuantLib { // << _v << std::endl << nRho << std::endl << strike << std::endl << std::endl; const Real callValue - = DengLiZhouSpreadEngine::calculate_vanilla_call(Log(_s), dr0, _dq, _v, nRho, strike); + = DengLiZhouBasketEngine::calculate_vanilla_call(Log(_s), dr0, _dq, _v, nRho, strike); if (payoff->optionType() == Option::Call) results_.value = std::max(0.0, callValue); else { - const Real fwd = _s[0]*_dq[0] - dr[0]*strike + const Real fwd = _s[0]*_dq[0] - dr0*strike - std::inner_product(_s.begin()+1, _s.end(), _dq.begin()+1, 0.0); results_.value = std::max(0.0, callValue - fwd); } } - Real DengLiZhouSpreadEngine::I(Real u, Real tF2, const Matrix& D, const Matrix& DF, Size i) { + Real DengLiZhouBasketEngine::I(Real u, Real tF2, const Matrix& D, const Matrix& DF, Size i) { const Real psi = 1.0/ (1.0 + std::inner_product( D.row_begin(i), D.row_end(i), D.row_begin(i), 0.0)); @@ -230,7 +199,7 @@ namespace QuantLib { return J_0 + J_1 - 0.5*J_2; } - Real DengLiZhouSpreadEngine::calculate_vanilla_call( + Real DengLiZhouBasketEngine::calculate_vanilla_call( const Array& x, DiscountFactor dr, const Array& dq, const Array& v, const Matrix& rho, Real K) { diff --git a/ql/pricingengines/basket/denglizhouspreadengine.hpp b/ql/pricingengines/basket/denglizhoubasketengine.hpp similarity index 93% rename from ql/pricingengines/basket/denglizhouspreadengine.hpp rename to ql/pricingengines/basket/denglizhoubasketengine.hpp index 0148fca7132..63fb5b59ffa 100644 --- a/ql/pricingengines/basket/denglizhouspreadengine.hpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.hpp @@ -21,8 +21,8 @@ \brief Deng, Li and Zhou: Closed-Form Approximation for Spread option pricing */ -#ifndef quantlib_deng_li_zhou_spread_engine_hpp -#define quantlib_deng_li_zhou_spread_engine_hpp +#ifndef quantlib_deng_li_zhou_basket_engine_hpp +#define quantlib_deng_li_zhou_basket_engine_hpp #include #include @@ -53,9 +53,9 @@ namespace QuantLib { \test the correctness of the returned value is tested by reproducing results available in literature. */ - class DengLiZhouSpreadEngine : public BasketOption::engine { + class DengLiZhouBasketEngine : public BasketOption::engine { public: - DengLiZhouSpreadEngine( + DengLiZhouBasketEngine( std::vector > processes, Matrix rho); diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp index 242b9d65b34..f2af9c7401c 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -54,6 +54,9 @@ namespace QuantLib { "correlation matrix has the wrong size."); QL_REQUIRE(xGrids_.size() == processes_.size(), "wrong number of xGrids is given."); + + for (const auto& process: processes_) + registerWith(process); } diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp new file mode 100644 index 00000000000..e1cc7a30c79 --- /dev/null +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -0,0 +1,99 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file singlefactorbsmnasketengine.cpp +*/ + +#include +#include +#include + +namespace QuantLib { + SingleFactorBsmBasketEngine::SingleFactorBsmBasketEngine( + std::vector > p) + : n_(p.size()), processes_(std::move(p)) { + for (const auto& process: processes_) + registerWith(process); + } + + Real SingleFactorBsmBasketEngine::rootSumExponentials( + const Array& a, const Array& sig, Real K) { + + const Array attr = a*sig; + QL_REQUIRE( + std::all_of( + attr.begin(), attr.end(), + [](Real x) -> bool { return x >= 0.0; } + ), + "a*sig should not be negative" + ); + + const bool logProb = + std::all_of( + a.begin(), a.end(), + [](Real x) -> bool { return x > 0;} + ); + + QL_REQUIRE(K > 0 || !logProb, + "non-positive strikes only allowed for spread options"); + + // linear approximation + const Real denom = std::accumulate(attr.begin(), attr.end(), 0.0); + const Real xInit = (std::abs(denom) > 1000*QL_EPSILON) + ? std::max(5.0, std::min(-5.0, + (K - std::accumulate(a.begin(), a.end(), 0.0))/denom) + ) + : 0.0; + + const auto sumExp = [&a, &sig, K](Real x) -> Real { + Real s = 0.0; + for (Size i=0; i < a.size(); ++i) + s += a[i]*std::exp(sig[i]*x); + return s - K; + }; + + return xInit; + } + + void SingleFactorBsmBasketEngine::calculate() const { + const ext::shared_ptr avgPayoff = + ext::dynamic_pointer_cast(arguments_.payoff); + QL_REQUIRE(avgPayoff, "average basket payoff expected"); + + // sort assets by their weight + const Array weights = avgPayoff->weights(); + QL_REQUIRE(n_ == weights.size(), + "wrong number of weights arguments in payoff"); + + const ext::shared_ptr exercise = + ext::dynamic_pointer_cast(arguments_.exercise); + QL_REQUIRE(exercise, "not an European exercise"); + const Date maturityDate = exercise->lastDate(); + + const detail::VectorBsmProcessExtractor pExtractor(processes_); + const Array s = pExtractor.getSpot(); + const Array dq = pExtractor.getDividendYield(maturityDate); + const Array v = pExtractor.getBlackVariance(maturityDate); + const DiscountFactor dr0 = pExtractor.getInterestRate(maturityDate); + + + + } +} + diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp new file mode 100644 index 00000000000..2ff4f6a0f53 --- /dev/null +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -0,0 +1,56 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file singlefactorbsmnasketengine.hpp + \brief Basket engine where all underlyings are driven by one stochastic factor +*/ + +#ifndef quantlib_single_factor_bsm_basket_engine_hpp +#define quantlib_single_factor_bsm_basket_engine_hpp + +#include +#include +#include + +namespace QuantLib { + + //! Pricing engine for baskets where all underlyings are driven by one stochastic factor + /*! Jaehyuk Choi, + Sum of all Black-Scholes-Merton Models: + An efficient Pricing Method for Spread, Basket and Asian Options, + https://arxiv.org/pdf/1805.03172 + + \ingroup basketengines + */ + class SingleFactorBsmBasketEngine : public BasketOption::engine { + public: + SingleFactorBsmBasketEngine( + std::vector > p); + + void calculate() const override; + static Real rootSumExponentials(const Array& a, const Array& sig, Real K); + + private: + const Size n_; + const std::vector > processes_; + }; +} + + +#endif diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp new file mode 100644 index 00000000000..145a71391f0 --- /dev/null +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp @@ -0,0 +1,85 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file bsmprocessesextractor.cpp +*/ +#include +#include + +#include + +namespace QuantLib { + namespace detail { + VectorBsmProcessExtractor::VectorBsmProcessExtractor( + std::vector > p) + : processes_(std::move(p)) { + } + + Array VectorBsmProcessExtractor::extractProcesses( + const std::function&)>& f) const { + + Array x(processes_.size()); + std::transform(processes_.begin(), processes_.end(), x.begin(), f); + + return x; + } + + DiscountFactor VectorBsmProcessExtractor::getInterestRate( + const Date& maturityDate) const { + const Array dr = extractProcesses( + [maturityDate](const auto& p) -> DiscountFactor { + return p->riskFreeRate()->discount(maturityDate); + } + ); + + QL_REQUIRE( + std::equal( + dr.begin()+1, dr.end(), dr.begin(), + std::pointer_to_binary_function(close_enough) + ), + "interest rates need to be the same for all underlyings" + ); + + return dr[0]; + } + + Array VectorBsmProcessExtractor::getSpot() const { + return extractProcesses([](const auto& p) -> Real { return p->x0(); }); + } + + Array VectorBsmProcessExtractor::getDividendYield( + const Date& maturityDate) const { + return extractProcesses( + [maturityDate](const auto& p) -> DiscountFactor { + return p->dividendYield()->discount(maturityDate); + } + ); + } + + Array VectorBsmProcessExtractor::getBlackVariance( + const Date& maturityDate) const { + return extractProcesses( + [maturityDate](const auto& p) -> Volatility { + return p->blackVolatility()->blackVariance(maturityDate, p->x0()); + } + ); + } + } +} diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp new file mode 100644 index 00000000000..7989aabedcc --- /dev/null +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp @@ -0,0 +1,51 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file bsmprocessesextractor.hpp + \brief helper class to extract underlying, volatility etc from a vector of processes +*/ + +#ifndef quantlib_vector_bsm_process_extractor_hpp +#define quantlib_vector_bsm_process_extractor_hpp + +#include +#include + +namespace QuantLib { + namespace detail { + class VectorBsmProcessExtractor { + public: + VectorBsmProcessExtractor( + std::vector > p); + + Array getSpot() const; + Array getBlackVariance(const Date& maturityDate) const; + Array getDividendYield(const Date& maturityDate) const; + DiscountFactor getInterestRate(const Date& maturityDate) const; + + private: + Array extractProcesses( + const std::function&)>& f) const; + + const std::vector > processes_; + }; + } +} + +#endif diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index f9b232f4221..1af4d367278 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include #include #include #include @@ -44,6 +44,7 @@ #include #include #include +#include "../ql/pricingengines/basket/denglizhoubasketengine.hpp" using namespace QuantLib; using namespace boost::unit_test_framework; @@ -1643,17 +1644,17 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { exercise ); - option.setPricingEngine(ext::make_shared(processes, rho)); + option.setPricingEngine(ext::make_shared(processes, rho)); const Real calculated = option.NPV(); option.setPricingEngine( ext::make_shared( - processes, rho, std::vector(4, 30), 20 + processes, rho, std::vector(4, 15), 10 ) ); const Real expected = option.NPV(); const Real diff = std::abs(calculated - expected); - const Real tol = 0.03; + const Real tol = 0.05; if (diff > tol) { BOOST_FAIL("failed to compare basket option prices" @@ -1708,7 +1709,8 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouWithNegativeStrike) { exercise ); - option.setPricingEngine(ext::make_shared(processes, rho)); + option.setPricingEngine( + ext::make_shared(processes, rho)); const Real calculated = option.NPV(); const Real expected = 3.34412; @@ -1724,6 +1726,66 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouWithNegativeStrike) { << "\n tolerance: " << tol); } +BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { + BOOST_TEST_MESSAGE("Testing the root of a sum of exponentials..."); + + BOOST_CHECK_THROW(SingleFactorBsmBasketEngine::rootSumExponentials( + {2.0, 3.0, 4.0}, {0.2, 0.4, -0.1}, 0.0) , Error + ); + BOOST_CHECK_THROW(SingleFactorBsmBasketEngine::rootSumExponentials( + {2.0, -3.0, 4.0}, {0.2, -0.4, -0.1}, 0.0), Error + ); + + const Real x = SingleFactorBsmBasketEngine::rootSumExponentials( + {2.0, -3.0, 4.0}, {0.2, -0.4, 0.1}, 3.1); + + std::cout << x << std::endl; + +} + + +BOOST_AUTO_TEST_CASE(testSingleFactorBsmBasketEngine) { + BOOST_TEST_MESSAGE( + "Testing single factor BSM basket engine against reference results..."); + + // Reference results are calculated with the python library PyFENG + // https://github.com/PyFE/PyFENG + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(3, July, 2024); + const Date maturity = today + Period(18, Months); + + const std::vector underlyings({220, 45, 102, 75}); + const std::vector volatilities({0.3, 0.45, 0.3, 0.25}); + const std::vector q({0.05, 0.075, 0.02, 0.1}); + + const Handle rTS = Handle( + flatRate(today, 0.03, dc)); + + std::vector > processes; + for (Size d=0; d < 4; ++d) + processes.push_back( + ext::make_shared( + Handle(ext::make_shared(underlyings[d])), + Handle(flatRate(today, q[d], dc)), rTS, + Handle(flatVol(today, volatilities[d], dc)) + ) + ); + + const Real strike = 150; + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Call, strike), + Array({0.5, 2.0, 1.0, 1.5}) + ), + ext::make_shared(maturity) + ); + + option.setPricingEngine( + ext::make_shared(processes)); + +} BOOST_AUTO_TEST_SUITE_END() From b01e2d88bcef1b9cd38931fa2b742b4789fdad6f Mon Sep 17 00:00:00 2001 From: klausspanderen Date: Thu, 11 Jul 2024 19:48:07 +0200 Subject: [PATCH 08/36] . --- .../basket/singlefactorbsmbasketengine.cpp | 122 +++++++++++++++--- .../basket/singlefactorbsmbasketengine.hpp | 25 +++- test-suite/basketoption.cpp | 49 +++++-- 3 files changed, 167 insertions(+), 29 deletions(-) diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp index e1cc7a30c79..773c6957a45 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -21,21 +21,68 @@ */ #include +#include +#include +#include +#include #include #include +#include + namespace QuantLib { - SingleFactorBsmBasketEngine::SingleFactorBsmBasketEngine( - std::vector > p) - : n_(p.size()), processes_(std::move(p)) { - for (const auto& process: processes_) - registerWith(process); + + SumExponentialsRootSolver::SumExponentialsRootSolver( + Array a, Array sig, Real K) + : a_(std::move(a)), sig_(std::move(sig)), K_(K), + fCtr_(0), fPrimeCtr_(0), fDoublePrimeCtr_(0) { + QL_REQUIRE(a_.size() == sig_.size(), + "Arrays must have the same size"); } - Real SingleFactorBsmBasketEngine::rootSumExponentials( - const Array& a, const Array& sig, Real K) { + Real SumExponentialsRootSolver::operator()(Real x) const { + ++fCtr_; + + Real s = 0.0; + for (Size i=0; i < a_.size(); ++i) + s += a_[i]*std::exp(sig_[i]*x); + return s - K_; + } + + Real SumExponentialsRootSolver::derivative(Real x) const { + ++fPrimeCtr_; + + Real s = 0.0; + for (Size i=0; i < a_.size(); ++i) + s += a_[i]*sig_[i]*std::exp(sig_[i]*x); + return s; + } + + Real SumExponentialsRootSolver::secondDerivative(Real x) const { + ++fDoublePrimeCtr_; + + Real s = 0.0; + for (Size i=0; i < a_.size(); ++i) + s += a_[i]*squared(sig_[i])*std::exp(sig_[i]*x); + return s; + } - const Array attr = a*sig; + Size SumExponentialsRootSolver::getFCtr() const { + return fCtr_; + } + + Size SumExponentialsRootSolver::getDerivativeCtr() const { + return fPrimeCtr_; + } + + Size SumExponentialsRootSolver::getSecondDerivativeCtr() const { + return fDoublePrimeCtr_; + } + + Real SumExponentialsRootSolver::rootSumExponentials( + Real xTol, Strategy strategy) const { + + const Array attr = a_*sig_; QL_REQUIRE( std::all_of( attr.begin(), attr.end(), @@ -46,29 +93,68 @@ namespace QuantLib { const bool logProb = std::all_of( - a.begin(), a.end(), + a_.begin(), a_.end(), [](Real x) -> bool { return x > 0;} ); - QL_REQUIRE(K > 0 || !logProb, + QL_REQUIRE(K_ > 0 || !logProb, "non-positive strikes only allowed for spread options"); // linear approximation const Real denom = std::accumulate(attr.begin(), attr.end(), 0.0); const Real xInit = (std::abs(denom) > 1000*QL_EPSILON) ? std::max(5.0, std::min(-5.0, - (K - std::accumulate(a.begin(), a.end(), 0.0))/denom) + (K_ - std::accumulate(a_.begin(), a_.end(), 0.0))/denom) ) : 0.0; - const auto sumExp = [&a, &sig, K](Real x) -> Real { - Real s = 0.0; - for (Size i=0; i < a.size(); ++i) - s += a[i]*std::exp(sig[i]*x); - return s - K; - }; + switch(strategy) { + case Brent: + return QuantLib::Brent().solve(*this, xTol, xInit, 0.1); + case Newton: + return QuantLib::Newton().solve(*this, xTol, xInit, 0.1); + case Ridder: + return QuantLib::Ridder().solve(*this, xTol, xInit, 0.1); + case Halley: + case SuperHalley: { + const Size maxIter = 100; + Size nIter = 0; + bool resultCloseEnough; + Real x = xInit; + Real xOld, fx; + + do { + xOld = x; + fx = (*this)(x); + const Real fPrime = derivative(x); + const Real lf = fx*secondDerivative(x)/(fPrime*fPrime); + const Real step = (strategy == Halley) + ? Real(1/(1 - 0.5*lf)*fx/fPrime) + : Real((1 + 0.5*lf/(1-lf))*fx/fPrime); + + x -= step; + resultCloseEnough = std::fabs(x-xOld) < 0.5*xTol; + } + while (!resultCloseEnough && ++nIter < maxIter); + + if (!resultCloseEnough && !close(std::fabs(fx), 0.0)) { + std::cout << "ouch" << std::endl; + x = QuantLib::Brent().solve(*this, xTol, xInit, 0.1); + + } + return x; + } + default: + QL_FAIL("unknown strategy type"); + } + } + - return xInit; + SingleFactorBsmBasketEngine::SingleFactorBsmBasketEngine( + std::vector > p) + : n_(p.size()), processes_(std::move(p)) { + for (const auto& process: processes_) + registerWith(process); } void SingleFactorBsmBasketEngine::calculate() const { diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index 2ff4f6a0f53..5d74952450d 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -38,13 +38,36 @@ namespace QuantLib { \ingroup basketengines */ + + class SumExponentialsRootSolver { + public: + enum Strategy {Ridder, Newton, Brent, Halley, SuperHalley}; + + SumExponentialsRootSolver(Array a, Array sig, Real K); + + Real operator()(Real x) const; + Real derivative(Real x) const; + Real secondDerivative(Real x) const; + + Real rootSumExponentials( + Real xTol = 1e4*QL_EPSILON, Strategy strategy = Brent) const; + + Size getFCtr() const; + Size getDerivativeCtr() const; + Size getSecondDerivativeCtr() const; + + private: + const Array a_, sig_; + const Real K_; + mutable Size fCtr_, fPrimeCtr_, fDoublePrimeCtr_; + }; + class SingleFactorBsmBasketEngine : public BasketOption::engine { public: SingleFactorBsmBasketEngine( std::vector > p); void calculate() const override; - static Real rootSumExponentials(const Array& a, const Array& sig, Real K); private: const Size n_; diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 1af4d367278..a7594e47920 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,8 @@ #include #include #include -#include "../ql/pricingengines/basket/denglizhoubasketengine.hpp" + +#include using namespace QuantLib; using namespace boost::unit_test_framework; @@ -1729,18 +1731,45 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouWithNegativeStrike) { BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { BOOST_TEST_MESSAGE("Testing the root of a sum of exponentials..."); - BOOST_CHECK_THROW(SingleFactorBsmBasketEngine::rootSumExponentials( - {2.0, 3.0, 4.0}, {0.2, 0.4, -0.1}, 0.0) , Error + BOOST_CHECK_THROW(SumExponentialsRootSolver( + {2.0, 3.0, 4.0}, {0.2, 0.4, -0.1}, 0.0).rootSumExponentials() , Error ); - BOOST_CHECK_THROW(SingleFactorBsmBasketEngine::rootSumExponentials( - {2.0, -3.0, 4.0}, {0.2, -0.4, -0.1}, 0.0), Error + BOOST_CHECK_THROW(SumExponentialsRootSolver( + {2.0, -3.0, 4.0}, {0.2, -0.4, -0.1}, 0.0).rootSumExponentials(), Error ); - const Real x = SingleFactorBsmBasketEngine::rootSumExponentials( - {2.0, -3.0, 4.0}, {0.2, -0.4, 0.1}, 3.1); - - std::cout << x << std::endl; - + MersenneTwisterUniformRng mt(42); + + for (auto strategy: { + SumExponentialsRootSolver::Brent, + SumExponentialsRootSolver::Newton, + SumExponentialsRootSolver::Ridder, + SumExponentialsRootSolver::Halley}) { + + Size fCtr = 0; + + // first test arbitrary problem + for (Size i=0; i < 1000; ++i) { + // problem size + const Size n = (mt.nextInt32() % 10)+1; + Array a(n), sig(n); + for (Size j=0; j < n; ++j) { + a[j] = mt.nextReal() - 1.0; + sig[j] = copysign(1.0, a[j])*mt.nextReal(); + } + const Real kMin = SumExponentialsRootSolver(a, sig, 0.0)(-10.0); + const Real kMax = SumExponentialsRootSolver(a, sig, 0.0)( 10.0); + const Real K = (kMax - kMin)*mt.nextReal() + kMin; + + const SumExponentialsRootSolver solver(a, sig, K); + const Real xRoot = solver.rootSumExponentials( + 1e1*QL_EPSILON, strategy); + + fCtr += solver.getFCtr() + solver.getDerivativeCtr() + solver.getSecondDerivativeCtr(); + } + + std::cout << fCtr << std::endl; + } } From a0d0ceca3dd717eee09507e59e3dbfaeadc3ab16 Mon Sep 17 00:00:00 2001 From: Klaus Spanderen Date: Fri, 26 Jul 2024 15:51:33 +0200 Subject: [PATCH 09/36] Update denglizhoubasketengine.cpp --- ql/pricingengines/basket/denglizhoubasketengine.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/ql/pricingengines/basket/denglizhoubasketengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp index a782988141c..1485a3f4172 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -159,9 +159,6 @@ namespace QuantLib { nRho[i+1][j+1] = rho[idx][std::get<1>(p[M+j])]; } - //std::cout << N << std::endl << _s << std::endl << _dq << std::endl - // << _v << std::endl << nRho << std::endl << strike << std::endl << std::endl; - const Real callValue = DengLiZhouBasketEngine::calculate_vanilla_call(Log(_s), dr0, _dq, _v, nRho, strike); From a306a54d12ba86844edfd4a9604c129c04d945a9 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sun, 11 Aug 2024 16:24:19 +0200 Subject: [PATCH 10/36] added halley solver --- ql/CMakeLists.txt | 1 + ql/math/solvers1d/halley.hpp | 75 +++++++++++++++++++ .../basket/singlefactorbsmbasketengine.cpp | 47 ++++-------- .../basket/singlefactorbsmbasketengine.hpp | 4 +- test-suite/basketoption.cpp | 41 +++++++--- test-suite/solvers.cpp | 14 ++++ 6 files changed, 135 insertions(+), 47 deletions(-) create mode 100644 ql/math/solvers1d/halley.hpp diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 3a026231d28..30469ba5076 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -1554,6 +1554,7 @@ set(QL_HEADERS math/solvers1d/brent.hpp math/solvers1d/falseposition.hpp math/solvers1d/finitedifferencenewtonsafe.hpp + math/solvers1d/halley.hpp math/solvers1d/newton.hpp math/solvers1d/newtonsafe.hpp math/solvers1d/ridder.hpp diff --git a/ql/math/solvers1d/halley.hpp b/ql/math/solvers1d/halley.hpp new file mode 100644 index 00000000000..ec0d737cc7c --- /dev/null +++ b/ql/math/solvers1d/halley.hpp @@ -0,0 +1,75 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file halley.hpp + \brief Halley 1-D solver +*/ + +#ifndef quantlib_solver1d_halley_hpp +#define quantlib_solver1d_halley_hpp + +#include + +namespace QuantLib { + + //! %Halley 1-D solver + /*! \note This solver requires that the passed function object + implement a method Real derivative(Real) + and Real secondDerivative(Real> + + \test the correctness of the returned values is tested by + checking them against known good results. + + \ingroup solvers + */ + class Halley : public Solver1D { + public: + template + Real solveImpl(const F& f, + Real xAccuracy) const { + + while (++evaluationNumber_ <= maxEvaluations_) { + const Real fx = f(root_); + const Real fPrime = f.derivative(root_); + const Real lf = fx*f.secondDerivative(root_)/(fPrime*fPrime); + const Real step = 1.0/(1.0 - 0.5*lf)*fx/fPrime; + root_ -= step; + + // jumped out of brackets, switch to NewtonSafe + if ((xMin_-root_)*(root_-xMax_) < 0.0) { + NewtonSafe s; + s.setMaxEvaluations(maxEvaluations_-evaluationNumber_); + return s.solve(f, xAccuracy, root_+step, xMin_, xMax_); + } + + if (std::abs(step) < xAccuracy) { + f(root_); + ++evaluationNumber_; + return root_; + } + + } + + QL_FAIL("maximum number of function evaluations (" + << maxEvaluations_ << ") exceeded"); + } + }; +} + +#endif diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp index 773c6957a45..9756e313221 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -103,47 +104,20 @@ namespace QuantLib { // linear approximation const Real denom = std::accumulate(attr.begin(), attr.end(), 0.0); const Real xInit = (std::abs(denom) > 1000*QL_EPSILON) - ? std::max(5.0, std::min(-5.0, - (K_ - std::accumulate(a_.begin(), a_.end(), 0.0))/denom) + ? std::min(10.0, std::max(-10.0, + (K_ - std::accumulate(a_.begin(), a_.end(), 0.0))/denom) ) : 0.0; switch(strategy) { case Brent: - return QuantLib::Brent().solve(*this, xTol, xInit, 0.1); + return QuantLib::Brent().solve(*this, xTol, xInit, 1.0); case Newton: - return QuantLib::Newton().solve(*this, xTol, xInit, 0.1); + return QuantLib::Newton().solve(*this, xTol, xInit, 1.0); case Ridder: - return QuantLib::Ridder().solve(*this, xTol, xInit, 0.1); + return QuantLib::Ridder().solve(*this, xTol, xInit, 1.0); case Halley: - case SuperHalley: { - const Size maxIter = 100; - Size nIter = 0; - bool resultCloseEnough; - Real x = xInit; - Real xOld, fx; - - do { - xOld = x; - fx = (*this)(x); - const Real fPrime = derivative(x); - const Real lf = fx*secondDerivative(x)/(fPrime*fPrime); - const Real step = (strategy == Halley) - ? Real(1/(1 - 0.5*lf)*fx/fPrime) - : Real((1 + 0.5*lf/(1-lf))*fx/fPrime); - - x -= step; - resultCloseEnough = std::fabs(x-xOld) < 0.5*xTol; - } - while (!resultCloseEnough && ++nIter < maxIter); - - if (!resultCloseEnough && !close(std::fabs(fx), 0.0)) { - std::cout << "ouch" << std::endl; - x = QuantLib::Brent().solve(*this, xTol, xInit, 0.1); - - } - return x; - } + return QuantLib::Halley().solve(*this, xTol, xInit, 1.0); default: QL_FAIL("unknown strategy type"); } @@ -161,6 +135,10 @@ namespace QuantLib { const ext::shared_ptr avgPayoff = ext::dynamic_pointer_cast(arguments_.payoff); QL_REQUIRE(avgPayoff, "average basket payoff expected"); + const ext::shared_ptr payoff = + ext::dynamic_pointer_cast(avgPayoff->basePayoff()); + QL_REQUIRE(payoff, "non-plain vanilla payoff given"); + const Real strike = // sort assets by their weight const Array weights = avgPayoff->weights(); @@ -178,6 +156,9 @@ namespace QuantLib { const Array v = pExtractor.getBlackVariance(maturityDate); const DiscountFactor dr0 = pExtractor.getInterestRate(maturityDate); + const Array fwd = s*Exp(dq)/dr0; + const Array a = weights*fwd*Exp(-0.5*v) + const SumExponentialsRootSolver solver(a, Sqrt(v), ); } diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index 5d74952450d..e9a67b2f3c8 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -41,7 +41,7 @@ namespace QuantLib { class SumExponentialsRootSolver { public: - enum Strategy {Ridder, Newton, Brent, Halley, SuperHalley}; + enum Strategy {Ridder, Newton, Brent, Halley}; SumExponentialsRootSolver(Array a, Array sig, Real K); @@ -50,7 +50,7 @@ namespace QuantLib { Real secondDerivative(Real x) const; Real rootSumExponentials( - Real xTol = 1e4*QL_EPSILON, Strategy strategy = Brent) const; + Real xTol = 1e6*QL_EPSILON, Strategy strategy = Brent) const; Size getFCtr() const; Size getDerivativeCtr() const; diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index a7594e47920..d27573979d3 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1741,34 +1741,51 @@ BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { MersenneTwisterUniformRng mt(42); for (auto strategy: { - SumExponentialsRootSolver::Brent, - SumExponentialsRootSolver::Newton, - SumExponentialsRootSolver::Ridder, - SumExponentialsRootSolver::Halley}) { + std::make_tuple("Brent", SumExponentialsRootSolver::Brent), + std::make_tuple("Newton", SumExponentialsRootSolver::Newton), + std::make_tuple("Ridder", SumExponentialsRootSolver::Ridder), + std::make_tuple("Halley", SumExponentialsRootSolver::Halley) + }) { Size fCtr = 0; + const Size n = 10000; + const Real tol = 1e8*QL_EPSILON; + const Real acc = 1e-4*tol; + IncrementalStatistics stats; - // first test arbitrary problem - for (Size i=0; i < 1000; ++i) { - // problem size + for (Size i=0; i < n; ++i) { const Size n = (mt.nextInt32() % 10)+1; Array a(n), sig(n); + const Real offset = (mt.nextReal() < 0.3)? -1.0 : 0.0; for (Size j=0; j < n; ++j) { - a[j] = mt.nextReal() - 1.0; + a[j] = mt.nextReal() + offset; sig[j] = copysign(1.0, a[j])*mt.nextReal(); } const Real kMin = SumExponentialsRootSolver(a, sig, 0.0)(-10.0); const Real kMax = SumExponentialsRootSolver(a, sig, 0.0)( 10.0); const Real K = (kMax - kMin)*mt.nextReal() + kMin; - const SumExponentialsRootSolver solver(a, sig, K); - const Real xRoot = solver.rootSumExponentials( - 1e1*QL_EPSILON, strategy); + const Real xValue = SumExponentialsRootSolver(a, sig, K) + .rootSumExponentials(acc, SumExponentialsRootSolver::Brent); + const SumExponentialsRootSolver solver(a, sig, K); + const Real xRoot = solver.rootSumExponentials(tol, std::get<1>(strategy)); + + stats.add(xValue - xRoot); fCtr += solver.getFCtr() + solver.getDerivativeCtr() + solver.getSecondDerivativeCtr(); } - std::cout << fCtr << std::endl; + if (fCtr > 15*n) { + BOOST_FAIL("too many function calls needed for solver " << std::get<0>(strategy)); + } + + if (stats.standardDeviation() > 10*tol) { + BOOST_FAIL("failed to find root of sum of exponentials" + << "\n solver : " << std::get<0>(strategy) + << std::fixed << std::setprecision(15) + << "\n stdev : " << stats.standardDeviation() + << "\n tolerance: " << tol); + } } } diff --git a/test-suite/solvers.cpp b/test-suite/solvers.cpp index 45aebf6e01d..13f9d135b95 100644 --- a/test-suite/solvers.cpp +++ b/test-suite/solvers.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include using namespace QuantLib; @@ -40,18 +41,24 @@ class F1 { public: Real operator()(Real x) const { return x*x-1.0; } Real derivative(Real x) const { return 2.0*x; } + Real secondDerivative(Real x) const { return 2.0;} }; class F2 { public: Real operator()(Real x) const { return 1.0-x*x; } Real derivative(Real x) const { return -2.0*x; } + Real secondDerivative(Real x) const { return -2.0;} }; class F3 { public: Real operator()(Real x) const { return std::atan(x-1); } Real derivative(Real x) const { return 1.0 / (1.0+(x-1.0)*(x-1.0)); } + Real secondDerivative(Real x) const { + const Real u = x-1.0; + return -2*u/((1.0+u*u)*(1.0+u*u)); + } }; template @@ -96,6 +103,7 @@ class Probe { return previous_ + offset_ - x*x; } Real derivative(Real x) const { return 2.0*x; } + Real secondDerivative(Real x) const { return 2.0; } private: Real& result_; Real previous_; @@ -209,6 +217,12 @@ BOOST_AUTO_TEST_CASE(testSecant) { test_solver(Secant(), "Secant", 1.0e-6); } +BOOST_AUTO_TEST_CASE(testHalley) { + BOOST_TEST_MESSAGE("Testing Halley solver..."); + test_solver(Halley(), "Halley", 1.0e-6); +} + + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From ad57ee34367fc1732759f155c1b1b18268ca7d72 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sun, 11 Aug 2024 16:24:50 +0200 Subject: [PATCH 11/36] add halley solver --- ql/pricingengines/basket/denglizhoubasketengine.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/ql/pricingengines/basket/denglizhoubasketengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp index a782988141c..1485a3f4172 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -159,9 +159,6 @@ namespace QuantLib { nRho[i+1][j+1] = rho[idx][std::get<1>(p[M+j])]; } - //std::cout << N << std::endl << _s << std::endl << _dq << std::endl - // << _v << std::endl << nRho << std::endl << strike << std::endl << std::endl; - const Real callValue = DengLiZhouBasketEngine::calculate_vanilla_call(Log(_s), dr0, _dq, _v, nRho, strike); From 67b32011cd9f827770545f77149b1e1402f31326 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sun, 1 Sep 2024 15:58:10 +0200 Subject: [PATCH 12/36] . --- ql/math/integrals/gaussianquadratures.cpp | 46 ++++++ ql/math/integrals/gaussianquadratures.hpp | 13 ++ .../basket/denglizhoubasketengine.cpp | 4 +- .../basket/singlefactorbsmbasketengine.cpp | 51 +++++-- .../basket/singlefactorbsmbasketengine.hpp | 7 +- .../basket/vectorbsmprocessextractor.cpp | 15 +- .../basket/vectorbsmprocessextractor.hpp | 5 +- test-suite/basketoption.cpp | 122 ++++++++++++---- test-suite/gaussianquadratures.cpp | 135 ++++++++++++++++++ 9 files changed, 345 insertions(+), 53 deletions(-) diff --git a/ql/math/integrals/gaussianquadratures.cpp b/ql/math/integrals/gaussianquadratures.cpp index 2a1819b689b..884bc7d3b8e 100644 --- a/ql/math/integrals/gaussianquadratures.cpp +++ b/ql/math/integrals/gaussianquadratures.cpp @@ -27,6 +27,8 @@ #include #include +#include + namespace QuantLib { GaussianQuadrature::GaussianQuadrature( @@ -59,6 +61,50 @@ namespace QuantLib { } + MulitDimGaussianIntegration::MulitDimGaussianIntegration( + const std::vector& ns, + const std::function(Size)>& genQuad) + : weights_(std::accumulate(ns.begin(), ns.end(), Size(1), std::multiplies<>()), 1.0), + x_(weights_.size(), Array(ns.size())) { + + const Size m = ns.size(); + const Size n = x_.size(); + + std::vector spacing(m); + spacing[0] = 1; + std::partial_sum(ns.begin(), ns.end()-1, spacing.begin()+1, std::multiplies<>()); + + std::map n2weights, n2x; + for (auto order: ns) { + if (n2x.find(order) == n2x.end()) { + const ext::shared_ptr quad = genQuad(order); + n2x[order] = quad->x(); + n2weights[order] = quad->weights(); + } + } + + for (Size i=0; i < n; ++i) { + for (Size j=0; j < m; ++j) { + const Size order = ns[j]; + const Size nx = (i / spacing[j]) % ns[j]; + weights_[i] *= n2weights[order][nx]; + x_[i][j] = n2x[order][nx]; + } + } + } + + Real MulitDimGaussianIntegration::operator()( + const std::function& f) const { + Real s = 0.0; + const Size n = x_.size(); + for (Size i=0; i < n; ++i) + s += weights_[i]*f(x_[i]); + + return s; + } + + + namespace detail { template GaussianQuadratureIntegrator::GaussianQuadratureIntegrator( diff --git a/ql/math/integrals/gaussianquadratures.hpp b/ql/math/integrals/gaussianquadratures.hpp index 605d185d536..3d0a913709a 100644 --- a/ql/math/integrals/gaussianquadratures.hpp +++ b/ql/math/integrals/gaussianquadratures.hpp @@ -76,6 +76,19 @@ namespace QuantLib { Array x_, w_; }; + class MulitDimGaussianIntegration { + public: + MulitDimGaussianIntegration( + const std::vector& ns, + const std::function(Size)>& genQuad); + + Real operator()(const std::function& f) const; + + private: + Array weights_; + std::vector x_; + }; + //! generalized Gauss-Laguerre integration /*! This class performs a 1-dimensional Gauss-Laguerre integration. diff --git a/ql/pricingengines/basket/denglizhoubasketengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp index 1485a3f4172..a068d244100 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -60,9 +60,9 @@ namespace QuantLib { const detail::VectorBsmProcessExtractor pExtractor(processes_); const Array s = pExtractor.getSpot(); - const Array dq = pExtractor.getDividendYield(maturityDate); + const Array dq = pExtractor.getDividendYieldDf(maturityDate); const Array v = pExtractor.getBlackVariance(maturityDate); - const DiscountFactor dr0 = pExtractor.getInterestRate(maturityDate); + const DiscountFactor dr0 = pExtractor.getInterestRateDf(maturityDate); std::vector< std::tuple > p; p.reserve(n_); diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp index 9756e313221..560cee30512 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -26,11 +26,10 @@ #include #include #include +#include #include #include -#include - namespace QuantLib { SumExponentialsRootSolver::SumExponentialsRootSolver( @@ -80,9 +79,7 @@ namespace QuantLib { return fDoublePrimeCtr_; } - Real SumExponentialsRootSolver::rootSumExponentials( - Real xTol, Strategy strategy) const { - + Real SumExponentialsRootSolver::getRoot(Real xTol, Strategy strategy) const { const Array attr = a_*sig_; QL_REQUIRE( std::all_of( @@ -125,8 +122,10 @@ namespace QuantLib { SingleFactorBsmBasketEngine::SingleFactorBsmBasketEngine( - std::vector > p) - : n_(p.size()), processes_(std::move(p)) { + std::vector > p, + Real xTol) + : xTol_(xTol), + n_(p.size()), processes_(std::move(p)) { for (const auto& process: processes_) registerWith(process); } @@ -138,7 +137,8 @@ namespace QuantLib { const ext::shared_ptr payoff = ext::dynamic_pointer_cast(avgPayoff->basePayoff()); QL_REQUIRE(payoff, "non-plain vanilla payoff given"); - const Real strike = + const Real cp = (payoff->optionType() == Option::Call) ? 1.0 : -1.0; + const Real strike = payoff->strike(); // sort assets by their weight const Array weights = avgPayoff->weights(); @@ -152,15 +152,38 @@ namespace QuantLib { const detail::VectorBsmProcessExtractor pExtractor(processes_); const Array s = pExtractor.getSpot(); - const Array dq = pExtractor.getDividendYield(maturityDate); - const Array v = pExtractor.getBlackVariance(maturityDate); - const DiscountFactor dr0 = pExtractor.getInterestRate(maturityDate); + const Array dq = pExtractor.getDividendYieldDf(maturityDate); + const DiscountFactor dr0 = pExtractor.getInterestRateDf(maturityDate); + + const Array stdDev = pExtractor.getBlackStdDev(maturityDate); + const Array v = stdDev*stdDev; - const Array fwd = s*Exp(dq)/dr0; - const Array a = weights*fwd*Exp(-0.5*v) - const SumExponentialsRootSolver solver(a, Sqrt(v), ); + const Array fwdBasket = weights * s * dq /dr0; + // first check if all vols are zero -> intrinsic case + if (std::all_of( + stdDev.begin(), stdDev.end(), + [](Real x) -> bool { return close_enough(x, 0.0); } + )) { + results_.value = dr0*payoff->operator()( + std::accumulate(fwdBasket.begin(), fwdBasket.end(), 0.0)); + } + else { + const Real d = -cp * SumExponentialsRootSolver( + fwdBasket*Exp(-0.5*v), stdDev, strike) + .getRoot(xTol_, SumExponentialsRootSolver::Halley); + + const CumulativeNormalDistribution N; + + results_.value = cp * dr0 * + std::inner_product( + fwdBasket.begin(), fwdBasket.end(), stdDev.begin(), + -strike*N(d), + std::plus<>(), + [d, cp, &N](Real x, Real y) -> Real { return x*N(d+cp*y); } + ); + } } } diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index e9a67b2f3c8..2a65ca8726d 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -49,8 +49,7 @@ namespace QuantLib { Real derivative(Real x) const; Real secondDerivative(Real x) const; - Real rootSumExponentials( - Real xTol = 1e6*QL_EPSILON, Strategy strategy = Brent) const; + Real getRoot(Real xTol = 1e6*QL_EPSILON, Strategy strategy = Brent) const; Size getFCtr() const; Size getDerivativeCtr() const; @@ -65,11 +64,13 @@ namespace QuantLib { class SingleFactorBsmBasketEngine : public BasketOption::engine { public: SingleFactorBsmBasketEngine( - std::vector > p); + std::vector > p, + Real xTol = 1e6*QL_EPSILON); void calculate() const override; private: + const Real xTol_; const Size n_; const std::vector > processes_; }; diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp index 145a71391f0..da4a9e89678 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp @@ -41,7 +41,7 @@ namespace QuantLib { return x; } - DiscountFactor VectorBsmProcessExtractor::getInterestRate( + DiscountFactor VectorBsmProcessExtractor::getInterestRateDf( const Date& maturityDate) const { const Array dr = extractProcesses( [maturityDate](const auto& p) -> DiscountFactor { @@ -64,7 +64,7 @@ namespace QuantLib { return extractProcesses([](const auto& p) -> Real { return p->x0(); }); } - Array VectorBsmProcessExtractor::getDividendYield( + Array VectorBsmProcessExtractor::getDividendYieldDf( const Date& maturityDate) const { return extractProcesses( [maturityDate](const auto& p) -> DiscountFactor { @@ -81,5 +81,16 @@ namespace QuantLib { } ); } + + Array VectorBsmProcessExtractor::getBlackStdDev( + const Date& maturityDate) const { + return extractProcesses( + [maturityDate](const auto& p) -> Volatility { + const Time maturity = p->blackVolatility()->timeFromReference(maturityDate); + return p->blackVolatility()->blackVol(maturityDate, p->x0())*std::sqrt(maturity); + } + ); + + } } } diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp index 7989aabedcc..3715c69b295 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp @@ -36,8 +36,9 @@ namespace QuantLib { Array getSpot() const; Array getBlackVariance(const Date& maturityDate) const; - Array getDividendYield(const Date& maturityDate) const; - DiscountFactor getInterestRate(const Date& maturityDate) const; + Array getBlackStdDev(const Date& maturityDate) const; + Array getDividendYieldDf(const Date& maturityDate) const; + DiscountFactor getInterestRateDf(const Date& maturityDate) const; private: Array extractProcesses( diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 66331151fd0..ef6c1909935 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1732,10 +1732,10 @@ BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { BOOST_TEST_MESSAGE("Testing the root of a sum of exponentials..."); BOOST_CHECK_THROW(SumExponentialsRootSolver( - {2.0, 3.0, 4.0}, {0.2, 0.4, -0.1}, 0.0).rootSumExponentials() , Error + {2.0, 3.0, 4.0}, {0.2, 0.4, -0.1}, 0.0).getRoot() , Error ); BOOST_CHECK_THROW(SumExponentialsRootSolver( - {2.0, -3.0, 4.0}, {0.2, -0.4, -0.1}, 0.0).rootSumExponentials(), Error + {2.0, -3.0, 4.0}, {0.2, -0.4, -0.1}, 0.0).getRoot(), Error ); MersenneTwisterUniformRng mt(42); @@ -1766,10 +1766,10 @@ BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { const Real K = (kMax - kMin)*mt.nextReal() + kMin; const Real xValue = SumExponentialsRootSolver(a, sig, K) - .rootSumExponentials(acc, SumExponentialsRootSolver::Brent); + .getRoot(acc, SumExponentialsRootSolver::Brent); const SumExponentialsRootSolver solver(a, sig, K); - const Real xRoot = solver.rootSumExponentials(tol, std::get<1>(strategy)); + const Real xRoot = solver.getRoot(tol, std::get<1>(strategy)); stats.add(xValue - xRoot); fCtr += solver.getFCtr() + solver.getDerivativeCtr() + solver.getSecondDerivativeCtr(); @@ -1794,43 +1794,105 @@ BOOST_AUTO_TEST_CASE(testSingleFactorBsmBasketEngine) { BOOST_TEST_MESSAGE( "Testing single factor BSM basket engine against reference results..."); - // Reference results are calculated with the python library PyFENG - // https://github.com/PyFE/PyFENG - const DayCounter dc = Actual365Fixed(); const Date today = Date(3, July, 2024); const Date maturity = today + Period(18, Months); + const Time deltaT = dc.yearFraction(today, maturity); + const Real sqrtDeltaT = std::sqrt(deltaT); + + struct TestCase { + const Array underlyings; + const Array volatilities; + const Array q; + const Rate r; + const Array weights; + Option::Type optionType; + }; - const std::vector underlyings({220, 45, 102, 75}); - const std::vector volatilities({0.3, 0.45, 0.3, 0.25}); - const std::vector q({0.05, 0.075, 0.02, 0.1}); + const std::vector testCases = { + { {200, 50, -125}, {0.4, 0.3, -0.5}, {0.03, 0.075, 0.04}, + 0.05, {0.5, 0.25, 1.0}, Option::Call }, + { {200, 50, -125}, {0.4, 0.3, -0.5}, {0.03, 0.075, 0.04}, + 0.05, {0.5, 0.25, 1.0}, Option::Put }, + { {100, 50}, {0.4, -0.3}, {0.03, 0.075}, 0.025, {1.0, -2.0}, Option::Put }, + { {100, 50}, {0.4, -0.3}, {0.03, 0.075}, 0.025, {1.0, -2.0}, Option::Call }, + { {100}, {0.4}, {0.03}, 0.045, {1.0}, Option::Call }, + { {100, 50, 100, 150}, {0.4, 0.0, 0.2, 0.1}, {0.03, 0.05, 0.02, 0}, 0.045, + {1.0, 2.0, 1.0, 1.0}, Option::Call }, + { {100, 50}, {0.0, 0.0}, {0.03, 0.05}, 0.055, {1.0, 1.95}, Option::Call } + }; - const Handle rTS = Handle( - flatRate(today, 0.03, dc)); + for (const auto& t: testCases) { + const Handle rTS + = Handle(flatRate(today, t.r, dc)); + + std::vector > processes; + for (Size d=0; d < t.underlyings.size(); ++d) + processes.push_back( + ext::make_shared( + Handle(ext::make_shared(t.underlyings[d])), + Handle(flatRate(today, t.q[d], dc)), rTS, + Handle(flatVol(today, t.volatilities[d], dc)) + ) + ); - std::vector > processes; - for (Size d=0; d < 4; ++d) - processes.push_back( - ext::make_shared( - Handle(ext::make_shared(underlyings[d])), - Handle(flatRate(today, q[d], dc)), rTS, - Handle(flatVol(today, volatilities[d], dc)) - ) + const Real strike = std::inner_product( + t.weights.begin(), t.weights.end(), t.underlyings.begin(), 0.0 ); - const Real strike = 150; + const ext::shared_ptr payoff + = ext::make_shared(t.optionType, strike); - BasketOption option( - ext::make_shared( - ext::make_shared(Option::Call, strike), - Array({0.5, 2.0, 1.0, 1.5}) - ), - ext::make_shared(maturity) - ); + BasketOption option( + ext::make_shared(payoff, t.weights), + ext::make_shared(maturity) + ); - option.setPricingEngine( - ext::make_shared(processes)); + option.setPricingEngine( + ext::make_shared(processes)); + + const Real calculated = option.NPV(); + + Array f(processes.size()); + for (Size i=0; i < f.size(); ++i) { + f[i] = t.weights[i] + * processes[i]->stateVariable()->value() + * processes[i]->dividendYield()->discount(maturity) + / rTS->discount(maturity) + * std::exp(-0.5*processes[i]->blackVolatility()->blackVariance(maturity, 0)); + } + + const SobolRsg rsg(1); + const InverseCumulativeNormal invCumNormal; + IncrementalStatistics stats; + + const Size nPath = 10000; + const DiscountFactor df = rTS->discount(maturity); + for (Size i = 0; i < nPath; ++i) { + const Real z = sqrtDeltaT*invCumNormal(rsg.nextSequence().value[0]); + Real s = 0.0; + for (Size j=0; j < processes.size(); ++j) + s += f[j] * std::exp(t.volatilities[j]*z); + + stats.add(df * payoff->operator()(s)); + } + + const Real expected = stats.mean(); + std::cout << std::setprecision(6) << calculated << " " << expected << std::endl; + const Real diff = std::abs(expected - calculated); + + const Real errorEstimate = stats.errorEstimate(); + const Real tol = std::max(1e-10, 0.1*errorEstimate); + if (diff > tol) { + BOOST_FAIL("failed to reproduce single factor basket prices" + << std::fixed << std::setprecision(8) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); + } + } } BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/gaussianquadratures.cpp b/test-suite/gaussianquadratures.cpp index c9bb7846ba0..f0b4361ded8 100644 --- a/test-suite/gaussianquadratures.cpp +++ b/test-suite/gaussianquadratures.cpp @@ -21,6 +21,7 @@ #include "utilities.hpp" #include #include +#include #include #include #include @@ -322,6 +323,140 @@ BOOST_AUTO_TEST_CASE(testNonCentralChiSquaredSumOfNodes) { } } } + + +BOOST_AUTO_TEST_CASE(testMultiDimensionalGaussIntegration) { + BOOST_TEST_MESSAGE("Testing multi-dimensional Gaussian quadrature..."); + + const auto normal = [](const Array& x) -> Real { + return std::exp(-DotProduct(x, x)); + }; + + for (Size n=1; n < 5; ++n) { + std::vector ns(n); + std::iota(ns.begin(), ns.end(), Size(1)); + + MulitDimGaussianIntegration quad( + ns, + [](const Size n) { + return ext::make_shared(n); + } + ); + + const Real calculated = quad(normal); + const Real expected = std::sqrt(std::pow(M_PI, Real(n))); + const Real tol = 1e4*QL_EPSILON; + const Real diff = std::abs(expected-calculated); + if (diff > tol) { + BOOST_ERROR("failed to reproduce multi dimensional Gaussian quadrature" + << std::setprecision(12) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff); + } + } + + // testing some Gaussian Integrals + // https://en.wikipedia.org/wiki/Gaussian_integral + MersenneTwisterUniformRng rng(1234); + const std::vector ns = {20, 28, 16, 22}; + const std::vector tols = {1e-8, 1e-6, 1e-2, 5e-2}; + for (Size n=1; n < 5; ++n) { + // create symmetric positive-definite matrix + Matrix a(n, n); + for (Size i=0; i < n; ++i) + for (Size j=0; j < n; ++j) + a[i][j] = (i==j) ? (i+1) : rng.nextReal(); + + const Matrix A = a*transpose(a); + const Matrix invA = inverse(A); + const Real det_2piA = std::sqrt(determinant(M_TWOPI*invA)); + + const MulitDimGaussianIntegration quad( + std::vector(ns.begin(), ns.begin()+n), + [](const Size n) { return ext::make_shared(n); } + ); + + const Real calculated = quad( + [&A](const Array& x) -> Real { return std::exp(-0.5*DotProduct(x, A*x)); } + ); + + const Real expected = det_2piA; + const Real diff = std::abs(calculated - expected); + if (diff > tols[n-1]) { + BOOST_ERROR("failed to reproduce multi dimensional Gaussian quadrature" + << "\n dimensions: " << n + << std::setprecision(12) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tols[n-1]); + } + } + + + Matrix a(3, 3); + for (Size i=0; i < 3; ++i) + for (Size j=0; j < 3; ++j) + a[i][j] = (i==j) ? (i+1) : rng.nextReal(); + + const Matrix A = a*transpose(a); + const Matrix invA = inverse(A); + const Real sqrt_det_2piA = std::sqrt(determinant(M_TWOPI*invA)); + + const MulitDimGaussianIntegration quadHigh( + std::vector({22, 18, 26}), + [](const Size n) { return ext::make_shared(n); } + ); + const MulitDimGaussianIntegration quad2( + std::vector(3, 2), + [](const Size n) { return ext::make_shared(n); } + ); + + for (Size i=0; i < 3; ++i) + for (Size j=0; j < 3; ++j) { + const Real expected = sqrt_det_2piA*invA[i][j]; + + Real calculated = quadHigh( + [&A, i, j](const Array& x) -> Real { + return x[i]*x[j]*std::exp(-0.5*DotProduct(x, A*x)); + } + ); + + Real diff = std::abs(calculated - expected); + Real tol = 1e-4; + if (diff > tol) { + BOOST_ERROR("failed to reproduce multi dimensional Gaussian quadrature" + << std::setprecision(12) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); + } + + Matrix inva = inverse(transpose(a)); + calculated = quad2( + [&inva, i, j](const Array& x) -> Real { + const Array f = M_SQRT2*inva*x; + return f[i]*f[j]*std::exp(-DotProduct(x, x)); + } + ); + + calculated *= determinant(M_SQRT2*inva); + diff = std::abs(calculated - expected); + tol = QL_EPSILON*1e4; + if (diff > tol) { + BOOST_ERROR("failed to reproduce multi dimensional Gaussian quadrature" + << std::setprecision(12) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); + } + } +} + + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From 4dcbb23de4284097e4d5a8ddbea32ddaef3753e3 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Tue, 10 Sep 2024 22:29:48 +0200 Subject: [PATCH 13/36] first light Choi engine --- ql/CMakeLists.txt | 2 + ql/pricingengines/basket/choibasketengine.cpp | 45 ++++++++++++++ ql/pricingengines/basket/choibasketengine.hpp | 59 +++++++++++++++++++ .../basket/denglizhoubasketengine.cpp | 4 +- .../basket/denglizhoubasketengine.hpp | 10 ++-- 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 ql/pricingengines/basket/choibasketengine.cpp create mode 100644 ql/pricingengines/basket/choibasketengine.hpp diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index afaabc418f0..6c9f26d9728 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -674,6 +674,7 @@ set(QL_SOURCES pricingengines/barrier/fdhestonrebateengine.cpp pricingengines/barrier/mcbarrierengine.cpp pricingengines/basket/bjerksundstenslandspreadengine.cpp + pricingengines/basket/choibasketengine.cpp pricingengines/basket/vectorbsmprocessextractor.cpp pricingengines/basket/denglizhoubasketengine.cpp pricingengines/basket/fd2dblackscholesvanillaengine.cpp @@ -1888,6 +1889,7 @@ set(QL_HEADERS pricingengines/barrier/fdhestonrebateengine.hpp pricingengines/barrier/mcbarrierengine.hpp pricingengines/basket/bjerksundstenslandspreadengine.hpp + pricingengines/basket/choibasketengine.hpp pricingengines/basket/vectorbsmprocessextractor.hpp pricingengines/basket/denglizhoubasketengine.hpp pricingengines/basket/fd2dblackscholesvanillaengine.hpp diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp new file mode 100644 index 00000000000..7ce5ecdc689 --- /dev/null +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -0,0 +1,45 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file choibasketengine.cpp +*/ + +#include + +namespace QuantLib { + + ChoiBasketEngine::ChoiBasketEngine( + std::vector > processes, + Matrix rho) + : n_(processes.size()), + processes_(std::move(processes)), + rho_(std::move(rho)) { + + QL_REQUIRE(n_ > 0, "No Black-Scholes process is given."); + QL_REQUIRE(n_ == rho_.size1() && rho_.size1() == rho_.size2(), + "process and correlation matrix must have the same size."); + + std::for_each(processes_.begin(), processes_.end(), + [this](const auto& p) { registerWith(p); }); + } + + void ChoiBasketEngine::calculate() const { + + } +} diff --git a/ql/pricingengines/basket/choibasketengine.hpp b/ql/pricingengines/basket/choibasketengine.hpp new file mode 100644 index 00000000000..ba63540d9b6 --- /dev/null +++ b/ql/pricingengines/basket/choibasketengine.hpp @@ -0,0 +1,59 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file choibasketengine.hpp + \brief Jaehyuk Choi: Sum of all Black-Scholes-Merton Models +*/ + +#ifndef quantlib_choi_basket_engine_hpp +#define quantlib_choi_basket_engine_hpp + +#include +#include + +namespace QuantLib { + + //! Pricing engine for basket option on multiple underlyings + /*! This class implements the pricing formula from + "Sum of all Black-Scholes-Merton Models: An efficient Pricing Method for + Spread, Basket and Asian Options", + Jaehyuk Choi, 2018 + https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2913048 + + \ingroup basketengines + + \test the correctness of the returned value is tested by + reproducing results available in literature. + */ + class ChoiBasketEngine : public BasketOption::engine { + public: + ChoiBasketEngine( + std::vector > processes, + Matrix rho); + + void calculate() const override; + + private: + const Size n_; + const std::vector > processes_; + const Matrix rho_; + }; +} + +#endif diff --git a/ql/pricingengines/basket/denglizhoubasketengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp index a068d244100..1a06407569d 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -39,8 +39,8 @@ namespace QuantLib { QL_REQUIRE(n_ == rho_.size1() && rho_.size1() == rho_.size2(), "process and correlation matrix must have the same size."); - for (Size i=0; i < n_; ++i) - registerWith(processes_[i]); + std::for_each(processes_.begin(), processes_.end(), + [this](const auto& p) { registerWith(p); }); } void DengLiZhouBasketEngine::calculate() const { diff --git a/ql/pricingengines/basket/denglizhoubasketengine.hpp b/ql/pricingengines/basket/denglizhoubasketengine.hpp index 63fb5b59ffa..bbcc067e2ed 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.hpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.hpp @@ -29,7 +29,7 @@ namespace QuantLib { - //! Pricing engine for basket option on two futures + //! Pricing engine for basket option on multiple underlyings /*! This class implements the pricing formula from "Multi-asset Spread Option Pricing and Hedging", S. Deng, M. Li, J.Zhou, 2008 @@ -37,15 +37,13 @@ namespace QuantLib { The typo in formula (37) for J^2 is corrected - This pricing formula works only if exactly one asset weight is positive. + This pricing formula only works if exactly one asset weight is positive. If more than one weight is positive then a mapping of the sum of correlated - log-normal processes onto one log-normal process has to has be carried out. - This implementation is using - + log-normal processes onto one log-normal process has to be carried out. + This implementation is using: "WKB Approximation for the Sum of Two Correlated Lognormal Random Variables", C.F. Lo 2013 https://www.m-hikari.com/ams/ams-2013/ams-125-128-2013/loAMS125-128-2013.pdf - for this task. \ingroup basketengines From 92aed5772ad2631171f316926d877569796200fe Mon Sep 17 00:00:00 2001 From: klausspanderen Date: Sat, 21 Sep 2024 13:41:50 +0200 Subject: [PATCH 14/36] . --- ql/CMakeLists.txt | 1 + ql/math/matrixutilities/householder.hpp | 94 +++++++++++++++++++++++++ test-suite/matrices.cpp | 66 +++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 ql/math/matrixutilities/householder.hpp diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 6c9f26d9728..6ef7fc76bfb 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -1486,6 +1486,7 @@ set(QL_HEADERS math/matrixutilities/expm.hpp math/matrixutilities/getcovariance.hpp math/matrixutilities/gmres.hpp + math/matrixutilities/householder.hpp math/matrixutilities/pseudosqrt.hpp math/matrixutilities/qrdecomposition.hpp math/matrixutilities/sparseilupreconditioner.hpp diff --git a/ql/math/matrixutilities/householder.hpp b/ql/math/matrixutilities/householder.hpp new file mode 100644 index 00000000000..3430e9b061c --- /dev/null +++ b/ql/math/matrixutilities/householder.hpp @@ -0,0 +1,94 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file householder.hpp + \brief Householder transformation and Householder projection +*/ + +#ifndef quantlib_householder_hpp +#define quantlib_householder_hpp + +#include +#include + +#include + +namespace QuantLib { + + /*! References: + https://en.wikipedia.org/wiki/Householder_transformation + */ + + class HouseholderTransformation { + public: + HouseholderTransformation(const Array v) + : v_(std::move(v)) {} + + Array operator()(const Array& x) const { + return x - (2.0*DotProduct(v_, x))*v_; + } + private: + const Array v_; + }; + + + class HouseholderReflection { + public: + HouseholderReflection(const Array e) + : e_(std::move(e)) {} + + Array reflectionVector(const Array& a) const { + const Real na = Norm2(a); + QL_REQUIRE(na > 0, "vector of length zero given"); + + const Real aDotE = DotProduct(a, e_); + const Array a1 = aDotE*e_; + const Array a2 = a - a1; + + const Real eps = DotProduct(a2, a2) / (aDotE*aDotE); + if (eps < QL_EPSILON*QL_EPSILON) { + return Array(a.size(), 0.0); + } + else if (eps < 1e-4) { + const Real eps2 = eps*eps; + const Real eps3 = eps*eps2; + const Real eps4 = eps2*eps2; + const Array v = + (a2 - a1*(eps/2.0 - eps2/8.0 + eps3/16.0 - 5/128.0*eps4)) + / (aDotE*std::sqrt(eps + eps2/4.0 - eps3/8.0 + 5/64.0*eps4)); + return v; + } + else { + const Array c = a - na*e_; + return c / Norm2(c); + } + } + + Array operator()(const Array& a) const { + const Array v = reflectionVector(a); + return HouseholderTransformation(v)(a); + } + + private: + const Array e_; + }; + +} + +#endif diff --git a/test-suite/matrices.cpp b/test-suite/matrices.cpp index 54e9937ba1b..e4b2ea128ab 100644 --- a/test-suite/matrices.cpp +++ b/test-suite/matrices.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -911,6 +912,71 @@ BOOST_AUTO_TEST_CASE(testCholeskySolverForIncomplete) { QL_CHECK_CLOSE_MATRIX((L*transpose(L)), rho); } +namespace { + void QL_CHECK_CLOSE_ARRAY_TOL( + const Array& actual, const Array& expected, Real tol) { + BOOST_REQUIRE(actual.size() == expected.size()); + for (auto i = 0u; i < actual.size(); i++) { + BOOST_CHECK_SMALL(actual[i] - expected[i], tol); + } + } +} + +BOOST_AUTO_TEST_CASE(testHouseholderTransformation) { + BOOST_TEST_MESSAGE("Testing Householder Transformation..."); + + MersenneTwisterUniformRng rng(1234); + + const auto I = [](Size i) -> Matrix { + Matrix id(i, i, 0.0); + for (Size j=0; j < i; ++j) + id[j][j] = 1.0; + + return id; + }; + + for (Size i=1; i < 10; ++i) { + Array v(i), x(i); + for (Size j=0; j < i; ++j) { + v[j] = rng.nextReal()-0.5; + x[j] = rng.nextReal()-0.5; + } + + const Array expected = (I(i)- 2.0*outerProduct(v, v))*x; + const Array calculated = HouseholderTransformation(v)(x); + QL_CHECK_CLOSE_ARRAY_TOL(calculated, expected, 1e4*QL_EPSILON); + } +} + +BOOST_AUTO_TEST_CASE(testHouseholderReflection) { + BOOST_TEST_MESSAGE("Testing Householder Reflection..."); + + const Real tol=1e4*QL_EPSILON; + + const auto e = [](Size n, Size m=0) -> Array { + Array e(n, 0.0); + e[m] = 1.0; + return e; + }; + +// for (Size i=0; i < 5; ++i) { +// QL_CHECK_CLOSE_ARRAY_TOL( +// HouseholderReflection(e(5))(e(5, i)), e(5), tol); +// QL_CHECK_CLOSE_ARRAY_TOL( +// HouseholderReflection(e(5))(M_PI*e(5, i)), M_PI*e(5), tol); +// QL_CHECK_CLOSE_ARRAY_TOL( +// HouseholderReflection(e(5))( +// e(5, i) + e(5)), +// ((i==0)? 2.0 : M_SQRT2)*e(5), tol); +// } + + // limits + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(3))(Array({10.0, 1e-50, 0.0})), 10.0*e(3), tol); + + + +} BOOST_AUTO_TEST_SUITE_END() From b1f1ae034aa92a5008ebf8d7b2e0d56aa662a538 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Thu, 10 Oct 2024 00:30:38 +0200 Subject: [PATCH 15/36] secure intermediate results --- ql/math/matrixutilities/householder.cpp | 78 +++++ ql/math/matrixutilities/householder.hpp | 53 +--- .../basket/bjerksundstenslandspreadengine.cpp | 14 +- .../basket/bjerksundstenslandspreadengine.hpp | 9 +- ql/pricingengines/basket/choibasketengine.cpp | 150 ++++++++- ql/pricingengines/basket/choibasketengine.hpp | 10 +- ql/pricingengines/basket/kirkengine.cpp | 14 +- ql/pricingengines/basket/kirkengine.hpp | 9 +- .../basket/operatorsplittingspreadengine.cpp | 24 +- .../basket/operatorsplittingspreadengine.hpp | 9 +- .../spreadblackscholesvanillaengine.cpp | 33 +- .../spreadblackscholesvanillaengine.hpp | 12 +- .../basket/vectorbsmprocessextractor.cpp | 1 - .../basket/vectorbsmprocessextractor.hpp | 3 +- test-suite/basketoption.cpp | 299 ++++++++++++++++-- test-suite/matrices.cpp | 47 ++- 16 files changed, 614 insertions(+), 151 deletions(-) create mode 100644 ql/math/matrixutilities/householder.cpp diff --git a/ql/math/matrixutilities/householder.cpp b/ql/math/matrixutilities/householder.cpp new file mode 100644 index 00000000000..835a72ae15c --- /dev/null +++ b/ql/math/matrixutilities/householder.cpp @@ -0,0 +1,78 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include + +namespace QuantLib { + + HouseholderTransformation::HouseholderTransformation(const Array v) + : v_(std::move(v)) {} + + + Array HouseholderTransformation::operator()(const Array& x) const { + return x - (2.0*DotProduct(v_, x))*v_; + } + + Matrix HouseholderTransformation::getMatrix() const { + const Array y = v_ / Norm2(v_); + const Size n = y.size(); + + Matrix m(n, n); + for (Size i=0; i < n; ++i) + for (Size j=0; j < n; ++j) + m[i][j] = ((i == j)? 1.0: 0.0) - 2*y[i]*y[j]; + + return m; + } + + HouseholderReflection::HouseholderReflection(const Array e) + : e_(std::move(e)) {} + + Array HouseholderReflection::reflectionVector(const Array& a) const { + const Real na = Norm2(a); + QL_REQUIRE(na > 0, "vector of length zero given"); + + const Real aDotE = DotProduct(a, e_); + const Array a1 = aDotE*e_; + const Array a2 = a - a1; + + const Real eps = DotProduct(a2, a2) / (aDotE*aDotE); + if (eps < QL_EPSILON*QL_EPSILON) { + return Array(a.size(), 0.0); + } + else if (eps < 1e-4) { + const Real eps2 = eps*eps; + const Real eps3 = eps*eps2; + const Real eps4 = eps2*eps2; + const Array v = + (a2 - a1*(eps/2.0 - eps2/8.0 + eps3/16.0 - 5/128.0*eps4)) + / (aDotE*std::sqrt(eps + eps2/4.0 - eps3/8.0 + 5/64.0*eps4)); + return v; + } + else { + const Array c = a - na*e_; + return c / Norm2(c); + } + } + + Array HouseholderReflection::operator()(const Array& a) const { + const Array v = reflectionVector(a); + return HouseholderTransformation(v)(a); + } +} diff --git a/ql/math/matrixutilities/householder.hpp b/ql/math/matrixutilities/householder.hpp index 3430e9b061c..d98c93fb468 100644 --- a/ql/math/matrixutilities/householder.hpp +++ b/ql/math/matrixutilities/householder.hpp @@ -24,10 +24,7 @@ #ifndef quantlib_householder_hpp #define quantlib_householder_hpp -#include -#include - -#include +#include namespace QuantLib { @@ -37,12 +34,11 @@ namespace QuantLib { class HouseholderTransformation { public: - HouseholderTransformation(const Array v) - : v_(std::move(v)) {} + HouseholderTransformation(const Array v); + + Matrix getMatrix() const; + Array operator()(const Array& x) const; - Array operator()(const Array& x) const { - return x - (2.0*DotProduct(v_, x))*v_; - } private: const Array v_; }; @@ -50,45 +46,14 @@ namespace QuantLib { class HouseholderReflection { public: - HouseholderReflection(const Array e) - : e_(std::move(e)) {} - - Array reflectionVector(const Array& a) const { - const Real na = Norm2(a); - QL_REQUIRE(na > 0, "vector of length zero given"); - - const Real aDotE = DotProduct(a, e_); - const Array a1 = aDotE*e_; - const Array a2 = a - a1; - - const Real eps = DotProduct(a2, a2) / (aDotE*aDotE); - if (eps < QL_EPSILON*QL_EPSILON) { - return Array(a.size(), 0.0); - } - else if (eps < 1e-4) { - const Real eps2 = eps*eps; - const Real eps3 = eps*eps2; - const Real eps4 = eps2*eps2; - const Array v = - (a2 - a1*(eps/2.0 - eps2/8.0 + eps3/16.0 - 5/128.0*eps4)) - / (aDotE*std::sqrt(eps + eps2/4.0 - eps3/8.0 + 5/64.0*eps4)); - return v; - } - else { - const Array c = a - na*e_; - return c / Norm2(c); - } - } - - Array operator()(const Array& a) const { - const Array v = reflectionVector(a); - return HouseholderTransformation(v)(a); - } + HouseholderReflection(const Array e); + + Array operator()(const Array& a) const; + Array reflectionVector(const Array& a) const; private: const Array e_; }; - } #endif diff --git a/ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp b/ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp index f6503369ff1..fe9715a5e06 100644 --- a/ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp +++ b/ql/pricingengines/basket/bjerksundstenslandspreadengine.cpp @@ -23,20 +23,20 @@ namespace QuantLib { BjerksundStenslandSpreadEngine::BjerksundStenslandSpreadEngine( - ext::shared_ptr process1, - ext::shared_ptr process2, + ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation) : SpreadBlackScholesVanillaEngine(process1, process2, correlation) { } Real BjerksundStenslandSpreadEngine::calculate( - Real k, Option::Type optionType, + Real f1, Real f2, Real k, Option::Type optionType, Real variance1, Real variance2, DiscountFactor df) const { const Real cp = (optionType == Option::Call) ? 1 : -1; - const Real a = f2_ + k; - const Real b = f2_/a; + const Real a = f2 + k; + const Real b = f2/a; const Real sigma1 = std::sqrt(variance1); const Real sigma2 = std::sqrt(variance2); @@ -44,7 +44,7 @@ namespace QuantLib { const Real stdev = std::sqrt( variance1 + b*b*variance2 - 2*rho_*b*sigma1*sigma2); - const Real lfa = std::log(f1_/a); + const Real lfa = std::log(f1/a); const Real d1 = (lfa + (0.5*variance1 + 0.5*b*b*variance2 - b*rho_*sigma1*sigma2))/stdev; @@ -53,7 +53,7 @@ namespace QuantLib { const Real d3 = (lfa + (-0.5*variance1 + 0.5*b*b*variance2))/stdev; const CumulativeNormalDistribution phi; - return df*cp*(f1_*phi(cp*d1) - f2_*phi(cp*d2) - k*phi(cp*d3)); + return df*cp*(f1*phi(cp*d1) - f2*phi(cp*d2) - k*phi(cp*d3)); } } diff --git a/ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp b/ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp index c810b0ffca4..fdf8d011f91 100644 --- a/ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp +++ b/ql/pricingengines/basket/bjerksundstenslandspreadengine.hpp @@ -38,14 +38,13 @@ namespace QuantLib { class BjerksundStenslandSpreadEngine : public SpreadBlackScholesVanillaEngine { public: BjerksundStenslandSpreadEngine( - ext::shared_ptr process1, - ext::shared_ptr process2, + ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation); protected: - Real calculate( - Real strike, Option::Type optionType, - Real variance1, Real variance2, DiscountFactor df) const override; + Real calculate(Real f1, Real f2, Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const override; }; } diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index 7ce5ecdc689..e27dd23fc64 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -20,16 +20,31 @@ /*! \file choibasketengine.cpp */ +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include + +#include +#include +#include namespace QuantLib { ChoiBasketEngine::ChoiBasketEngine( std::vector > processes, - Matrix rho) + Matrix rho, Real lambda) : n_(processes.size()), processes_(std::move(processes)), - rho_(std::move(rho)) { + rho_(std::move(rho)), + lambda_(lambda) { QL_REQUIRE(n_ > 0, "No Black-Scholes process is given."); QL_REQUIRE(n_ == rho_.size1() && rho_.size1() == rho_.size2(), @@ -40,6 +55,137 @@ namespace QuantLib { } void ChoiBasketEngine::calculate() const { + const ext::shared_ptr exercise = + ext::dynamic_pointer_cast(arguments_.exercise); + QL_REQUIRE(exercise, "not an European exercise"); + + const Date maturityDate = exercise->lastDate(); + + const detail::VectorBsmProcessExtractor pExtractor(processes_); + const Array s = pExtractor.getSpot(); + const Array dq = pExtractor.getDividendYieldDf(maturityDate); + const Array vol = pExtractor.getBlackStdDev(maturityDate); + const DiscountFactor dr0 = pExtractor.getInterestRateDf(maturityDate); + + const Array fwd = s * dq/dr0; + + const ext::shared_ptr avgPayoff = + ext::dynamic_pointer_cast(arguments_.payoff); + QL_REQUIRE(avgPayoff, "average basket payoff expected"); + + const Array weights = avgPayoff->weights(); + QL_REQUIRE(n_ == weights.size() && n_ > 1, + "wrong number of weights arguments in payoff"); + + const Array g = weights*fwd / Norm2(weights*fwd); + + const Matrix Sigma = getCovariance(vol.begin(), vol.end(), rho_); + Array vStar1 = Sigma*g; + vStar1 /= std::sqrt(DotProduct(g, vStar1)); + + const Matrix C = CholeskyDecomposition(Sigma); + + // todo: this needs to be scaled with sqrt(maturity) + constexpr Real eps = 100*std::sqrt(QL_EPSILON); + // publication sets tol=0, pyfeng implementation sets tol=0.01 + constexpr Real tol = 100*std::sqrt(QL_EPSILON); + + bool flip = false; + for (Size i=0; i < n_; ++i) + if (boost::math::sign(g[i])*vStar1[i] < tol*vol[i]) { + flip = true; + vStar1[i] = eps * boost::math::sign(g[i]) * vol[i]; + } + + Array q1(n_); + if (flip) { + //q1 = inverse(C)*vStar1; + for (Size i=0; i < n_; ++i) + q1[i] = (vStar1[i] - std::inner_product( + C.row_begin(i), C.row_begin(i) + i, q1.begin(), 0.0))/C[i][i]; + + vStar1 /= Norm2(q1); + } + else { + q1 = transpose(C)*g; + } + q1 /= Norm2(q1); + + Array e1(n_, 0.0); + e1[0] = 1.0; + + const Matrix R = HouseholderTransformation( + HouseholderReflection(e1).reflectionVector(q1)).getMatrix(); + Matrix R_2_n = Matrix(n_, n_-1); + for (Size i=0; i < n_; ++i) + std::copy(R.row_begin(i)+1, R.row_end(i), R_2_n.row_begin(i)); + + const SVD svd(C*R_2_n); + const Matrix U = svd.U(); + const Array sv = svd.singularValues(); + + Matrix v(n_, n_-1); + for (Size i=0; i < n_-1; ++i) + std::transform( + U.column_begin(i), U.column_end(i), v.column_begin(i), + [i, &sv](Real x) -> Real { return sv[i]*x; } + ); + + std::vector nIntOrder(n_-1); + const Real intScale = lambda_ / std::abs(DotProduct(g, vStar1)); + for (Size i=0; i < n_-1; ++i) + nIntOrder[i] = Size(std::lround(1 + intScale*sv[i])); + + std::vector > quotes; + std::vector > p; + for (Size i=0; i < n_; ++i) { + quotes.push_back(ext::make_shared(fwd[i])); + + const Handle bv = processes_[i]->blackVolatility(); + const Volatility vol = vStar1[i] / std::sqrt( + bv->dayCounter().yearFraction(bv->referenceDate(), maturityDate) + ); + p.push_back( + ext::make_shared( + Handle(quotes[i]), + processes_[i]->riskFreeRate(), + Handle( + ext::make_shared( + bv->referenceDate(), bv->calendar(), + Handle(ext::make_shared(vol)), + bv->dayCounter() + ) + ) + ) + ); + } + + BasketOption option(avgPayoff, exercise); + option.setPricingEngine( + ext::make_shared(p) + ); + + Array vq(n_, n_); + for (Size i=0; i < n_; ++i) + vq[i] = 0.5*std::accumulate( + v.row_begin(i), v.row_end(i), 0.0, + [](Real acc, Real x) -> Real { return acc + x*x; } + ); + + const auto bsm1dPricer = [&](const Array& x) -> Real { + const Array f = Exp(-M_SQRT2*v*x - vq) * fwd; + + for (Size i=0; i < f.size(); ++i) + quotes[i]->setValue(f[i]); + + return std::exp(-DotProduct(x, x)) * option.NPV(); + }; + + MulitDimGaussianIntegration ghq( + nIntOrder, + [](const Size n) { return ext::make_shared(n); } + ); + results_.value = ghq(bsm1dPricer) * std::pow(M_PI, -0.5*nIntOrder.size()); } } diff --git a/ql/pricingengines/basket/choibasketengine.hpp b/ql/pricingengines/basket/choibasketengine.hpp index ba63540d9b6..a9dbb2c9d63 100644 --- a/ql/pricingengines/basket/choibasketengine.hpp +++ b/ql/pricingengines/basket/choibasketengine.hpp @@ -32,10 +32,12 @@ namespace QuantLib { //! Pricing engine for basket option on multiple underlyings /*! This class implements the pricing formula from "Sum of all Black-Scholes-Merton Models: An efficient Pricing Method for - Spread, Basket and Asian Options", - Jaehyuk Choi, 2018 + Spread, Basket and Asian Options", Jaehyuk Choi, 2018 https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2913048 + Python implementation from the author of the paper is also available + https://github.com/PyFE/PyFENG + \ingroup basketengines \test the correctness of the returned value is tested by @@ -45,7 +47,8 @@ namespace QuantLib { public: ChoiBasketEngine( std::vector > processes, - Matrix rho); + Matrix rho, + Real lambda = 4.0); void calculate() const override; @@ -53,6 +56,7 @@ namespace QuantLib { const Size n_; const std::vector > processes_; const Matrix rho_; + const Real lambda_; }; } diff --git a/ql/pricingengines/basket/kirkengine.cpp b/ql/pricingengines/basket/kirkengine.cpp index 629f01520d4..d00b09b375d 100644 --- a/ql/pricingengines/basket/kirkengine.cpp +++ b/ql/pricingengines/basket/kirkengine.cpp @@ -23,29 +23,29 @@ namespace QuantLib { - KirkEngine::KirkEngine(ext::shared_ptr process1, - ext::shared_ptr process2, + KirkEngine::KirkEngine(ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation) : SpreadBlackScholesVanillaEngine(process1, process2, correlation) { } Real KirkEngine::calculate( - Real strike, Option::Type optionType, + Real f1, Real f2, Real strike, Option::Type optionType, Real variance1, Real variance2, DiscountFactor df) const { - const Real f = f1_/(f2_ + strike); + const Real f = f1/(f2 + strike); const Real v = std::sqrt(variance1 - + variance2*squared(f2_/(f2_+strike)) + + variance2*squared(f2/(f2+strike)) - 2*rho_*std::sqrt(variance1*variance2) - *(f2_/(f2_+strike))); + *(f2/(f2+strike))); BlackCalculator black( ext::make_shared( optionType,1.0), f, v, df); - return (f2_ + strike)*black.value(); + return (f2 + strike)*black.value(); } } diff --git a/ql/pricingengines/basket/kirkengine.hpp b/ql/pricingengines/basket/kirkengine.hpp index 0d48abc522f..5f8264adfb0 100644 --- a/ql/pricingengines/basket/kirkengine.hpp +++ b/ql/pricingengines/basket/kirkengine.hpp @@ -41,14 +41,13 @@ namespace QuantLib { */ class KirkEngine : public SpreadBlackScholesVanillaEngine { public: - KirkEngine(ext::shared_ptr process1, - ext::shared_ptr process2, + KirkEngine(ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation); protected: - Real calculate( - Real strike, Option::Type optionType, - Real variance1, Real variance2, DiscountFactor df) const override; + Real calculate(Real f1, Real f2, Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const override; }; } diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp index 74a81f42e32..c93746ce181 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp @@ -27,8 +27,8 @@ namespace QuantLib { OperatorSplittingSpreadEngine::OperatorSplittingSpreadEngine( - ext::shared_ptr process1, - ext::shared_ptr process2, + ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation, Order order) : SpreadBlackScholesVanillaEngine(process1, process2, correlation), @@ -36,32 +36,32 @@ namespace QuantLib { } Real OperatorSplittingSpreadEngine::calculate( - Real k, Option::Type optionType, + Real f1, Real f2, Real k, Option::Type optionType, Real variance1, Real variance2, DiscountFactor df) const { - const auto callPutParityPrice = [this, df, k, optionType](Real callPrice) -> Real { + const auto callPutParityPrice = [this, f1, f2, df, k, optionType](Real callPrice) -> Real { if (optionType == Option::Call) return callPrice; else - return callPrice - df*(f1_-f2_-k); + return callPrice - df*(f1-f2-k); }; const Real vol1 = std::sqrt(variance1); const Real vol2 = std::sqrt(variance2); - const Real sig2 = vol2*f2_/(f2_+k); + const Real sig2 = vol2*f2/(f2+k); const Real sig_m = std::sqrt(variance1 +sig2*(sig2 - 2*rho_*vol1)); - const Real d1 = (std::log(f1_) - std::log(f2_ + k))/sig_m + 0.5*sig_m; + const Real d1 = (std::log(f1) - std::log(f2 + k))/sig_m + 0.5*sig_m; const Real d2 = d1 - sig_m; const CumulativeNormalDistribution N; - const Real kirkCallNPV = df*(f1_*N(d1) - (f2_ + k)*N(d2)); + const Real kirkCallNPV = df*(f1*N(d1) - (f2 + k)*N(d2)); const Real vv = (rho_*vol1 - sig2)*vol2/(sig_m*sig_m); const Real oPlt = -sig2*sig2 * k * df * NormalDistribution()(d2) * vv *( d2*(1 - rho_*vol1/sig2) - - 0.5*sig_m * vv * k / (f2_+k) + - 0.5*sig_m * vv * k / (f2+k) * ( d1*d2 + (1-rho_*rho_)*squared(vol1/(rho_*vol1-sig2)))); if (order_ == First) @@ -86,9 +86,9 @@ namespace QuantLib { pStrange2[R1_, R2_] := pStrange1[R1, R2] + (t/2)^2/Factorial[2]*opt[R1, R2][opt[R1, R2][pLT[R1, R2]]] */ - const Real R2 = f2_+k; - const Real R1 = f1_/R2; - const Real F2 = f2_; + const Real R2 = f2+k; + const Real R1 = f1/R2; + const Real F2 = f2; const Real F22 = F2*F2; const Real F23 = F22*F2; diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.hpp b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp index 461874c5336..5b5e824d712 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.hpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp @@ -39,15 +39,14 @@ namespace QuantLib { public: enum Order {First, Second}; OperatorSplittingSpreadEngine( - ext::shared_ptr process1, - ext::shared_ptr process2, + ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation, Order order = Second); protected: - Real calculate( - Real strike, Option::Type optionType, - Real variance1, Real variance2, DiscountFactor df) const override; + Real calculate(Real f1, Real f2, Real strike, Option::Type optionType, + Real variance1, Real variance2, DiscountFactor df) const override; const Order order_; }; diff --git a/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp b/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp index ac1dfc58711..96b2180bf8c 100644 --- a/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/spreadblackscholesvanillaengine.cpp @@ -23,23 +23,16 @@ namespace QuantLib { SpreadBlackScholesVanillaEngine::SpreadBlackScholesVanillaEngine( - ext::shared_ptr process1, - ext::shared_ptr process2, + ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation) - : process1_(std::move(process1)), process2_(std::move(process2)), rho_(correlation) { - update(); - + : process1_(std::move(process1)), + process2_(std::move(process2)), + rho_(correlation) { registerWith(process1_); registerWith(process2_); } - void SpreadBlackScholesVanillaEngine::update() { - f1_ = process1_->stateVariable()->value(); - f2_ = process2_->stateVariable()->value(); - - BasketOption::engine::update(); - } - void SpreadBlackScholesVanillaEngine::calculate() const { const ext::shared_ptr exercise = ext::dynamic_pointer_cast(arguments_.exercise); @@ -56,14 +49,24 @@ namespace QuantLib { const Real strike = payoff->strike(); const Option::Type optionType = payoff->optionType(); + + const Date maturityDate = exercise->lastDate(); + const Real f1 = process1_->stateVariable()->value() + / process1_->riskFreeRate()->discount(maturityDate) + * process1_->dividendYield()->discount(maturityDate); + + const Real f2 = process2_->stateVariable()->value() + / process2_->riskFreeRate()->discount(maturityDate) + * process2_->dividendYield()->discount(maturityDate); + const Real variance1 = - process1_->blackVolatility()->blackVariance(exercise->lastDate(), f1_); + process1_->blackVolatility()->blackVariance(maturityDate, f1); const Real variance2 = - process2_->blackVolatility()->blackVariance(exercise->lastDate(), f2_); + process2_->blackVolatility()->blackVariance(maturityDate, f2); const DiscountFactor df = process1_->riskFreeRate()->discount(exercise->lastDate()); - results_.value = calculate(strike, optionType, variance1, variance2, df); + results_.value = calculate(f1, f2, strike, optionType, variance1, variance2, df); } } diff --git a/ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp b/ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp index 41fbdb269cf..67edf7544f4 100644 --- a/ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp +++ b/ql/pricingengines/basket/spreadblackscholesvanillaengine.hpp @@ -32,22 +32,20 @@ namespace QuantLib { class SpreadBlackScholesVanillaEngine : public BasketOption::engine { public: SpreadBlackScholesVanillaEngine( - ext::shared_ptr process1, - ext::shared_ptr process2, + ext::shared_ptr process1, + ext::shared_ptr process2, Real correlation); - void update() override; void calculate() const override; protected: virtual Real calculate( - Real strike, Option::Type optionType, + Real f1, Real f2, Real strike, Option::Type optionType, Real variance1, Real variance2, DiscountFactor df) const = 0; - const ext::shared_ptr process1_; - const ext::shared_ptr process2_; + const ext::shared_ptr process1_; + const ext::shared_ptr process2_; const Real rho_; - Real f1_, f2_; }; } diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp index da4a9e89678..0d8bf10d1fe 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp @@ -90,7 +90,6 @@ namespace QuantLib { return p->blackVolatility()->blackVol(maturityDate, p->x0())*std::sqrt(maturity); } ); - } } } diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp index 3715c69b295..18382b6a54e 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp @@ -35,8 +35,9 @@ namespace QuantLib { std::vector > p); Array getSpot() const; - Array getBlackVariance(const Date& maturityDate) const; Array getBlackStdDev(const Date& maturityDate) const; + Array getBlackVariance(const Date& maturityDate) const; + Array getBlackVolatility(const Date& maturityDate) const; Array getDividendYieldDf(const Date& maturityDate) const; DiscountFactor getInterestRateDf(const Date& maturityDate) const; diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index ef6c1909935..f3ce962379c 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -24,10 +24,12 @@ #include "utilities.hpp" #include #include +#include #include #include #include #include +#include #include #include #include @@ -1173,38 +1175,57 @@ BOOST_AUTO_TEST_CASE(testOperatorSplittingSpreadEngine) { ext::make_shared(Option::Call, 20.0)), ext::make_shared(maturity)); - const Real testData[][2] = { - {-0.9, 18.9323}, - {-0.7, 18.0092}, - {-0.5, 17.0325}, - {-0.4, 16.5211}, - {-0.3, 15.9925}, - {-0.2, 15.4449}, - {-0.1, 14.8762}, - { 0.0, 14.284}, - { 0.1, 13.6651}, - { 0.2, 13.016}, - { 0.3, 12.3319}, - { 0.4, 11.6067}, - { 0.5, 10.8323}, - { 0.7, 9.0863}, - { 0.9, 6.9148} + const Real testData[][3] = { + {-0.9, 18.9323, 18.9361}, + {-0.7, 18.0092, 18.012}, + {-0.5, 17.0325, 17.0344}, + {-0.4, 16.5211, 16.5227}, + {-0.3, 15.9925, 15.9937}, + {-0.2, 15.4449, 15.4458}, + {-0.1, 14.8762, 14.8768}, + { 0.0, 14.284, 14.2843}, + { 0.1, 13.6651, 13.6654}, + { 0.2, 13.016, 13.0161}, + { 0.3, 12.3319, 12.3319}, + { 0.4, 11.6067, 11.6067}, + { 0.5, 10.8323, 10.8323}, + { 0.7, 9.0863, 9.0862}, + { 0.9, 6.9148, 6.9134} }; for (Size i = 0; i < LENGTH(testData); ++i) { const Real rho = testData[i][0]; - const Real expected = testData[i][1]; + Real expected = testData[i][1]; - const ext::shared_ptr osEngine - = ext::make_shared(p1, p2, rho); + option.setPricingEngine( + ext::make_shared( + p1, p2, rho, OperatorSplittingSpreadEngine::First) + ); - option.setPricingEngine(osEngine); + Real diff = std::abs(option.NPV() - expected); + Real tol = 0.0001; - const Real diff = std::abs(option.NPV() - expected); - const Real tol = 0.0001; + if (diff > tol) { + BOOST_FAIL("failed to reproduce reference values " + "using the first order operator splitting spread engine." + << std::fixed << std::setprecision(5) + << "\n calculated: " << option.NPV() + << "\n expected : " << expected + << "\n diff : " << diff + << "\n tolerance : " << tol); + } + + option.setPricingEngine( + ext::make_shared( + p1, p2, rho, OperatorSplittingSpreadEngine::Second) + ); + + expected = testData[i][2]; + diff = std::abs(option.NPV() - expected); + tol = 0.0005; if (diff > tol) { BOOST_FAIL("failed to reproduce reference values " - "using the operator splitting spread engine." + "using the second order operator splitting spread engine." << std::fixed << std::setprecision(5) << "\n calculated: " << option.NPV() << "\n expected : " << expected @@ -1216,7 +1237,7 @@ BOOST_AUTO_TEST_CASE(testOperatorSplittingSpreadEngine) { BOOST_AUTO_TEST_CASE(testStrangSplittingSpreadEngineVsMathematica) { - BOOST_TEST_MESSAGE("Testing Strang Operator Splitting spread engine" + BOOST_TEST_MESSAGE("Testing Strang Operator Splitting spread engine " "vs Mathematica results..."); // Example taken from @@ -1878,7 +1899,6 @@ BOOST_AUTO_TEST_CASE(testSingleFactorBsmBasketEngine) { } const Real expected = stats.mean(); - std::cout << std::setprecision(6) << calculated << " " << expected << std::endl; const Real diff = std::abs(expected - calculated); const Real errorEstimate = stats.errorEstimate(); @@ -1895,6 +1915,235 @@ BOOST_AUTO_TEST_CASE(testSingleFactorBsmBasketEngine) { } } +BOOST_AUTO_TEST_CASE(testGoldenChoiBasketEngineExample) { + BOOST_TEST_MESSAGE( + "Testing BSM Choi basket engine against reference results..."); + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(26, September, 2024); + + const Handle rTS + = Handle(flatRate(today, 0.05, dc)); + + const Real strike = 20; + const Date maturity = today + Period(18, Months); + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Put, strike), + Array({1, -2, -1, 4}) + ), + ext::make_shared(maturity) + ); + + const auto processGen = [&rTS, &today, &dc](Real spot, Rate q, Volatility vol) + -> ext::shared_ptr { + return ext::make_shared( + Handle(ext::make_shared(spot)), + Handle(flatRate(today, q, dc)), + rTS, + Handle(flatVol(today, vol, dc)) + ); + }; + + const std::vector > + processes({ + processGen(100, 0.075, 0.45), + processGen(50, 0.035, 0.4), + processGen(75, 0.08 , 0.35), + processGen(25, 0.02 , 0.2) + } + ); + + const Matrix rho = { + { 1.0, 0.2, 0.3, 0.0 }, + { 0.2, 1.0, -0.3, 0.1 }, + { 0.3, -0.3, 1.0, 0.7 }, + { 0.0, 0.1, 0.7, 1.0 }, + }; + + option.setPricingEngine( + ext::make_shared(processes, rho, 10) + ); + + const Real expected = 15.9200853315129; + const Real calculated = option.NPV(); + const Real diff = std::abs(expected - calculated); + const Real tol = 1e-10; + + if (diff > tol) + BOOST_FAIL("failed to reproduce reference price with Choi engine" + << std::fixed << std::setprecision(12) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); +} + +BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { + BOOST_TEST_MESSAGE( + "Testing benchmark spread- and basket options from the literature..."); + + // Benchmark set is derived from + // "Sum of all Black-Scholes-Merton Models: An efficient Pricing Method for + // Spread, Basket and Asian Options", Jaehyuk Choi, 2018 + + struct SobolBrownianBridgeRsgType { + enum { allowsErrorEstimate = 0 }; + typedef SobolBrownianBridgeRsg rsg_type; + + static rsg_type make_sequence_generator(Size dim, BigNatural seed) { + return rsg_type( + dim, 1, + SobolBrownianGenerator::Diagonal, + seed, + SobolRsg::JoeKuoD7 + ); + } + }; + + struct Benchmark { + const Array underlyings; + const Array volatilities; + const Array q; + const Rate r; + const Matrix rho; + const Array weights; + const Array maturities; + const Array strikes; + Option::Type optionType; + const Array referenceNPVs; + }; + + const std::vector benchmarks = { + { + {100.0, 96.0}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{0.5}}, + {1.0, -1.0}, {1.0}, + {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Call, + {8.312460732881519,8.114993760660171,7.920819775954081,7.729932490363331,7.542323895849758,7.35798429885716,7.176902356575362,6.999065115204262,6.824458050072985,6.653065107468672} + } + }; + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(26, September, 2024); + + for (const auto& b: benchmarks) { + const Size n = b.underlyings.size(); + + const Handle rTS + = Handle(flatRate(today, b.r, dc)); + + std::vector > processes; + for (Size i=0; i < n; ++i) + processes.push_back( + ext::make_shared( + Handle(ext::make_shared(b.underlyings[i])), + Handle(flatRate(today, b.q[i], dc)), rTS, + Handle(flatVol(today, b.volatilities[i], dc)) + ) + ); + + Matrix rho(n, n); + for (Size i=0; i < n; ++i) + for (Size j=0; j < n; ++j) + rho[i][j] = (i == j) ? 1.0 : b.rho[0][0]; + + + const ext::shared_ptr choiEngine = + ext::make_shared(processes, rho, 20); + + const ext::shared_ptr dengLiZhou = + ext::make_shared(processes, rho); + + const ext::shared_ptr kirkEngine = + ext::make_shared(processes[0], processes[1], rho[0][1]); + + std::vector calculated; + for (Real t: b.maturities) { + const Date maturityDate = yearFractionToDate(dc, today, t); + const ext::shared_ptr exercise = + ext::make_shared(maturityDate); + + for (Real K: b.strikes) { + const ext::shared_ptr payoff = + ext::make_shared(b.optionType, K); + + const ext::shared_ptr basketPayoff = + ext::make_shared(payoff, b.weights); + + const ext::shared_ptr spreadPayoff = + ext::make_shared(payoff); + + BasketOption option(spreadPayoff, exercise); + option.setPricingEngine(kirkEngine); + + calculated.push_back(option.NPV()); + + //std::cout << std::setprecision(16) << option.NPV() << "," ; + } + } + + const Array calculatedNPVs(calculated.begin(), calculated.end()); + const Array diff = b.referenceNPVs - calculatedNPVs; + const Array absDiff = Abs(diff); + + const Real rmse = std::sqrt(DotProduct(diff, diff))/n; + const Real mae = std::accumulate(absDiff.begin(), absDiff.end(), 0.0)/n; + + std::cout << diff << std::endl; + std::cout << rmse << " " << mae << std::endl; + + std::cout << std::endl; + + } +// const Real strike = std::inner_product( +// b.weights.begin(), b.weights.end(), b.underlyings.begin(), 0.0 +// ); +// +// const ext::shared_ptr payoff +// = ext::make_shared(t.optionType, strike); +// +// BasketOption option( +// ext::make_shared(payoff, d.weights), +// ext::make_shared(maturity) +// ); + + +// +// Size i=1024; +// for (; i < 1000000000; i*=2) { +// option.setPricingEngine( +// MakeMCEuropeanBasketEngine( +// //MakeMCEuropeanBasketEngine( +// ext::make_shared( +// std::vector >( +// processes.begin(), processes.end() +// ), +// rho +// ) +// ) +// .withSteps(1) +// .withSamples(i) +// .withSeed(1234ul) +// ); +// +// std::cout << i << " " << option.NPV() << std::endl; +// } +// +// option.setPricingEngine( +// ext::make_shared(processes, rho)); +// std::cout << option.NPV() << std::endl; +// +// +// option.setPricingEngine( +// ext::make_shared( +// processes, rho, +// std::vector({25, 25, 25, 25}), 75 +// ) +// ); +// std::cout << option.NPV() << std::endl; +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/matrices.cpp b/test-suite/matrices.cpp index e4b2ea128ab..aae4f374b5d 100644 --- a/test-suite/matrices.cpp +++ b/test-suite/matrices.cpp @@ -959,23 +959,46 @@ BOOST_AUTO_TEST_CASE(testHouseholderReflection) { return e; }; -// for (Size i=0; i < 5; ++i) { -// QL_CHECK_CLOSE_ARRAY_TOL( -// HouseholderReflection(e(5))(e(5, i)), e(5), tol); -// QL_CHECK_CLOSE_ARRAY_TOL( -// HouseholderReflection(e(5))(M_PI*e(5, i)), M_PI*e(5), tol); -// QL_CHECK_CLOSE_ARRAY_TOL( -// HouseholderReflection(e(5))( -// e(5, i) + e(5)), -// ((i==0)? 2.0 : M_SQRT2)*e(5), tol); -// } + for (Size i=0; i < 5; ++i) { + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(5))(e(5, i)), e(5), tol); + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(5))(M_PI*e(5, i)), M_PI*e(5), tol); + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(5))( + e(5, i) + e(5)), + ((i==0)? 2.0 : M_SQRT2)*e(5), tol); + } // limits - QL_CHECK_CLOSE_ARRAY_TOL( - HouseholderReflection(e(3))(Array({10.0, 1e-50, 0.0})), 10.0*e(3), tol); + for (Real x=10; x > 1e-50; x*=0.1) { + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(3))( + Array({10.0, x, 0})), + std::sqrt(10.0*10.0+x*x)*e(3), tol + ); + + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(3))( + Array({10.0, x, 1e-3})), + std::sqrt(10.0*10.0+x*x+1e-3*1e-3)*e(3), tol + ); + } + + MersenneTwisterUniformRng rng(1234); + + for (Size i=0; i < 100; ++i) { + const Array v = Array({rng.nextReal(), rng.nextReal(), rng.nextReal()}) - 0.5; + const Matrix u = HouseholderTransformation(v / Norm2(v)).getMatrix(); + const Array eu = u*e(3, i%3); + const Array a = Array({rng.nextReal(), rng.nextReal(), rng.nextReal()}) - 0.5; + const Matrix H = HouseholderTransformation( + HouseholderReflection(eu).reflectionVector(a)).getMatrix(); + QL_CHECK_CLOSE_ARRAY_TOL(u*H*a, Norm2(a)*e(3, i%3), tol); + } } BOOST_AUTO_TEST_SUITE_END() From 75bc084e9956c5c92de9a8bd1b7c828a89f8a3d9 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sat, 19 Oct 2024 12:18:21 +0200 Subject: [PATCH 16/36] forward deltas for Choi engine --- ql/CMakeLists.txt | 1 + ql/math/integrals/gaussianquadratures.hpp | 3 + ql/pricingengines/basket/choibasketengine.cpp | 95 +++- ql/pricingengines/basket/choibasketengine.hpp | 9 +- .../fdndimblackscholesvanillaengine.cpp | 18 +- .../basket/singlefactorbsmbasketengine.cpp | 18 +- .../basket/singlefactorbsmbasketengine.hpp | 2 +- .../basket/vectorbsmprocessextractor.hpp | 1 - test-suite/basketoption.cpp | 465 +++++++++++++----- 9 files changed, 448 insertions(+), 164 deletions(-) diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 6ef7fc76bfb..eb4985f1555 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -384,6 +384,7 @@ set(QL_SOURCES math/matrixutilities/factorreduction.cpp math/matrixutilities/getcovariance.cpp math/matrixutilities/gmres.cpp + math/matrixutilities/householder.cpp math/matrixutilities/pseudosqrt.cpp math/matrixutilities/qrdecomposition.cpp math/matrixutilities/sparseilupreconditioner.cpp diff --git a/ql/math/integrals/gaussianquadratures.hpp b/ql/math/integrals/gaussianquadratures.hpp index 3d0a913709a..5c6f7d98498 100644 --- a/ql/math/integrals/gaussianquadratures.hpp +++ b/ql/math/integrals/gaussianquadratures.hpp @@ -84,6 +84,9 @@ namespace QuantLib { Real operator()(const std::function& f) const; + const Array& weights() const { return weights_; } + const std::vector& x() const { return x_; } + private: Array weights_; std::vector x_; diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index e27dd23fc64..29b5bac3da9 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -27,24 +27,29 @@ #include #include #include +#include #include #include #include #include #include -#include #include namespace QuantLib { ChoiBasketEngine::ChoiBasketEngine( std::vector > processes, - Matrix rho, Real lambda) + Matrix rho, Real lambda, + Size maxNrIntegrationSteps, + bool calcFwdDelta, bool controlVariate) : n_(processes.size()), processes_(std::move(processes)), rho_(std::move(rho)), - lambda_(lambda) { + lambda_(lambda), + maxNrIntegrationSteps_(maxNrIntegrationSteps), + calcFwdDelta_(calcFwdDelta || controlVariate), + controlVariate_(controlVariate) { QL_REQUIRE(n_ > 0, "No Black-Scholes process is given."); QL_REQUIRE(n_ == rho_.size1() && rho_.size1() == rho_.size2(), @@ -64,7 +69,7 @@ namespace QuantLib { const detail::VectorBsmProcessExtractor pExtractor(processes_); const Array s = pExtractor.getSpot(); const Array dq = pExtractor.getDividendYieldDf(maturityDate); - const Array vol = pExtractor.getBlackStdDev(maturityDate); + const Array stdDev = pExtractor.getBlackStdDev(maturityDate); const DiscountFactor dr0 = pExtractor.getInterestRateDf(maturityDate); const Array fwd = s * dq/dr0; @@ -79,22 +84,21 @@ namespace QuantLib { const Array g = weights*fwd / Norm2(weights*fwd); - const Matrix Sigma = getCovariance(vol.begin(), vol.end(), rho_); + const Matrix Sigma = getCovariance(stdDev.begin(), stdDev.end(), rho_); Array vStar1 = Sigma*g; vStar1 /= std::sqrt(DotProduct(g, vStar1)); const Matrix C = CholeskyDecomposition(Sigma); - // todo: this needs to be scaled with sqrt(maturity) constexpr Real eps = 100*std::sqrt(QL_EPSILON); // publication sets tol=0, pyfeng implementation sets tol=0.01 constexpr Real tol = 100*std::sqrt(QL_EPSILON); bool flip = false; for (Size i=0; i < n_; ++i) - if (boost::math::sign(g[i])*vStar1[i] < tol*vol[i]) { + if (boost::math::sign(g[i])*vStar1[i] < tol*stdDev[i]) { flip = true; - vStar1[i] = eps * boost::math::sign(g[i]) * vol[i]; + vStar1[i] = eps * boost::math::sign(g[i]) * stdDev[i]; } Array q1(n_); @@ -161,31 +165,82 @@ namespace QuantLib { } BasketOption option(avgPayoff, exercise); - option.setPricingEngine( - ext::make_shared(p) - ); + option.setPricingEngine(ext::make_shared(p)); - Array vq(n_, n_); + Array vq(n_); for (Size i=0; i < n_; ++i) vq[i] = 0.5*std::accumulate( v.row_begin(i), v.row_end(i), 0.0, [](Real acc, Real x) -> Real { return acc + x*x; } ); - const auto bsm1dPricer = [&](const Array& x) -> Real { - const Array f = Exp(-M_SQRT2*v*x - vq) * fwd; - - for (Size i=0; i < f.size(); ++i) - quotes[i]->setValue(f[i]); + for (Size i=0; i < nIntOrder.size(); ++i) + std::cout << nIntOrder[i] << " "; + std::cout << std::endl; - return std::exp(-DotProduct(x, x)) * option.NPV(); - }; MulitDimGaussianIntegration ghq( nIntOrder, [](const Size n) { return ext::make_shared(n); } ); + const Real normFactor = std::pow(M_PI, -0.5*nIntOrder.size()); + + std::vector dStore; + dStore.reserve(ghq.weights().size()); + const auto bsm1dPricer = [&](const Array& z) -> Real { + const Array f = Exp(-M_SQRT2*(v*z) - vq) * fwd; + + for (Size i=0; i < f.size(); ++i) + quotes[i]->setValue(f[i]); + + dStore.push_back(ext::any_cast(option.additionalResults().at("d"))); + return std::exp(-DotProduct(z, z)) * option.NPV(); + }; - results_.value = ghq(bsm1dPricer) * std::pow(M_PI, -0.5*nIntOrder.size()); + results_.value = ghq(bsm1dPricer) * normFactor; + + if (calcFwdDelta_) { + const ext::shared_ptr payoff = + ext::dynamic_pointer_cast(avgPayoff->basePayoff()); + QL_REQUIRE(payoff, "non-plain vanilla payoff given"); + const Real putIndicator = (payoff->optionType() == Option::Call) ? 0.0 : -1.0; + + Size dStoreCounter; + const CumulativeNormalDistribution N; + + Array fwdDelta(n_), fHat(n_); + for (Size k=0; k < n_; ++k) { + dStoreCounter = 0; + + const auto deltaPricer = [&](const Array& z) -> Real { + const Real d = dStore[dStoreCounter++]; + const Real vz = std::inner_product( + v.row_begin(k), v.row_end(k), z.begin(), 0.0); + const Real f = std::exp(-M_SQRT2*vz - vq[k]); + + return std::exp(-DotProduct(z, z)) * f * N(d + vStar1[k]); + }; + + fwdDelta[k] = dr0*weights[k]*(ghq(deltaPricer) * normFactor + putIndicator); + + const std::string deltaName = "forwardDelta " + std::to_string(k); + results_.additionalResults[deltaName] = fwdDelta[k]; + } + + if (controlVariate_) { + for (Size k=0; k < n_; ++k) { + const auto fHatPricer = [&](const Array& z) -> Real { + const Real vz = std::inner_product( + v.row_begin(k), v.row_end(k), z.begin(), 0.0); + const Real f = std::exp(-M_SQRT2*vz - vq[k]); + + return std::exp(-DotProduct(z, z)) * f; + }; + fHat[k] = ghq(fHatPricer) * normFactor; + } + const Array cv = fwdDelta*fwd*(fHat-1.0); + results_.value -= std::accumulate(cv.begin(), cv.end(), 0.0); + } + } } } diff --git a/ql/pricingengines/basket/choibasketengine.hpp b/ql/pricingengines/basket/choibasketengine.hpp index a9dbb2c9d63..18af9a74135 100644 --- a/ql/pricingengines/basket/choibasketengine.hpp +++ b/ql/pricingengines/basket/choibasketengine.hpp @@ -45,10 +45,15 @@ namespace QuantLib { */ class ChoiBasketEngine : public BasketOption::engine { public: + // lambda controls the precision, + // fast: 2, accurate: 6, high precision: 15 ChoiBasketEngine( std::vector > processes, Matrix rho, - Real lambda = 4.0); + Real lambda = 4.0, + Size maxNrIntegrationSteps = std::numeric_limits::max(), + bool calcfwdDelta = false, + bool controlVariate = false); void calculate() const override; @@ -57,6 +62,8 @@ namespace QuantLib { const std::vector > processes_; const Matrix rho_; const Real lambda_; + const Size maxNrIntegrationSteps_; + const bool calcFwdDelta_, controlVariate_; }; } diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp index f2af9c7401c..29132c4011f 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -27,7 +27,7 @@ #include #include - +#include namespace QuantLib { @@ -61,6 +61,15 @@ namespace QuantLib { void FdndimBlackScholesVanillaEngine::calculate() const { + #ifndef PDE_MAX_SUPPORTED_DIM + #define PDE_MAX_SUPPORTED_DIM 4 + #endif + QL_REQUIRE(processes_.size() <= PDE_MAX_SUPPORTED_DIM, + "This engine does not support " << processes_.size() << " underlyings. " + << "Max number of underlyings is " << PDE_MAX_SUPPORTED_DIM << ". " + << "Please change preprocessor constant PDE_MAX_SUPPORTED_DIM and recompile " + << "if a larger number of underlyings is needed."); + const Time maturity = processes_[0]->time(arguments_.exercise->lastDate()); std::vector > meshers; @@ -103,8 +112,6 @@ namespace QuantLib { logX.push_back(std::log(p->x0())); switch(processes_.size()) { - #define PDE_MAX_SUPPORTED_DIM 6 - #define BOOST_PP_LOCAL_MACRO(n) \ case n : \ results_.value = ext::make_shared>( \ @@ -112,11 +119,6 @@ namespace QuantLib { break; #define BOOST_PP_LOCAL_LIMITS (1, PDE_MAX_SUPPORTED_DIM) #include BOOST_PP_LOCAL_ITERATE() - default: - QL_FAIL("This engine does not support " << processes_.size() << " underlyings. " - << "Max number of underlyings is " << PDE_MAX_SUPPORTED_DIM << ". " - << "Change preprocessor constant PDE_MAX_SUPPORTED_DIM and recompile " - << "if a large number of underlyings is needed."); } } } diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp index 560cee30512..8083a2bf0ba 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -126,8 +126,9 @@ namespace QuantLib { Real xTol) : xTol_(xTol), n_(p.size()), processes_(std::move(p)) { - for (const auto& process: processes_) - registerWith(process); + + std::for_each(processes_.begin(), processes_.end(), + [this](const auto& p) { registerWith(p); }); } void SingleFactorBsmBasketEngine::calculate() const { @@ -137,7 +138,6 @@ namespace QuantLib { const ext::shared_ptr payoff = ext::dynamic_pointer_cast(avgPayoff->basePayoff()); QL_REQUIRE(payoff, "non-plain vanilla payoff given"); - const Real cp = (payoff->optionType() == Option::Call) ? 1.0 : -1.0; const Real strike = payoff->strike(); // sort assets by their weight @@ -158,7 +158,6 @@ namespace QuantLib { const Array stdDev = pExtractor.getBlackStdDev(maturityDate); const Array v = stdDev*stdDev; - const Array fwdBasket = weights * s * dq /dr0; // first check if all vols are zero -> intrinsic case @@ -170,19 +169,22 @@ namespace QuantLib { std::accumulate(fwdBasket.begin(), fwdBasket.end(), 0.0)); } else { - const Real d = -cp * SumExponentialsRootSolver( + const Real d = -SumExponentialsRootSolver( fwdBasket*Exp(-0.5*v), stdDev, strike) - .getRoot(xTol_, SumExponentialsRootSolver::Halley); + .getRoot(xTol_, SumExponentialsRootSolver::Brent); const CumulativeNormalDistribution N; + const Real cp = (payoff->optionType() == Option::Call) ? 1.0 : -1.0; results_.value = cp * dr0 * std::inner_product( fwdBasket.begin(), fwdBasket.end(), stdDev.begin(), - -strike*N(d), + -strike*N(cp*d), std::plus<>(), - [d, cp, &N](Real x, Real y) -> Real { return x*N(d+cp*y); } + [d, cp, &N](Real x, Real y) -> Real { return x*N(cp*(d+y)); } ); + + results_.additionalResults["d"] = d; } } } diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index 2a65ca8726d..eda427149f3 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -65,7 +65,7 @@ namespace QuantLib { public: SingleFactorBsmBasketEngine( std::vector > p, - Real xTol = 1e6*QL_EPSILON); + Real xTol = 1e4*QL_EPSILON); void calculate() const override; diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp index 18382b6a54e..662e85b4b07 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp @@ -37,7 +37,6 @@ namespace QuantLib { Array getSpot() const; Array getBlackStdDev(const Date& maturityDate) const; Array getBlackVariance(const Date& maturityDate) const; - Array getBlackVolatility(const Date& maturityDate) const; Array getDividendYieldDf(const Date& maturityDate) const; DiscountFactor getInterestRateDf(const Date& maturityDate) const; diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index f3ce962379c..2801cc9576d 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -48,6 +48,8 @@ #include #include +#include +#include #include using namespace QuantLib; @@ -1928,18 +1930,18 @@ BOOST_AUTO_TEST_CASE(testGoldenChoiBasketEngineExample) { const Real strike = 20; const Date maturity = today + Period(18, Months); - BasketOption option( - ext::make_shared( - ext::make_shared(Option::Put, strike), - Array({1, -2, -1, 4}) - ), - ext::make_shared(maturity) - ); + const std::vector> spots = { + ext::make_shared(100), + ext::make_shared(50), + ext::make_shared(75), + ext::make_shared(25) + }; - const auto processGen = [&rTS, &today, &dc](Real spot, Rate q, Volatility vol) + const auto processGen = [&rTS, &today, &dc]( + const ext::shared_ptr& spot, Rate q, Volatility vol) -> ext::shared_ptr { return ext::make_shared( - Handle(ext::make_shared(spot)), + Handle(spot), Handle(flatRate(today, q, dc)), rTS, Handle(flatVol(today, vol, dc)) @@ -1948,10 +1950,10 @@ BOOST_AUTO_TEST_CASE(testGoldenChoiBasketEngineExample) { const std::vector > processes({ - processGen(100, 0.075, 0.45), - processGen(50, 0.035, 0.4), - processGen(75, 0.08 , 0.35), - processGen(25, 0.02 , 0.2) + processGen(spots[0], 0.075, 0.45), + processGen(spots[1], 0.035, 0.4), + processGen(spots[2], 0.08 , 0.35), + processGen(spots[3], 0.02 , 0.2) } ); @@ -1962,24 +1964,70 @@ BOOST_AUTO_TEST_CASE(testGoldenChoiBasketEngineExample) { { 0.0, 0.1, 0.7, 1.0 }, }; - option.setPricingEngine( - ext::make_shared(processes, rho, 10) - ); + const ext::shared_ptr engine = + ext::make_shared(processes, rho, 7.0, true, true); - const Real expected = 15.9200853315129; - const Real calculated = option.NPV(); - const Real diff = std::abs(expected - calculated); - const Real tol = 1e-10; + const Array expected = {15.92008513388834, 22.36122704630282}; + const std::vector optionTypes = {Option::Put, Option::Call}; - if (diff > tol) - BOOST_FAIL("failed to reproduce reference price with Choi engine" - << std::fixed << std::setprecision(12) - << "\n calculated: " << calculated - << "\n expected: " << expected - << "\n diff: " << diff - << "\n tolerance: " << tol); + for (Size i=0; i < expected.size(); ++i) { + BasketOption option( + ext::make_shared( + ext::make_shared(optionTypes[i], strike), + Array({1, -2, -1, 4}) + ), + ext::make_shared(maturity) + ); + option.setPricingEngine(engine); + + const Real calculated = option.NPV(); + const Real npvDiff = std::abs(expected[i] - calculated); + const Real npvTol = 1e-5; + + if (npvDiff > npvTol) + BOOST_FAIL("failed to reproduce reference price with Choi engine" + << std::fixed << std::setprecision(8) + << "\n option type: " << optionTypes[i] + << "\n calculated: " << calculated + << "\n expected: " << expected[i] + << "\n diff: " << npvDiff + << "\n tolerance: " << npvTol); + + for (Size k=0; k < processes.size(); ++k) { + const Real baseSpot = spots[k]->value(); + + spots[k]->setValue(baseSpot*1.001); + const Real up = option.NPV(); + spots[k]->setValue(baseSpot*0.999); + const Real down = option.NPV(); + + spots[k]->setValue(baseSpot); + + const Real expectedDeltaSpot = (up - down) / (0.002*baseSpot); + const Real expectedDeltaFwd = expectedDeltaSpot + / processes[k]->dividendYield()->discount(maturity) + * processes[0]->riskFreeRate()->discount(maturity); + + const std::string deltaName = "forwardDelta " + std::to_string(k); + const Real deltaDiff = std::abs(expectedDeltaFwd + - ext::any_cast(option.additionalResults().at(deltaName))); + const Real deltaTol = 5e-5; + + if (deltaDiff > deltaTol) + BOOST_FAIL("failed to reproduce forward delta with Choi engine" + << std::fixed << std::setprecision(8) + << "\n option type: " << optionTypes[i] + << "\n underlying: " << i + << "\n calculated: " << calculated + << "\n expected: " << expected[i] + << "\n diff: " << npvDiff + << "\n tolerance: " << npvTol); + + } + } } + BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { BOOST_TEST_MESSAGE( "Testing benchmark spread- and basket options from the literature..."); @@ -2007,7 +2055,7 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { const Array volatilities; const Array q; const Rate r; - const Matrix rho; + const std::vector rhos; const Array weights; const Array maturities; const Array strikes; @@ -2016,24 +2064,192 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { }; const std::vector benchmarks = { +// // Dempster and Hong [2002], Hurd and Zhou [2010] +// { +// {100.0, 96.0}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{{0.5}}}, +// {1.0, -1.0}, {1.0}, +// {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Put, +// {4.86947800209290982, 5.03394599708595702, 5.20170697959426764, 5.37275466121791023, 5.54708103391874285, 5.72467640414054557, 5.90552942907314105, 6.08962715491644957, 6.27695505699956779, 6.46749708160964865} +// }, +// { +// {100, 96}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{{0.5}}}, +// {1.0, -1.0}, {1.0}, +// {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Call, +// {8.312460732881519, 8.114993760660171, 7.920819775954081, +// 7.729932490363331, 7.542323895849758, 7.35798429885716, 7.176902356575362, +// 6.999065115204262, 6.824458050072985, 6.653065107468672} +// }, +// // Choi [2018] +// { +// {200, 100}, {0.15, 0.3}, {0.0, 0.0}, 0.0, +// { {{-0.9}}, {{-0.7}}, {{-0.5}}, {{-0.3}}, {{-0.1}}, {{0.1}}, {{0.3}}, {{0.5}}, {{0.7}}, {{0.9}} }, +// {1.0, -1.0}, {1.0}, {100}, Option::Call, +// {23.1398673777858619, 21.9077989170003313, 20.5982705317786383, 19.1954201364940467, +// 17.6770248596142956, 16.0102190445729207, 14.1425869461427691, 11.9804918293938165, +// 9.32094392217566181, 5.47927202785675949} +// }, +// // Krekel et al [2004], Caldana et al. [2016] +// { +// {100, 100, 100, 100}, {0.4, 0.4, 0.4, 0.4}, {0, 0, 0, 0}, 0, {{{0.5}}}, +// {0.25, 0.25, 0.25, 0.25}, {5}, {50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150}, +// Option::Call, +// {54.3101760503818554, 47.4811264983728805, 41.5225192321721579, 36.3517843455707421, +// 31.8768031971830865, 28.0073695445039341, 24.6605295130931736, 21.7625788671709337, +// 19.2493294434234272, 17.0655419939919533, 15.1640102889333352} +// }, +// { +// {100, 100, 100, 100}, {0.4, 0.4, 0.4, 0.4}, {0, 0, 0, 0}, 0, +// {{{-0.1}}, {{0.1}}, {{0.3}}, {{0.5}}, {{0.8}}, {{0.95}}}, +// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, +// {17.756916333753729, 21.6920964834602223, 25.029299237118412, +// 28.0073695445038631, 32.0412264523680363, 33.9186874338078042} +// }, +// { +// {100, 100, 100, 100}, {0.05, 0.05, 0.05, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, +// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, {19.4590949762084549} +// }, +// { +// {100, 100, 100, 100}, {0.4, 0.4, 0.4, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, +// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, {36.048540687480191 } +// }, +// { +// {100, 100, 100, 100}, {0.8, 0.8, 0.8, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, +// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Put, {56.7772198387342684} +// }, +// // Milevsky and Posner [1998], Zhou and Wnag [2008] +// { +// {100, 100, 100, 100, 100, 100, 100}, +// {0.1155, 0.2068, 0.1453, 0.1799, 0.1559, 0.1462, 0.1568}, +// {0.0169, 0.0239, 0.0136, 0.0192, 0.0081, 0.0362, 0.0166}, 0.063, +// {{{1.00, 0.35, 0.10, 0.27, 0.04, 0.17, 0.71}, +// {0.35, 1.00, 0.39, 0.27, 0.50,-0.08, 0.15}, +// {0.10, 0.39, 1.00, 0.53, 0.70,-0.23, 0.09}, +// {0.27, 0.27, 0.53, 1.00, 0.46,-0.22, 0.32}, +// {0.04, 0.50, 0.70, 0.46, 1.00,-0.29, 0.13}, +// {0.17,-0.08,-0.23,-0.22,-0.29, 1.00,-0.03}, +// {0.71, 0.15, 0.09, 0.32, 0.13,-0.03, 1.00} +// }}, +// {0.10, 0.15, 0.15, 0.05, 0.20, 0.10, 0.25}, +// {0.5, 1, 2, 3}, {80, 100, 120}, Option::Call, +// {21.6065524428379092, 3.88986167789384707, 0.0238386363683683114, +// 23.1411626921050093, 6.2216810431377656, 0.353558402011174056, +// 26.0424328294544232, 10.2156011934593263, 2.05700439027528237, +// 28.6992602369071967, 13.7425580125613358, 4.45783894060629216} +// } +// // Deng, Li and Zhou [2008] +// { +// {150, 60, 50}, {0.3, 0.3, 0.3}, {0, 0, 0}, 0.05, +// {{{1.0, 0.2, 0.8}, +// {0.2, 1.0, 0.4}, +// {0.8, 0.4, 1.0} +// }}, +// {1, -1, -1}, {0.25}, {30, 35, 40, 45, 50}, Option::Call, +// {13.5670355467464869, 10.3469714924350296, 7.65022045034505815, +// 5.48080150445291903, 3.80525160380840344} +// }, +// { +// {150, 60, 50}, {0.6, 0.6, 0.6}, {0, 0, 0}, 0.05, +// {{{1.0, 0.2, 0.8}, +// {0.2, 1.0, 0.4}, +// {0.8, 0.4, 1.0} +// }}, +// {1, -1, -1}, {0.25}, {30, 35, 40, 45, 50}, Option::Call, +// {20.187167856927644, 17.4567855185085179, 15.0073026904179034, +// 12.8307539528848373, 10.9140154840369128} +// }, { - {100.0, 96.0}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{0.5}}, - {1.0, -1.0}, {1.0}, - {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Call, - {8.312460732881519,8.114993760660171,7.920819775954081,7.729932490363331,7.542323895849758,7.35798429885716,7.176902356575362,6.999065115204262,6.824458050072985,6.653065107468672} + Array(11, 10.0), + Array(11, 0.3), Array(11, 0.0), 0.05, {{{0.4}}}, + {11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, + {0.25}, {0, 5, 10, 15, 20}, Option::Call, + {11.5795246248385411, 8.11486124235371697, 5.36890684803262097, + 3.35146299782942902, 1.97711593317682488 } } }; const DayCounter dc = Actual365Fixed(); const Date today = Date(26, September, 2024); + typedef std::vector> + BlackScholesProcesses; + + const auto choiEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return ext::make_shared(p, rho, 3, false, false); + }; + + const auto dengLiZhouEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return ext::make_shared(p, rho); + }; + + const auto kirkEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return ext::make_shared(p[0], p[1], rho[0][1]); + }; + + const auto bsEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return ext::make_shared(p[0], p[1], rho[0][1]); + }; + + const auto osFirstOrderEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return ext::make_shared( + p[0], p[1], rho[0][1], OperatorSplittingSpreadEngine::First); + }; + + const auto osSecondOrderEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return ext::make_shared( + p[0], p[1], rho[0][1], OperatorSplittingSpreadEngine::Second); + }; + + const auto qmcEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return MakeMCEuropeanBasketEngine( + ext::make_shared( + std::vector >( + p.begin(), p.end() + ), + rho + ) + ) + .withSteps(1) + .withSamples(16374*16-1) + .withSeed(1234ul); + }; + + const auto fdmEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time t) + -> ext::shared_ptr { + return ext::make_shared( + p, rho, std::vector(p.size(), 15), + Size(15.0*t) + ); + }; + + typedef std::function( + const BlackScholesProcesses&, const Matrix&, Time)> + PricingEngineFactory; + + const std::vector> engines = { + {"Choi", choiEngine}, + {"Deng-Li-Zhou", dengLiZhouEngine}, +// {"Kirk", kirkEngine}, + {"Quasi-Monte-Carlo", qmcEngine} +// {"Bjerksund-Stensland", bsEngine}, +// {"Operator Splitting first order", osFirstOrderEngine}, +// {"Operator Splitting second order", osSecondOrderEngine}, +// {"FDM", fdmEngine} + }; + for (const auto& b: benchmarks) { const Size n = b.underlyings.size(); const Handle rTS = Handle(flatRate(today, b.r, dc)); - std::vector > processes; + BlackScholesProcesses processes; for (Size i=0; i < n; ++i) processes.push_back( ext::make_shared( @@ -2043,105 +2259,104 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { ) ); - Matrix rho(n, n); - for (Size i=0; i < n; ++i) - for (Size j=0; j < n; ++j) - rho[i][j] = (i == j) ? 1.0 : b.rho[0][0]; - - - const ext::shared_ptr choiEngine = - ext::make_shared(processes, rho, 20); - - const ext::shared_ptr dengLiZhou = - ext::make_shared(processes, rho); - - const ext::shared_ptr kirkEngine = - ext::make_shared(processes[0], processes[1], rho[0][1]); - - std::vector calculated; - for (Real t: b.maturities) { - const Date maturityDate = yearFractionToDate(dc, today, t); - const ext::shared_ptr exercise = - ext::make_shared(maturityDate); - - for (Real K: b.strikes) { - const ext::shared_ptr payoff = - ext::make_shared(b.optionType, K); - - const ext::shared_ptr basketPayoff = - ext::make_shared(payoff, b.weights); + for (const auto& engine: engines) { + std::vector calculated, runTimes; + + for (const auto& cor: b.rhos) { + Matrix rho(n, n); + if (cor.size1() == n && cor.size2() == n) + rho = cor; + else + for (Size i=0; i < n; ++i) + for (Size j=0; j < n; ++j) + rho[i][j] = (i == j) ? 1.0 : cor[0][0]; + + for (Real t: b.maturities) { + const Date maturityDate = yearFractionToDate(dc, today, t); + const ext::shared_ptr exercise = + ext::make_shared(maturityDate); + + const ext::shared_ptr pricingEngine = + engine.second(processes, rho, t); + + const bool isSpreadEngine = + ext::dynamic_pointer_cast( + pricingEngine) != nullptr; + + if (isSpreadEngine && b.weights != Array({1, -1})) + // benchmark not suitable for a two asset spread engine + continue; + + const bool isDengLiZhouEngine = + ext::dynamic_pointer_cast( + pricingEngine) != nullptr; + + if (isDengLiZhouEngine + && ( std::find_if(b.weights.begin(), b.weights.end(), + [](Real x) {return x < 0.0;} ) == b.weights.end() + || std::find_if(b.weights.begin(), b.weights.end(), + [](Real x) {return x > 0.0;} ) == b.weights.end())) + continue; + + const bool isFDMEngine = + ext::dynamic_pointer_cast( + pricingEngine) != nullptr; + if (isFDMEngine and n > 4) + continue; + + for (Real K: b.strikes) { + const ext::shared_ptr payoff = + ext::make_shared(b.optionType, K); + + const ext::shared_ptr basketPayoff = + (isSpreadEngine) + ? ext::shared_ptr( + ext::make_shared(payoff)) + : ext::shared_ptr( + ext::make_shared(payoff, b.weights)); + + BasketOption option(basketPayoff, exercise); + option.setPricingEngine(pricingEngine); + + Size npvCalculations = 1; + Size batch = 1; + auto t1 = std::chrono::high_resolution_clock::now(), t2 = t1; + + calculated.push_back(option.NPV()); + +// while (std::chrono::duration_cast( +// (t2 = std::chrono::high_resolution_clock::now()) - t1).count() < 10000000) { +// batch *= 4; +// for (Size i=0; i < batch; ++i, ++npvCalculations) { +// option.update(); +// option.NPV(); +// } +// } + runTimes.push_back( + std::chrono::duration_cast( + t2-t1).count()/Real(npvCalculations) + ); + } + } + } - const ext::shared_ptr spreadPayoff = - ext::make_shared(payoff); + if (calculated.size() > 0) { + const Array calculatedNPVs(calculated.begin(), calculated.end()); + const Array diff = b.referenceNPVs - calculatedNPVs; + const Array absDiff = Abs(diff); - BasketOption option(spreadPayoff, exercise); - option.setPricingEngine(kirkEngine); + const Real rmse = std::sqrt(DotProduct(diff, diff))/n; + const Real mae = std::accumulate(absDiff.begin(), absDiff.end(), 0.0)/n; - calculated.push_back(option.NPV()); + std::cout << engine.first << std::endl; + std::cout << "bla " << std::setprecision(18) << calculatedNPVs << std::endl; + std::cout << diff << std::endl; + std::cout << rmse << " " << mae << std::endl; - //std::cout << std::setprecision(16) << option.NPV() << "," ; + std::cout << std::endl; } } - - const Array calculatedNPVs(calculated.begin(), calculated.end()); - const Array diff = b.referenceNPVs - calculatedNPVs; - const Array absDiff = Abs(diff); - - const Real rmse = std::sqrt(DotProduct(diff, diff))/n; - const Real mae = std::accumulate(absDiff.begin(), absDiff.end(), 0.0)/n; - - std::cout << diff << std::endl; - std::cout << rmse << " " << mae << std::endl; - - std::cout << std::endl; - } -// const Real strike = std::inner_product( -// b.weights.begin(), b.weights.end(), b.underlyings.begin(), 0.0 -// ); -// -// const ext::shared_ptr payoff -// = ext::make_shared(t.optionType, strike); -// -// BasketOption option( -// ext::make_shared(payoff, d.weights), -// ext::make_shared(maturity) -// ); - - -// -// Size i=1024; -// for (; i < 1000000000; i*=2) { -// option.setPricingEngine( -// MakeMCEuropeanBasketEngine( -// //MakeMCEuropeanBasketEngine( -// ext::make_shared( -// std::vector >( -// processes.begin(), processes.end() -// ), -// rho -// ) -// ) -// .withSteps(1) -// .withSamples(i) -// .withSeed(1234ul) -// ); -// -// std::cout << i << " " << option.NPV() << std::endl; -// } -// -// option.setPricingEngine( -// ext::make_shared(processes, rho)); -// std::cout << option.NPV() << std::endl; -// -// -// option.setPricingEngine( -// ext::make_shared( -// processes, rho, -// std::vector({25, 25, 25, 25}), 75 -// ) -// ); -// std::cout << option.NPV() << std::endl; } BOOST_AUTO_TEST_SUITE_END() From 4e44689eea409ffe5e6e7c97c2cde31c326dfe0f Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 21 Oct 2024 20:06:31 +0200 Subject: [PATCH 17/36] large spread and basket option test case --- ql/pricingengines/basket/choibasketengine.cpp | 23 +- .../fdndimblackscholesvanillaengine.cpp | 1 - .../basket/operatorsplittingspreadengine.cpp | 2 - test-suite/basketoption.cpp | 343 +++++++++--------- 4 files changed, 194 insertions(+), 175 deletions(-) diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index 29b5bac3da9..b3945401782 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -55,6 +55,8 @@ namespace QuantLib { QL_REQUIRE(n_ == rho_.size1() && rho_.size1() == rho_.size2(), "process and correlation matrix must have the same size."); + QL_REQUIRE(lambda_ > 0.0, "lambda must be positive"); + std::for_each(processes_.begin(), processes_.end(), [this](const auto& p) { registerWith(p); }); } @@ -136,9 +138,19 @@ namespace QuantLib { ); std::vector nIntOrder(n_-1); - const Real intScale = lambda_ / std::abs(DotProduct(g, vStar1)); - for (Size i=0; i < n_-1; ++i) - nIntOrder[i] = Size(std::lround(1 + intScale*sv[i])); + Real lambda = lambda_; + const Real alpha = 1/std::abs(DotProduct(g, vStar1)); + do { + const Real intScale = lambda * alpha; + for (Size i=0; i < n_-1; ++i) + nIntOrder[i] = Size(std::lround(1 + intScale*sv[i])); + + lambda*=0.9; + QL_REQUIRE(lambda/lambda_ > 1e-10, + "can not rescale lambda to fit max integration order"); + } while (std::accumulate( + nIntOrder.begin(), nIntOrder.end(), 1.0, std::multiplies<>()) + > Real(maxNrIntegrationSteps_)); std::vector > quotes; std::vector > p; @@ -174,11 +186,6 @@ namespace QuantLib { [](Real acc, Real x) -> Real { return acc + x*x; } ); - for (Size i=0; i < nIntOrder.size(); ++i) - std::cout << nIntOrder[i] << " "; - std::cout << std::endl; - - MulitDimGaussianIntegration ghq( nIntOrder, [](const Size n) { return ext::make_shared(n); } diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp index 29132c4011f..fc01f3752a6 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -27,7 +27,6 @@ #include #include -#include namespace QuantLib { diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp index c93746ce181..851ef1f74b8 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp @@ -22,8 +22,6 @@ #include #include -#include - namespace QuantLib { OperatorSplittingSpreadEngine::OperatorSplittingSpreadEngine( diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 2801cc9576d..b714c5c989e 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -48,8 +48,6 @@ #include #include -#include -#include #include using namespace QuantLib; @@ -1194,7 +1192,7 @@ BOOST_AUTO_TEST_CASE(testOperatorSplittingSpreadEngine) { { 0.7, 9.0863, 9.0862}, { 0.9, 6.9148, 6.9134} }; - for (Size i = 0; i < LENGTH(testData); ++i) { + for (Size i = 0; i < std::size(testData); ++i) { const Real rho = testData[i][0]; Real expected = testData[i][1]; @@ -2022,7 +2020,6 @@ BOOST_AUTO_TEST_CASE(testGoldenChoiBasketEngineExample) { << "\n expected: " << expected[i] << "\n diff: " << npvDiff << "\n tolerance: " << npvTol); - } } } @@ -2064,106 +2061,136 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { }; const std::vector benchmarks = { -// // Dempster and Hong [2002], Hurd and Zhou [2010] -// { -// {100.0, 96.0}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{{0.5}}}, -// {1.0, -1.0}, {1.0}, -// {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Put, -// {4.86947800209290982, 5.03394599708595702, 5.20170697959426764, 5.37275466121791023, 5.54708103391874285, 5.72467640414054557, 5.90552942907314105, 6.08962715491644957, 6.27695505699956779, 6.46749708160964865} -// }, -// { -// {100, 96}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{{0.5}}}, -// {1.0, -1.0}, {1.0}, -// {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Call, -// {8.312460732881519, 8.114993760660171, 7.920819775954081, -// 7.729932490363331, 7.542323895849758, 7.35798429885716, 7.176902356575362, -// 6.999065115204262, 6.824458050072985, 6.653065107468672} -// }, -// // Choi [2018] -// { -// {200, 100}, {0.15, 0.3}, {0.0, 0.0}, 0.0, -// { {{-0.9}}, {{-0.7}}, {{-0.5}}, {{-0.3}}, {{-0.1}}, {{0.1}}, {{0.3}}, {{0.5}}, {{0.7}}, {{0.9}} }, -// {1.0, -1.0}, {1.0}, {100}, Option::Call, -// {23.1398673777858619, 21.9077989170003313, 20.5982705317786383, 19.1954201364940467, -// 17.6770248596142956, 16.0102190445729207, 14.1425869461427691, 11.9804918293938165, -// 9.32094392217566181, 5.47927202785675949} -// }, -// // Krekel et al [2004], Caldana et al. [2016] -// { -// {100, 100, 100, 100}, {0.4, 0.4, 0.4, 0.4}, {0, 0, 0, 0}, 0, {{{0.5}}}, -// {0.25, 0.25, 0.25, 0.25}, {5}, {50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150}, -// Option::Call, -// {54.3101760503818554, 47.4811264983728805, 41.5225192321721579, 36.3517843455707421, -// 31.8768031971830865, 28.0073695445039341, 24.6605295130931736, 21.7625788671709337, -// 19.2493294434234272, 17.0655419939919533, 15.1640102889333352} -// }, -// { -// {100, 100, 100, 100}, {0.4, 0.4, 0.4, 0.4}, {0, 0, 0, 0}, 0, -// {{{-0.1}}, {{0.1}}, {{0.3}}, {{0.5}}, {{0.8}}, {{0.95}}}, -// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, -// {17.756916333753729, 21.6920964834602223, 25.029299237118412, -// 28.0073695445038631, 32.0412264523680363, 33.9186874338078042} -// }, -// { -// {100, 100, 100, 100}, {0.05, 0.05, 0.05, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, -// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, {19.4590949762084549} -// }, -// { -// {100, 100, 100, 100}, {0.4, 0.4, 0.4, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, -// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, {36.048540687480191 } -// }, -// { -// {100, 100, 100, 100}, {0.8, 0.8, 0.8, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, -// {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Put, {56.7772198387342684} -// }, -// // Milevsky and Posner [1998], Zhou and Wnag [2008] -// { -// {100, 100, 100, 100, 100, 100, 100}, -// {0.1155, 0.2068, 0.1453, 0.1799, 0.1559, 0.1462, 0.1568}, -// {0.0169, 0.0239, 0.0136, 0.0192, 0.0081, 0.0362, 0.0166}, 0.063, -// {{{1.00, 0.35, 0.10, 0.27, 0.04, 0.17, 0.71}, -// {0.35, 1.00, 0.39, 0.27, 0.50,-0.08, 0.15}, -// {0.10, 0.39, 1.00, 0.53, 0.70,-0.23, 0.09}, -// {0.27, 0.27, 0.53, 1.00, 0.46,-0.22, 0.32}, -// {0.04, 0.50, 0.70, 0.46, 1.00,-0.29, 0.13}, -// {0.17,-0.08,-0.23,-0.22,-0.29, 1.00,-0.03}, -// {0.71, 0.15, 0.09, 0.32, 0.13,-0.03, 1.00} -// }}, -// {0.10, 0.15, 0.15, 0.05, 0.20, 0.10, 0.25}, -// {0.5, 1, 2, 3}, {80, 100, 120}, Option::Call, -// {21.6065524428379092, 3.88986167789384707, 0.0238386363683683114, -// 23.1411626921050093, 6.2216810431377656, 0.353558402011174056, -// 26.0424328294544232, 10.2156011934593263, 2.05700439027528237, -// 28.6992602369071967, 13.7425580125613358, 4.45783894060629216} -// } -// // Deng, Li and Zhou [2008] -// { -// {150, 60, 50}, {0.3, 0.3, 0.3}, {0, 0, 0}, 0.05, -// {{{1.0, 0.2, 0.8}, -// {0.2, 1.0, 0.4}, -// {0.8, 0.4, 1.0} -// }}, -// {1, -1, -1}, {0.25}, {30, 35, 40, 45, 50}, Option::Call, -// {13.5670355467464869, 10.3469714924350296, 7.65022045034505815, -// 5.48080150445291903, 3.80525160380840344} -// }, -// { -// {150, 60, 50}, {0.6, 0.6, 0.6}, {0, 0, 0}, 0.05, -// {{{1.0, 0.2, 0.8}, -// {0.2, 1.0, 0.4}, -// {0.8, 0.4, 1.0} -// }}, -// {1, -1, -1}, {0.25}, {30, 35, 40, 45, 50}, Option::Call, -// {20.187167856927644, 17.4567855185085179, 15.0073026904179034, -// 12.8307539528848373, 10.9140154840369128} -// }, + // Dempster and Hong [2002], Hurd and Zhou [2010] + { + {100.0, 96.0}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{{0.5}}}, + {1.0, -1.0}, {1.0}, + {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Put, + {4.86947800209290982, 5.03394599708595702, 5.20170697959426764, 5.37275466121791023, 5.54708103391874285, 5.72467640414054557, 5.90552942907314105, 6.08962715491644957, 6.27695505699956779, 6.46749708160964865} + }, + { + {100, 96}, {0.2, 0.1}, {0.05, 0.05}, 0.1, {{{0.5}}}, + {1.0, -1.0}, {1.0}, + {0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8, 3.2, 3.6, 4.0}, Option::Call, + {8.312460732881519, 8.114993760660171, 7.920819775954081, + 7.729932490363331, 7.542323895849758, 7.35798429885716, 7.176902356575362, + 6.999065115204262, 6.824458050072985, 6.653065107468672} + }, + // Choi [2018] + { + {200, 100}, {0.15, 0.3}, {0.0, 0.0}, 0.0, + { {{-0.9}}, {{-0.7}}, {{-0.5}}, {{-0.3}}, {{-0.1}}, {{0.1}}, {{0.3}}, {{0.5}}, {{0.7}}, {{0.9}} }, + {1.0, -1.0}, {1.0}, {100}, Option::Call, + {23.1398673777858619, 21.9077989170003313, 20.5982705317786383, 19.1954201364940467, + 17.6770248596142956, 16.0102190445729207, 14.1425869461427691, 11.9804918293938165, + 9.32094392217566181, 5.47927202785675949} + }, + // Chi-Fai Lo [2016] + { + {110, 90}, {0.3, 0.2}, {0.0, 0.0}, 0.05, {{{0.6}}}, + {1.0, -1.0}, {1, 2, 3, 4, 5}, {10, 20, 60}, Option::Call, + {16.1049476565509657, 10.9766115516406035, 1.83123363415313212, 20.2228932552954817, + 15.6927477228442918, 5.41564349607575757, 23.4547216033491992, 19.3375645886881351, + 8.93714184429789604, 26.1938805393685357, 22.4110876517984252, 12.2205433053952373, + 28.6052191202546133, 25.1093443516670014, 15.2670988551783733} + }, + { + {110, 90}, {0.3, 0.2}, {0.03, 0.02}, 0.05, + {{{0.9}}, {{0.7}}, {{0.5}}, {{0.3}}, {{0.1}}, {{-0.1}}, {{-0.3}}, {{-0.5}}, {{-0.7}}, {{-0.9}}}, + {1.0, -1.0}, {1}, {20}, Option::Put, + {7.40655995328727013, 9.57965665828979596, 11.3257533225283407, 12.8253504634935833, + 14.1588257209597579, 15.3703653131104065, 16.4873731315140475, 17.5282444792535337, + 18.5060419239761167, 19.4304406116564437} + }, + // Krekel et al [2004], Caldana et al. [2016] + { + {100, 100, 100, 100}, {0.4, 0.4, 0.4, 0.4, 0.4}, {0, 0, 0, 0, 0}, 0, {{{0.5}}}, + {0.25, 0.25, 0.25, 0.25}, {5}, {50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150}, + Option::Call, + {54.3101760503818554, 47.4811264983728805, 41.5225192321721579, 36.3517843455707421, + 31.8768031971830865, 28.0073695445039341, 24.6605295130931736, 21.7625788671709337, + 19.2493294434234272, 17.0655419939919533, 15.1640102889333352} + }, + { + {100, 100, 100, 100}, {0.4, 0.4, 0.4, 0.4}, {0, 0, 0, 0}, 0, + {{{-0.1}}, {{0.1}}, {{0.3}}, {{0.5}}, {{0.8}}, {{0.95}}}, + {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, + {17.756916333753729, 21.6920964834602223, 25.029299237118412, + 28.0073695445038631, 32.0412264523680363, 33.9186874338078042} + }, + { + {100, 100, 100, 100}, {0.05, 0.05, 0.05, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, + {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, {19.4590949762084549} + }, + { + {100, 100, 100, 100}, {0.4, 0.4, 0.4, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, + {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Call, {36.048540687480191 } + }, + { + {100, 100, 100, 100}, {0.8, 0.8, 0.8, 1.0}, {0, 0, 0, 0}, 0, {{{0.5}}}, + {0.25, 0.25, 0.25, 0.25}, {5}, {100}, Option::Put, {56.7772198387342684} + }, + // Milevsky and Posner [1998], Zhou and Wnag [2008] + { + {100, 100, 100, 100, 100, 100, 100}, + {0.1155, 0.2068, 0.1453, 0.1799, 0.1559, 0.1462, 0.1568}, + {0.0169, 0.0239, 0.0136, 0.0192, 0.0081, 0.0362, 0.0166}, 0.063, + {{{1.00, 0.35, 0.10, 0.27, 0.04, 0.17, 0.71}, + {0.35, 1.00, 0.39, 0.27, 0.50,-0.08, 0.15}, + {0.10, 0.39, 1.00, 0.53, 0.70,-0.23, 0.09}, + {0.27, 0.27, 0.53, 1.00, 0.46,-0.22, 0.32}, + {0.04, 0.50, 0.70, 0.46, 1.00,-0.29, 0.13}, + {0.17,-0.08,-0.23,-0.22,-0.29, 1.00,-0.03}, + {0.71, 0.15, 0.09, 0.32, 0.13,-0.03, 1.00} + }}, + {0.10, 0.15, 0.15, 0.05, 0.20, 0.10, 0.25}, + {0.5, 1, 2, 3}, {80, 100, 120}, Option::Call, + {21.6065524428379092, 3.88986167789384707, 0.0238386363683683114, + 23.1411626921050093, 6.2216810431377656, 0.353558402011174056, + 26.0424328294544232, 10.2156011934593263, 2.05700439027528237, + 28.6992602369071967, 13.7425580125613358, 4.45783894060629216} + }, + // Deng, Li and Zhou [2008] + { + {150, 60, 50}, {0.3, 0.3, 0.3}, {0, 0, 0}, 0.05, + {{{1.0, 0.2, 0.8}, + {0.2, 1.0, 0.4}, + {0.8, 0.4, 1.0} + }}, + {1, -1, -1}, {0.25}, {30, 35, 40, 45, 50}, Option::Call, + {13.5670355467464869, 10.3469714924350296, 7.65022045034505815, + 5.48080150445291903, 3.80525160380840344} + }, + { + {150, 60, 50}, {0.6, 0.6, 0.6}, {0, 0, 0}, 0.05, + {{{1.0, 0.2, 0.8}, + {0.2, 1.0, 0.4}, + {0.8, 0.4, 1.0} + }}, + {1, -1, -1}, {0.25}, {30, 35, 40, 45, 50}, Option::Call, + {20.187167856927644, 17.4567855185085179, 15.0073026904179034, + 12.8307539528848373, 10.9140154840369128} + }, { Array(11, 10.0), Array(11, 0.3), Array(11, 0.0), 0.05, {{{0.4}}}, {11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, {0.25}, {0, 5, 10, 15, 20}, Option::Call, - {11.5795246248385411, 8.11486124235371697, 5.36890684803262097, - 3.35146299782942902, 1.97711593317682488 } + {11.5795246248372834, 8.11486124233140238, 5.36890684802773066, + 3.35146299782513601, 1.97711593318812251} + }, + // unknown + { + {80, 120, 100, 100}, {0.3, 0.4, 0.2, 0.35}, {0.01, 0.03, 0.07, 0.04}, 0.03, + {{{1.0, 0.5, 0.35, 0.35}, + {0.5, 1.0, 0.5, 0.6}, + {0.35, 0.5, 1.0,-0.1}, + {0.35, 0.6,-0.1, 1.0} + }}, + {2, 1, -1, -1.5}, {1.5}, {-10, -5, 5, 10, 15, 20, 25, 30, 35, 40, 45}, Option::Put, + {8.261706095014931, 9.48942603546257, 12.36147376566713, 14.01364513745725, + 15.81293112893055, 17.75999586876829, 19.85432452565376, 22.09433488973327, + 24.47750315548787, 27.00049629189819, 29.65930486322155} } }; @@ -2175,7 +2202,12 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { const auto choiEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) -> ext::shared_ptr { - return ext::make_shared(p, rho, 3, false, false); + return ext::make_shared(p, rho, 20, 2 << 12); + }; + + const auto choiCvEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) + -> ext::shared_ptr { + return ext::make_shared(p, rho, 20, 2 << 12, true, true); }; const auto dengLiZhouEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time) @@ -2216,51 +2248,44 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { ) ) .withSteps(1) - .withSamples(16374*16-1) - .withSeed(1234ul); - }; - - const auto fdmEngine = [](const BlackScholesProcesses& p, const Matrix& rho, Time t) - -> ext::shared_ptr { - return ext::make_shared( - p, rho, std::vector(p.size(), 15), - Size(15.0*t) - ); + .withSamples(4096-1) + .withSeed(12345ul); }; typedef std::function( const BlackScholesProcesses&, const Matrix&, Time)> PricingEngineFactory; - const std::vector> engines = { - {"Choi", choiEngine}, - {"Deng-Li-Zhou", dengLiZhouEngine}, -// {"Kirk", kirkEngine}, - {"Quasi-Monte-Carlo", qmcEngine} -// {"Bjerksund-Stensland", bsEngine}, -// {"Operator Splitting first order", osFirstOrderEngine}, -// {"Operator Splitting second order", osSecondOrderEngine}, -// {"FDM", fdmEngine} + const std::vector> engines = { + {"Choi", choiEngine, 0.000182177}, + {"Choi Control Variate", choiCvEngine, 0.000228738}, + {"Deng-Li-Zhou", dengLiZhouEngine, 0.0629703}, + {"Kirk", kirkEngine, 0.030673}, + {"Quasi-Monte-Carlo", qmcEngine, 0.28862}, + {"Bjerksund-Stensland", bsEngine, 0.0222423}, + {"Operator Splitting first order", osFirstOrderEngine, 0.00406318}, + {"Operator Splitting second order", osSecondOrderEngine, 0.000317259} }; - for (const auto& b: benchmarks) { - const Size n = b.underlyings.size(); + for (const auto& engine: engines) { + std::vector diff, relDiff; - const Handle rTS - = Handle(flatRate(today, b.r, dc)); + for (const auto& b: benchmarks) { + std::vector calculated; + const Size n = b.underlyings.size(); - BlackScholesProcesses processes; - for (Size i=0; i < n; ++i) - processes.push_back( - ext::make_shared( - Handle(ext::make_shared(b.underlyings[i])), - Handle(flatRate(today, b.q[i], dc)), rTS, - Handle(flatVol(today, b.volatilities[i], dc)) - ) - ); + const Handle rTS + = Handle(flatRate(today, b.r, dc)); - for (const auto& engine: engines) { - std::vector calculated, runTimes; + BlackScholesProcesses processes; + for (Size i=0; i < n; ++i) + processes.push_back( + ext::make_shared( + Handle(ext::make_shared(b.underlyings[i])), + Handle(flatRate(today, b.q[i], dc)), rTS, + Handle(flatVol(today, b.volatilities[i], dc)) + ) + ); for (const auto& cor: b.rhos) { Matrix rho(n, n); @@ -2277,7 +2302,7 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { ext::make_shared(maturityDate); const ext::shared_ptr pricingEngine = - engine.second(processes, rho, t); + std::get<1>(engine)(processes, rho, t); const bool isSpreadEngine = ext::dynamic_pointer_cast( @@ -2318,42 +2343,32 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { BasketOption option(basketPayoff, exercise); option.setPricingEngine(pricingEngine); - Size npvCalculations = 1; - Size batch = 1; - auto t1 = std::chrono::high_resolution_clock::now(), t2 = t1; - calculated.push_back(option.NPV()); - -// while (std::chrono::duration_cast( -// (t2 = std::chrono::high_resolution_clock::now()) - t1).count() < 10000000) { -// batch *= 4; -// for (Size i=0; i < batch; ++i, ++npvCalculations) { -// option.update(); -// option.NPV(); -// } -// } - runTimes.push_back( - std::chrono::duration_cast( - t2-t1).count()/Real(npvCalculations) - ); } } } if (calculated.size() > 0) { - const Array calculatedNPVs(calculated.begin(), calculated.end()); - const Array diff = b.referenceNPVs - calculatedNPVs; - const Array absDiff = Abs(diff); - - const Real rmse = std::sqrt(DotProduct(diff, diff))/n; - const Real mae = std::accumulate(absDiff.begin(), absDiff.end(), 0.0)/n; - - std::cout << engine.first << std::endl; - std::cout << "bla " << std::setprecision(18) << calculatedNPVs << std::endl; - std::cout << diff << std::endl; - std::cout << rmse << " " << mae << std::endl; + for (Size i=0; i < calculated.size(); ++i) { + diff.push_back(b.referenceNPVs[i] - calculated[i]); + relDiff.push_back(diff.back()/b.referenceNPVs[i]); + } + } + } - std::cout << std::endl; + if (diff.size() > 0) { + const Real calculatedRmse = std::sqrt( + DotProduct(Array(diff.begin(), diff.end()), + Array(diff.begin(), diff.end()) ) /diff.size()); + + const Real expectedRmse = std::get<2>(engine); + const Real relTol = 5; + if (calculatedRmse / expectedRmse > relTol) { + BOOST_FAIL( + "failed to reproduce basket- and spread-option benchmark prices" + << "\n Engine : " << std::get<0>(engine) + << "\n expected RMSE : " << expectedRmse + << "\n calculated RMSE: " << calculatedRmse); } } } From 3ffc47ac97f35bef755bedc31e38c23d3ff96140 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 21 Oct 2024 20:32:16 +0200 Subject: [PATCH 18/36] fixed smaller issues --- ql/pricingengines/basket/choibasketengine.cpp | 4 ++-- .../basket/fdndimblackscholesvanillaengine.cpp | 8 ++------ .../basket/fdndimblackscholesvanillaengine.hpp | 6 +----- ql/pricingengines/basket/singlefactorbsmbasketengine.cpp | 2 +- ql/pricingengines/basket/singlefactorbsmbasketengine.hpp | 2 +- test-suite/basketoption.cpp | 2 +- 6 files changed, 8 insertions(+), 16 deletions(-) diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index b3945401782..9db60572174 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -92,9 +92,9 @@ namespace QuantLib { const Matrix C = CholeskyDecomposition(Sigma); - constexpr Real eps = 100*std::sqrt(QL_EPSILON); + const Real eps = 100*std::sqrt(QL_EPSILON); // publication sets tol=0, pyfeng implementation sets tol=0.01 - constexpr Real tol = 100*std::sqrt(QL_EPSILON); + const Real tol = 100*std::sqrt(QL_EPSILON); bool flip = false; for (Size i=0; i < n_; ++i) diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp index fc01f3752a6..3a03df2fbee 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -35,17 +35,13 @@ namespace QuantLib { Matrix correlation, std::vector xGrids, Size tGrid, Size dampingSteps, - const FdmSchemeDesc& schemeDesc, - bool localVol, - Real illegalLocalVolOverwrite) + const FdmSchemeDesc& schemeDesc) : processes_(std::move(processes)), correlation_(std::move(correlation)), xGrids_(std::move(xGrids)), tGrid_(tGrid), dampingSteps_(dampingSteps), - schemeDesc_(schemeDesc), - localVol_(localVol), - illegalLocalVolOverwrite_(illegalLocalVolOverwrite) { + schemeDesc_(schemeDesc) { QL_REQUIRE(!processes_.empty(), "no Black-Scholes process is given."); QL_REQUIRE(correlation_.size1() == correlation_.size2() diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp index f973641c49a..12ed48f5aad 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp @@ -47,9 +47,7 @@ namespace QuantLib { Matrix correlation, std::vector xGrids, Size tGrid = 50, Size dampingSteps = 0, - const FdmSchemeDesc& schemeDesc = FdmSchemeDesc::Hundsdorfer(), - bool localVol = false, - Real illegalLocalVolOverwrite = -Null()); + const FdmSchemeDesc& schemeDesc = FdmSchemeDesc::Hundsdorfer()); void calculate() const override; @@ -59,8 +57,6 @@ namespace QuantLib { const std::vector xGrids_; const Size tGrid_, dampingSteps_; const FdmSchemeDesc schemeDesc_; - const bool localVol_; - const Real illegalLocalVolOverwrite_; }; } diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp index 8083a2bf0ba..9ea29061e8a 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -17,7 +17,7 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ -/*! \file singlefactorbsmnasketengine.cpp +/*! \file singlefactorbsmbasketengine.cpp */ #include diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index eda427149f3..57714582558 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -17,7 +17,7 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ -/*! \file singlefactorbsmnasketengine.hpp +/*! \file singlefactorbsmbasketengine.hpp \brief Basket engine where all underlyings are driven by one stochastic factor */ diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index b714c5c989e..66bf3b85a1c 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1963,7 +1963,7 @@ BOOST_AUTO_TEST_CASE(testGoldenChoiBasketEngineExample) { }; const ext::shared_ptr engine = - ext::make_shared(processes, rho, 7.0, true, true); + ext::make_shared(processes, rho, 7.0, 10000, true, true); const Array expected = {15.92008513388834, 22.36122704630282}; const std::vector optionTypes = {Option::Put, Option::Call}; From 7472600ce946ba61a2af54ad1b7759fe9b4004ba Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 21 Oct 2024 21:06:54 +0200 Subject: [PATCH 19/36] fixed smaller issues --- ql/math/matrixutilities/Makefile.am | 2 ++ ql/math/solvers1d/Makefile.am | 1 + .../finitedifferences/operators/Makefile.am | 2 ++ ql/pricingengines/basket/Makefile.am | 20 +++++++++++++++++-- .../basket/operatorsplittingspreadengine.cpp | 2 +- .../basket/vectorbsmprocessextractor.cpp | 2 +- 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ql/math/matrixutilities/Makefile.am b/ql/math/matrixutilities/Makefile.am index 47fa2fbc5b0..f799326d290 100644 --- a/ql/math/matrixutilities/Makefile.am +++ b/ql/math/matrixutilities/Makefile.am @@ -11,6 +11,7 @@ this_include_HEADERS = \ factorreduction.hpp \ getcovariance.hpp \ gmres.hpp \ + householder.hpp \ pseudosqrt.hpp \ qrdecomposition.hpp \ sparseilupreconditioner.hpp \ @@ -28,6 +29,7 @@ cpp_files = \ factorreduction.cpp \ getcovariance.cpp \ gmres.cpp \ + householder.cpp \ pseudosqrt.cpp \ qrdecomposition.cpp \ sparseilupreconditioner.cpp \ diff --git a/ql/math/solvers1d/Makefile.am b/ql/math/solvers1d/Makefile.am index d3d866344c5..8d3e7099fd5 100644 --- a/ql/math/solvers1d/Makefile.am +++ b/ql/math/solvers1d/Makefile.am @@ -8,6 +8,7 @@ this_include_HEADERS = \ brent.hpp \ falseposition.hpp \ finitedifferencenewtonsafe.hpp \ + halley.hpp \ newton.hpp \ newtonsafe.hpp \ ridder.hpp \ diff --git a/ql/methods/finitedifferences/operators/Makefile.am b/ql/methods/finitedifferences/operators/Makefile.am index b3cb2fb6656..755b82ce572 100644 --- a/ql/methods/finitedifferences/operators/Makefile.am +++ b/ql/methods/finitedifferences/operators/Makefile.am @@ -20,6 +20,7 @@ this_include_HEADERS = \ fdmlinearopiterator.hpp \ fdmlinearoplayout.hpp \ fdmlocalvolfwdop.hpp \ + fdmndimblackscholesop.hpp \ fdmornsteinuhlenbeckop.hpp \ fdmsabrop.hpp \ fdmsquarerootfwdop.hpp \ @@ -46,6 +47,7 @@ cpp_files = \ fdmhullwhiteop.cpp \ fdmlinearoplayout.cpp \ fdmlocalvolfwdop.cpp \ + fdmndimblackscholesop.cpp \ fdmornsteinuhlenbeckop.cpp \ fdmsabrop.cpp \ fdmsquarerootfwdop.cpp \ diff --git a/ql/pricingengines/basket/Makefile.am b/ql/pricingengines/basket/Makefile.am index fc2defd8e7f..05a6fbb26e4 100644 --- a/ql/pricingengines/basket/Makefile.am +++ b/ql/pricingengines/basket/Makefile.am @@ -4,18 +4,34 @@ AM_CPPFLAGS = -I${top_builddir} -I${top_srcdir} this_includedir=${includedir}/${subdir} this_include_HEADERS = \ all.hpp \ + bjerksundstenslandspreadengine.hpp \ + choibasketengine.hpp \ + denglizhoubasketengine.hpp \ fd2dblackscholesvanillaengine.hpp \ + fdndimblackscholesvanillaengine.hpp \ kirkengine.hpp \ mcamericanbasketengine.hpp \ mceuropeanbasketengine.hpp \ - stulzengine.hpp + operatorsplittingspreadengine.hpp \ + singlefactorbsmbasketengine.hpp \ + spreadblackscholesvanillaengine.hpp \ + stulzengine.hpp \ + vectorbsmprocessextractor.hpp cpp_files = \ + bjerksundstenslandspreadengine.cpp \ + choibasketengine.cpp \ + denglizhoubasketengine.cpp \ fd2dblackscholesvanillaengine.cpp \ + fdndimblackscholesvanillaengine.cpp \ kirkengine.cpp \ mcamericanbasketengine.cpp \ mceuropeanbasketengine.cpp \ - stulzengine.cpp + operatorsplittingspreadengine.cpp \ + singlefactorbsmbasketengine.cpp \ + spreadblackscholesvanillaengine.cpp \ + stulzengine.cpp \ + vectorbsmprocessextractor.cpp if UNITY_BUILD diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp index 851ef1f74b8..149cbc8d8a5 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp @@ -37,7 +37,7 @@ namespace QuantLib { Real f1, Real f2, Real k, Option::Type optionType, Real variance1, Real variance2, DiscountFactor df) const { - const auto callPutParityPrice = [this, f1, f2, df, k, optionType](Real callPrice) -> Real { + const auto callPutParityPrice = [f1, f2, df, k, optionType](Real callPrice) -> Real { if (optionType == Option::Call) return callPrice; else diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp index 0d8bf10d1fe..a8f68ee05ba 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp @@ -52,7 +52,7 @@ namespace QuantLib { QL_REQUIRE( std::equal( dr.begin()+1, dr.end(), dr.begin(), - std::pointer_to_binary_function(close_enough) + [](Real a, Real b) -> bool { return close_enough(a, b);} ), "interest rates need to be the same for all underlyings" ); From bd3a01ddb98103cd7dee61a98f609fcb1fbeb0ef Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 21 Oct 2024 21:11:53 +0200 Subject: [PATCH 20/36] added all.hpp files --- ql/math/matrixutilities/all.hpp | 1 + ql/math/solvers1d/all.hpp | 1 + ql/methods/finitedifferences/operators/all.hpp | 1 + ql/pricingengines/basket/all.hpp | 8 ++++++++ ql/pricingengines/basket/vectorbsmprocessextractor.cpp | 2 +- ql/pricingengines/basket/vectorbsmprocessextractor.hpp | 2 +- 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ql/math/matrixutilities/all.hpp b/ql/math/matrixutilities/all.hpp index 142b45a57bb..291eaa6a4c3 100644 --- a/ql/math/matrixutilities/all.hpp +++ b/ql/math/matrixutilities/all.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/ql/math/solvers1d/all.hpp b/ql/math/solvers1d/all.hpp index 6a9d63269fb..b66267ea35a 100644 --- a/ql/math/solvers1d/all.hpp +++ b/ql/math/solvers1d/all.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include diff --git a/ql/methods/finitedifferences/operators/all.hpp b/ql/methods/finitedifferences/operators/all.hpp index 2a76483616d..21642a5f34a 100644 --- a/ql/methods/finitedifferences/operators/all.hpp +++ b/ql/methods/finitedifferences/operators/all.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include diff --git a/ql/pricingengines/basket/all.hpp b/ql/pricingengines/basket/all.hpp index 54eed556a74..d45b41ded4e 100644 --- a/ql/pricingengines/basket/all.hpp +++ b/ql/pricingengines/basket/all.hpp @@ -1,9 +1,17 @@ /* This file is automatically generated; do not edit. */ /* Add the files to be included into Makefile.am instead. */ +#include +#include +#include #include +#include #include #include #include +#include +#include +#include #include +#include diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp index a8f68ee05ba..04b9adb4dd7 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.cpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.cpp @@ -17,7 +17,7 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ -/*! \file bsmprocessesextractor.cpp +/*! \file vectorbsmprocessextractor.cpp */ #include #include diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp index 662e85b4b07..a640f7a7ae1 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp @@ -17,7 +17,7 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ -/*! \file bsmprocessesextractor.hpp +/*! \file vectorbsmprocessextractor.hpp \brief helper class to extract underlying, volatility etc from a vector of processes */ From b40e6e81e83952455a5d7a8ff6094bbae097d44f Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 21 Oct 2024 22:02:54 +0200 Subject: [PATCH 21/36] fixed VS build files --- QuantLib.vcxproj | 23 ++++++++++++- QuantLib.vcxproj.filters | 67 +++++++++++++++++++++++++++++++++++-- test-suite/basketoption.cpp | 2 +- 3 files changed, 88 insertions(+), 4 deletions(-) mode change 100644 => 100755 test-suite/basketoption.cpp diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index b11f40de9d9..f52b31cf595 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -1,4 +1,4 @@ - + @@ -1066,6 +1066,7 @@ + @@ -1137,6 +1138,7 @@ + @@ -1198,6 +1200,7 @@ + @@ -1495,11 +1498,19 @@ + + + + + + + + @@ -2271,6 +2282,7 @@ + @@ -2344,6 +2356,7 @@ + @@ -2555,11 +2568,19 @@ + + + + + + + + diff --git a/QuantLib.vcxproj.filters b/QuantLib.vcxproj.filters index 1aa50931d2d..82513647d03 100644 --- a/QuantLib.vcxproj.filters +++ b/QuantLib.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -4413,6 +4413,39 @@ indexes\ibor + + math\matrixutilities + + + math\solvers1D + + + methods\finitedifferences\operators + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + @@ -7161,5 +7194,35 @@ time\daycounters + + math\matrixutilities + + + methods\finitedifferences\operators + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + + + pricingengines\basket + - + \ No newline at end of file diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp old mode 100644 new mode 100755 index 66bf3b85a1c..ba074d77dd9 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -2326,7 +2326,7 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { const bool isFDMEngine = ext::dynamic_pointer_cast( pricingEngine) != nullptr; - if (isFDMEngine and n > 4) + if (isFDMEngine && n > 4) continue; for (Real K: b.strikes) { From 1d4be6450d635761d0fd7b20fbaef1238be78e07 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 21 Oct 2024 22:28:36 +0200 Subject: [PATCH 22/36] fixed VS build files --- QuantLib.vcxproj | 2 +- test-suite/matrices.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index f52b31cf595..8cfad4faec0 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -1,4 +1,4 @@ - + diff --git a/test-suite/matrices.cpp b/test-suite/matrices.cpp index 63c855a9592..76e45eab6e9 100644 --- a/test-suite/matrices.cpp +++ b/test-suite/matrices.cpp @@ -863,8 +863,8 @@ namespace MatrixTests { BOOST_AUTO_TEST_CASE(testPrincipalMatrixSqrt) { BOOST_TEST_MESSAGE("Testing principal matrix pseudo sqrt..."); - std::vector dims = {1, 4, 10, 40}; - for (auto n: dims) { + std::vector dims = {1, 4, 10, 40}; + for (Size n: dims) { const Matrix rho = MatrixTests::createTestCorrelationMatrix(n); const Matrix sqrtRho = pseudoSqrt(rho, SalvagingAlgorithm::Principal); @@ -882,8 +882,8 @@ BOOST_AUTO_TEST_CASE(testCholeskySolverFor) { MersenneTwisterUniformRng rng(1234); - std::vector dims = {1, 4, 10, 25, 50}; - for (auto n: dims) { + std::vector dims = {1, 4, 10, 25, 50}; + for (Size n: dims) { Array b(n); for (Size i=0; i < n; ++i) From 84e723722613f6ead785dadd1362da7edbdde2bd Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 28 Oct 2024 19:47:55 +0100 Subject: [PATCH 23/36] Choi engine supports spread payoffs --- ql/CMakeLists.txt | 2 +- ql/pricingengines/basket/choibasketengine.cpp | 11 ++++++++++- ql/pricingengines/basket/choibasketengine.hpp | 4 ++-- ql/pricingengines/basket/denglizhoubasketengine.cpp | 11 ++++++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 498c5ef1ee1..2ee8ff9388c 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -1879,7 +1879,7 @@ set(QL_HEADERS pricingengines/barrier/fdhestonrebateengine.hpp pricingengines/barrier/mcbarrierengine.hpp pricingengines/basket/bjerksundstenslandspreadengine.hpp - pricingengines/basket/choibasketengine.hpp + pricingengines/basket/choibasketengine.hpp pricingengines/basket/vectorbsmprocessextractor.hpp pricingengines/basket/denglizhoubasketengine.hpp pricingengines/basket/fd2dblackscholesvanillaengine.hpp diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index 9db60572174..ecb0f573138 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -77,7 +77,16 @@ namespace QuantLib { const Array fwd = s * dq/dr0; const ext::shared_ptr avgPayoff = - ext::dynamic_pointer_cast(arguments_.payoff); + (ext::dynamic_pointer_cast(arguments_.payoff) != nullptr) + ? ext::dynamic_pointer_cast(arguments_.payoff) + : (ext::dynamic_pointer_cast(arguments_.payoff) != nullptr) + ? ext::make_shared( + ext::dynamic_pointer_cast( + arguments_.payoff)->basePayoff(), + Array({1.0, -1.0}) + ) + : ext::shared_ptr(); + QL_REQUIRE(avgPayoff, "average basket payoff expected"); const Array weights = avgPayoff->weights(); diff --git a/ql/pricingengines/basket/choibasketengine.hpp b/ql/pricingengines/basket/choibasketengine.hpp index 18af9a74135..799efc0f68a 100644 --- a/ql/pricingengines/basket/choibasketengine.hpp +++ b/ql/pricingengines/basket/choibasketengine.hpp @@ -46,11 +46,11 @@ namespace QuantLib { class ChoiBasketEngine : public BasketOption::engine { public: // lambda controls the precision, - // fast: 2, accurate: 6, high precision: 15 + // fast: 4, accurate: 8, high precision: 20 ChoiBasketEngine( std::vector > processes, Matrix rho, - Real lambda = 4.0, + Real lambda = 10.0, Size maxNrIntegrationSteps = std::numeric_limits::max(), bool calcfwdDelta = false, bool controlVariate = false); diff --git a/ql/pricingengines/basket/denglizhoubasketengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp index 1a06407569d..0327a48e1d1 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -50,7 +50,16 @@ namespace QuantLib { const Date maturityDate = exercise->lastDate(); const ext::shared_ptr avgPayoff = - ext::dynamic_pointer_cast(arguments_.payoff); + (ext::dynamic_pointer_cast(arguments_.payoff) != nullptr) + ? ext::dynamic_pointer_cast(arguments_.payoff) + : (ext::dynamic_pointer_cast(arguments_.payoff) != nullptr) + ? ext::make_shared( + ext::dynamic_pointer_cast( + arguments_.payoff)->basePayoff(), + Array({1.0, -1.0}) + ) + : ext::shared_ptr(); + QL_REQUIRE(avgPayoff, "average basket payoff expected"); // sort assets by their weight From a7fa540976d2c5a6d48927eab93177b0d2160a1a Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Tue, 5 Nov 2024 21:33:05 +0100 Subject: [PATCH 24/36] introduced FDM Wiener operator --- CMakeLists.txt | 2 +- ql/CMakeLists.txt | 2 + .../operators/fdmwienerop.cpp | 94 +++++++++ .../operators/fdmwienerop.hpp | 61 ++++++ ql/pricingengines/basket/choibasketengine.cpp | 4 +- .../basket/denglizhoubasketengine.cpp | 2 +- .../fdndimblackscholesvanillaengine.cpp | 185 +++++++++++++++++- .../fdndimblackscholesvanillaengine.hpp | 20 +- test-suite/basketoption.cpp | 135 ++++++++++++- test-suite/nthorderderivativeop.cpp | 31 --- 10 files changed, 484 insertions(+), 52 deletions(-) create mode 100644 ql/methods/finitedifferences/operators/fdmwienerop.cpp create mode 100644 ql/methods/finitedifferences/operators/fdmwienerop.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 69e32a2d092..22133f1d7fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ set(QL_INSTALL_CMAKEDIR "lib/cmake/${PACKAGE_NAME}" CACHE STRING "Installation directory for CMake scripts") # Options -option(QL_BUILD_EXAMPLES "Build examples" ON) +option(QL_BUILD_EXAMPLES "Build examples" OFF) option(QL_BUILD_TEST_SUITE "Build test suite" ON) option(QL_BUILD_FUZZ_TEST_SUITE "Build fuzz test suite" OFF) option(QL_ENABLE_OPENMP "Detect and use OpenMP" OFF) diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 2ee8ff9388c..f69397222f0 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -463,6 +463,7 @@ set(QL_SOURCES methods/finitedifferences/operators/fdmornsteinuhlenbeckop.cpp methods/finitedifferences/operators/fdmsabrop.cpp methods/finitedifferences/operators/fdmsquarerootfwdop.cpp + methods/finitedifferences/operators/fdmwienerop.cpp methods/finitedifferences/operators/firstderivativeop.cpp methods/finitedifferences/operators/ninepointlinearop.cpp methods/finitedifferences/operators/nthorderderivativeop.cpp @@ -1608,6 +1609,7 @@ set(QL_HEADERS methods/finitedifferences/operators/fdmornsteinuhlenbeckop.hpp methods/finitedifferences/operators/fdmsabrop.hpp methods/finitedifferences/operators/fdmsquarerootfwdop.hpp + methods/finitedifferences/operators/fdmwienerop.cpp methods/finitedifferences/operators/firstderivativeop.hpp methods/finitedifferences/operators/modtriplebandlinearop.hpp methods/finitedifferences/operators/ninepointlinearop.hpp diff --git a/ql/methods/finitedifferences/operators/fdmwienerop.cpp b/ql/methods/finitedifferences/operators/fdmwienerop.cpp new file mode 100644 index 00000000000..742e67b6dd1 --- /dev/null +++ b/ql/methods/finitedifferences/operators/fdmwienerop.cpp @@ -0,0 +1,94 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + + +/*! \file fdmndimwienerop.cpp +*/ + +#include +#include +#include +#include +#include + +#include + +namespace QuantLib { + + FdmWienerOp::FdmWienerOp( + ext::shared_ptr mesher, + ext::shared_ptr rTS, + Array lambdas) + : rTS_(rTS), + r_(0.0) { + + QL_REQUIRE(mesher->layout()->dim().size() == lambdas.size(), + "mesher and lambdas need to be of the same dimension"); + + for (Size i=0; i < lambdas.size(); ++i) + ops_.emplace_back(ext::make_shared( + SecondDerivativeOp(i, mesher) + .mult(Array(mesher->layout()->size(), 0.5*lambdas[i]))) + ); + } + + Size FdmWienerOp::size() const { + return ops_.size(); + } + + void FdmWienerOp::setTime(Time t1, Time t2) { + if (rTS_ != nullptr) + r_ = rTS_->forwardRate(t1, t2, Continuous).rate(); + } + + Array FdmWienerOp::apply(const Array& x) const { + Array y(-r_*x); + for (const auto& op: ops_) + y += op->apply(x); + + return y; + } + + Array FdmWienerOp::apply_mixed(const Array& x) const { + return Array(x.size(), 0.0); + } + + Array FdmWienerOp::apply_direction(Size direction, const Array& x) const { + return ops_[direction]->apply(x); + } + + Array FdmWienerOp::solve_splitting( + Size direction, const Array& x, Real s) const { + + return ops_[direction]->solve_splitting(x, s, 1.0); + } + + Array FdmWienerOp::preconditioner(const Array& r, Real dt) const { + return solve_splitting(0, r, dt); + } + + std::vector FdmWienerOp::toMatrixDecomp() const { + std::vector retVal; + + for (const auto& op: ops_) + retVal.push_back(op->toMatrix()); + + return retVal; + } +} diff --git a/ql/methods/finitedifferences/operators/fdmwienerop.hpp b/ql/methods/finitedifferences/operators/fdmwienerop.hpp new file mode 100644 index 00000000000..2cdc7f65b3f --- /dev/null +++ b/ql/methods/finitedifferences/operators/fdmwienerop.hpp @@ -0,0 +1,61 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + + +/*! \file fdmwienerop.hpp +*/ + +#ifndef quantlib_fdm_wiener_op_hpp +#define quantlib_fdm_wiener_op_hpp + +#include + +namespace QuantLib { + + class FdmMesher; + class YieldTermStructure; + class TripleBandLinearOp; + + class FdmWienerOp : public FdmLinearOpComposite { + public: + FdmWienerOp( + ext::shared_ptr mesher, + ext::shared_ptr rTS, + Array lambdas); + + Size size() const override; + void setTime(Time t1, Time t2) override; + Array apply(const Array& x) const override; + Array apply_mixed(const Array& x) const override; + + Array apply_direction(Size direction, const Array& x) const override; + + Array solve_splitting(Size direction, const Array& x, Real s) const override; + Array preconditioner(const Array& r, Real s) const override; + + std::vector toMatrixDecomp() const override; + + private: + const ext::shared_ptr rTS_; + std::vector > ops_; + Rate r_; + + }; +} +#endif diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index ecb0f573138..92c98b5aaae 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -71,7 +71,7 @@ namespace QuantLib { const detail::VectorBsmProcessExtractor pExtractor(processes_); const Array s = pExtractor.getSpot(); const Array dq = pExtractor.getDividendYieldDf(maturityDate); - const Array stdDev = pExtractor.getBlackStdDev(maturityDate); + const Array stdDev = Sqrt(pExtractor.getBlackVariance(maturityDate)); const DiscountFactor dr0 = pExtractor.getInterestRateDf(maturityDate); const Array fwd = s * dq/dr0; @@ -87,7 +87,7 @@ namespace QuantLib { ) : ext::shared_ptr(); - QL_REQUIRE(avgPayoff, "average basket payoff expected"); + QL_REQUIRE(avgPayoff, "average or spread basket payoff expected"); const Array weights = avgPayoff->weights(); QL_REQUIRE(n_ == weights.size() && n_ > 1, diff --git a/ql/pricingengines/basket/denglizhoubasketengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp index 0327a48e1d1..bb453d5688d 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -60,7 +60,7 @@ namespace QuantLib { ) : ext::shared_ptr(); - QL_REQUIRE(avgPayoff, "average basket payoff expected"); + QL_REQUIRE(avgPayoff, "average or spread basket payoff expected"); // sort assets by their weight const Array weights = avgPayoff->weights(); diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp index 3a03df2fbee..d1a14336857 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -18,43 +18,209 @@ */ #include +#include +#include +#include +#include #include -#include +#include +#include #include -#include #include #include #include +#include #include namespace QuantLib { + namespace detail { + class FdmPCABasketInnerValue: public FdmInnerValueCalculator { + public: + FdmPCABasketInnerValue( + ext::shared_ptr payoff, + ext::shared_ptr mesher, + Array logS0, Array vols, + std::vector> qTS, + ext::shared_ptr rTS, + Matrix Q, Array l) + : n_(logS0.size()), + payoff_(std::move(payoff)), + mesher_(std::move(mesher)), + logS0_(std::move(logS0)), v_(vols*vols), + qTS_(std::move(qTS)), + rTS_(std::move(rTS)), + Q_(std::move(Q)), l_(std::move(l)), + cachedT_(Null()), + qf_(n_) { } + + Real innerValue(const FdmLinearOpIterator& iter, Time t) override { + if (!close_enough(t, cachedT_)) { + rf_ = rTS_->discount(t); + for (Size i=0; i < n_; ++i) + qf_[i] = qTS_[i]->discount(t); + } + Array x(n_); + for (Size i=0; i < n_; ++i) + x[i] = mesher_->location(iter, i); + + const Array S = Exp(Q_*x - 0.5*v_*t + logS0_)*qf_/rf_; + + return (*payoff_)(S); + } + Real avgInnerValue(const FdmLinearOpIterator& iter, Time t) override { + return innerValue(iter, t); + } + + private: + const Size n_; + const ext::shared_ptr payoff_; + const ext::shared_ptr mesher_; + const Array logS0_, v_; + const std::vector> qTS_; + const ext::shared_ptr rTS_; + const Matrix Q_; + const Array l_; + Time cachedT_; + Array qf_; + DiscountFactor rf_; + }; + } + FdndimBlackScholesVanillaEngine::FdndimBlackScholesVanillaEngine( std::vector > processes, - Matrix correlation, + Matrix rho, std::vector xGrids, Size tGrid, Size dampingSteps, const FdmSchemeDesc& schemeDesc) : processes_(std::move(processes)), - correlation_(std::move(correlation)), + rho_(std::move(rho)), xGrids_(std::move(xGrids)), tGrid_(tGrid), dampingSteps_(dampingSteps), schemeDesc_(schemeDesc) { QL_REQUIRE(!processes_.empty(), "no Black-Scholes process is given."); - QL_REQUIRE(correlation_.size1() == correlation_.size2() - && correlation_.size1() == processes_.size(), + QL_REQUIRE(rho_.size1() == rho_.size2() + && rho_.size1() == processes_.size(), "correlation matrix has the wrong size."); - QL_REQUIRE(xGrids_.size() == processes_.size(), + QL_REQUIRE(xGrids_.size() == 1 || xGrids_.size() == processes_.size(), "wrong number of xGrids is given."); - for (const auto& process: processes_) - registerWith(process); + std::for_each(processes_.begin(), processes_.end(), + [this](const auto& p) { registerWith(p); }); } + FdndimBlackScholesVanillaEngine::FdndimBlackScholesVanillaEngine( + std::vector > processes, + Matrix rho, Size xGrid, Size tGrid, Size dampingSteps, + const FdmSchemeDesc& schemeDesc) + : FdndimBlackScholesVanillaEngine( + processes, rho, std::vector(1, xGrid), tGrid, dampingSteps, schemeDesc) + {} + + + void FdndimBlackScholesVanillaEngine::calculate() const { + #ifndef PDE_MAX_SUPPORTED_DIM + #define PDE_MAX_SUPPORTED_DIM 4 + #endif + QL_REQUIRE(processes_.size() <= PDE_MAX_SUPPORTED_DIM, + "This engine does not support " << processes_.size() << " underlyings. " + << "Max number of underlyings is " << PDE_MAX_SUPPORTED_DIM << ". " + << "Please change preprocessor constant PDE_MAX_SUPPORTED_DIM and recompile " + << "if a larger number of underlyings is needed."); + + const Date maturityDate = arguments_.exercise->lastDate(); + const Time maturity = processes_[0]->time(maturityDate); + const Real sqrtT = std::sqrt(maturity); + + const detail::VectorBsmProcessExtractor pExtractor(processes_); + const Array s = pExtractor.getSpot(); + const Array dq = pExtractor.getDividendYieldDf(maturityDate); + const Array stdDev = Sqrt(pExtractor.getBlackVariance(maturityDate)); + const Array vols = stdDev / sqrtT; + + const SymmetricSchurDecomposition schur( + getCovariance(vols.begin(), vols.end(), rho_)); + const Matrix Q = schur.eigenvectors(); + const Array l = schur.eigenvalues(); + const Real eps = 1e-4; + std::vector > meshers; + + for (Size i=0; i < processes_.size(); ++i) { + const Size xGrid = (xGrids_.size() > 1) + ? xGrids_[i] + : std::max(Size(4), Size(xGrids_[0]*std::sqrt(l[i]/l[0]))); + QL_REQUIRE(xGrid >= 4, "minimum grid size is four"); + + const Real xStepStize = (1.0-2*eps)/(xGrid-1); + + std::vector x(xGrid); + for (Size j=0; j < xGrid; ++j) + x[j] = 1.3*std::sqrt(l[i])*sqrtT + *InverseCumulativeNormal()(eps + j*xStepStize); + + meshers.emplace_back(ext::make_shared(x)); + } + + const ext::shared_ptr mesher = + ext::make_shared(meshers); + + const ext::shared_ptr payoff + = ext::dynamic_pointer_cast(arguments_.payoff); + QL_REQUIRE(payoff, "basket payoff expected"); + + const ext::shared_ptr rTS = + processes_[0]->riskFreeRate().currentLink(); + + std::vector> qTS(processes_.size()); + for (Size i=0; i < processes_.size(); ++i) + qTS[i] = processes_[i]->dividendYield().currentLink(); + + const ext::shared_ptr calculator = + ext::make_shared( + payoff, mesher, + Log(s), stdDev/sqrtT, + qTS, rTS, + Q, l + ); + + const ext::shared_ptr conditions + = FdmStepConditionComposite::vanillaComposite( + DividendSchedule(), arguments_.exercise, + mesher, calculator, + rTS->referenceDate(), rTS->dayCounter()); + + const FdmBoundaryConditionSet boundaries; + const FdmSolverDesc solverDesc + = { mesher, boundaries, conditions, calculator, + maturity, tGrid_, dampingSteps_ }; + + const bool isEuropean = + ext::dynamic_pointer_cast(arguments_.exercise) != nullptr; + const ext::shared_ptr op = + ext::make_shared( + mesher, + (isEuropean)? ext::shared_ptr() : rTS, + l); + + switch(processes_.size()) { + #define BOOST_PP_LOCAL_MACRO(n) \ + case n : \ + results_.value = ext::make_shared>( \ + solverDesc, schemeDesc_, op)->interpolateAt( \ + std::vector(processes_.size(), 0.0)); \ + break; + #define BOOST_PP_LOCAL_LIMITS (1, PDE_MAX_SUPPORTED_DIM) + #include BOOST_PP_LOCAL_ITERATE() + } + + if (isEuropean) + results_.value *= pExtractor.getInterestRateDf(maturityDate); + } +/* void FdndimBlackScholesVanillaEngine::calculate() const { #ifndef PDE_MAX_SUPPORTED_DIM #define PDE_MAX_SUPPORTED_DIM 4 @@ -116,4 +282,5 @@ namespace QuantLib { #include BOOST_PP_LOCAL_ITERATE() } } + */ } diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp index 12ed48f5aad..f8d21740677 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp @@ -25,14 +25,14 @@ #define quantlib_fd_ndim_black_scholes_vanilla_engine_hpp #include -#include #include -#include #include namespace QuantLib { - //! n-dimensional dimensional finite-differences Black Scholes vanilla option engine + class GeneralizedBlackScholesProcess; + + //! n-dimensional finite-differences Black Scholes vanilla option engine /*! \ingroup basketengines @@ -44,16 +44,24 @@ namespace QuantLib { public: FdndimBlackScholesVanillaEngine( std::vector > processes, - Matrix correlation, + Matrix rho, std::vector xGrids, Size tGrid = 50, Size dampingSteps = 0, - const FdmSchemeDesc& schemeDesc = FdmSchemeDesc::Hundsdorfer()); + const FdmSchemeDesc& schemeDesc = FdmSchemeDesc::Douglas()); + + + // Auto-scaling of grids, larges eigenvalue gets xGrid size. + FdndimBlackScholesVanillaEngine( + std::vector > processes, + Matrix rho, + Size xGrid, Size tGrid = 50, Size dampingSteps = 0, + const FdmSchemeDesc& schemeDesc = FdmSchemeDesc::Douglas()); void calculate() const override; private: const std::vector > processes_; - const Matrix correlation_; + const Matrix rho_; const std::vector xGrids_; const Size tGrid_, dampingSteps_; const FdmSchemeDesc schemeDesc_; diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index ba074d77dd9..2fdfc909688 100755 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -36,12 +36,13 @@ #include #include #include -#include +#include #include #include #include #include #include +#include #include #include #include @@ -1672,7 +1673,7 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { option.setPricingEngine( ext::make_shared( - processes, rho, std::vector(4, 15), 10 + processes, rho, std::vector(4, 50), 10 ) ); const Real expected = option.NPV(); @@ -2374,6 +2375,136 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { } } + +BOOST_AUTO_TEST_CASE(testFdmAmericanBasketOptions) { + BOOST_TEST_MESSAGE("Testing American Basket and Spread Options using FDM..."); + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(28, October, 2024); + const Date maturity = today + Period(9, Months); + + const Handle rTS + = Handle(flatRate(today, 0.1, dc)); + + const auto processGen = [&](Real spot, Rate q, Volatility vol) { + return ext::make_shared( + Handle(ext::make_shared(spot)), + Handle(flatRate(today, q, dc)), + rTS, + Handle(flatVol(today, vol, dc)) + ); + }; + + const std::vector > + processes({ + processGen(100, 0.02, 0.4), + processGen(25, 0.035, 0.5), + processGen(90, 0.08 , 0.25) + } + ); + + const Matrix rho = { + { 1.0, 0.2, 0.6 }, + { 0.2, 1.0, -0.3 }, + { 0.6, -0.3, 1.0 } + }; + + BasketOption option( + ext::make_shared( + ext::make_shared(Option::Put, -30), + Array({1, -2, -1}) + ), + ext::make_shared(today, maturity) + ); + + option.setPricingEngine( + ext::make_shared( + processes, rho, 40 + ) + ); + const Real expected = 15.1858; + + option.setPricingEngine( + ext::make_shared( + processes, rho, std::vector(processes.size(), 20), 15 + ) + ); + + const Real calculated = option.NPV(); + const Real diff = std::abs(calculated - expected); + const Real tol = 0.01; + if (diff > tol) + BOOST_FAIL("failed to reproduce american spread-basket option price" + << std::fixed << std::setprecision(8) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); +} + +BOOST_AUTO_TEST_CASE(testPrecisionAmericanBasketOptions) { + BOOST_TEST_MESSAGE("Testing high precision American Options using multi-dim FDM..."); + + const DayCounter dc = Actual365Fixed(); + const Date today = Date(28, October, 2024); + const Date maturity = today + Period(18, Months); + + const ext::shared_ptr p = + ext::make_shared( + Handle(ext::make_shared(100)), + Handle( + ext::make_shared( + std::vector({today, today + Period(1, Months), today + Period(2, Years)}), + std::vector({0.05, 0.075, 0.02}), + dc, Calendar() + ) + ), + Handle( + ext::make_shared( + std::vector({today, today + Period(3, Months), today + Period(2, Years)}), + std::vector({0.15, 0.1, 0.2}), + dc)), + Handle(flatVol(today, 0.25, dc)) + ); + + const ext::shared_ptr exercise = + ext::make_shared(today, maturity); + + const ext::shared_ptr payoff = + ext::make_shared(Option::Put, 120); + + VanillaOption vanillaOption(payoff, exercise); + vanillaOption.setPricingEngine( + ext::make_shared(p, 200, 800) + ); + + BasketOption basketOption( + ext::make_shared(payoff, Array({1})), + exercise + ); + basketOption.setPricingEngine( + ext::make_shared( + std::vector>({p}), + Matrix({{1}}), 200, 800 + ) + ); + + const Real expected = vanillaOption.NPV(); + const Real calculated = basketOption.NPV(); + const Real diff = std::abs(expected - calculated); + const Real tol = 0.02; + if (diff > tol) + BOOST_FAIL("failed to reproduce american vanilla option " + "price with multi-dim FDM engine" + << std::fixed << std::setprecision(8) + << "\n calculated: " << calculated + << "\n expected: " << expected + << "\n diff: " << diff + << "\n tolerance: " << tol); +} + + + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/nthorderderivativeop.cpp b/test-suite/nthorderderivativeop.cpp index fc529ce765f..1a5acc8af52 100644 --- a/test-suite/nthorderderivativeop.cpp +++ b/test-suite/nthorderderivativeop.cpp @@ -495,37 +495,6 @@ class AvgPayoffFct { const Real growthFactor_; }; -class MyInnerValueCalculator : public FdmInnerValueCalculator { - public: - MyInnerValueCalculator(ext::shared_ptr payoff, - ext::shared_ptr mesher, - ext::shared_ptr rTS, - ext::shared_ptr qTS, - Volatility vol, - Size direction) - : payoff_(std::move(payoff)), mesher_(std::move(mesher)), - rTS_(std::move(rTS)), qTS_(std::move(qTS)), vol_(vol), - direction_(direction) {} - - Real innerValue(const FdmLinearOpIterator& iter, Time t) override { - const Real g = mesher_->location(iter, direction_); - const Real sT = std::exp(g - 0.5*vol_*vol_*t); - - return (*payoff_)(sT); - } - - Real avgInnerValue(const FdmLinearOpIterator& iter, Time t) override { - return innerValue(iter, t); - } - - private: - const ext::shared_ptr payoff_; - const ext::shared_ptr mesher_; - const ext::shared_ptr rTS_, qTS_; - const Volatility vol_; - const Size direction_; -}; - Array priceReport(const GridSetup& setup, const Array& strikes) { const Date today(2, May, 2018); From 7418b93588b4c41a5dc88688538b6bfdeb688366 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Wed, 6 Nov 2024 19:48:21 +0100 Subject: [PATCH 25/36] removed old multi dim Black-Scholes Op --- QuantLib.vcxproj | 4 +- QuantLib.vcxproj.filters | 4 +- .../finitedifferences/operators/Makefile.am | 4 +- .../operators/fdmndimblackscholesop.cpp | 141 ------------------ .../operators/fdmndimblackscholesop.hpp | 67 --------- .../fdndimblackscholesvanillaengine.cpp | 63 -------- 6 files changed, 6 insertions(+), 277 deletions(-) delete mode 100644 ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp delete mode 100644 ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index 8cfad4faec0..0c71568ca4d 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -1200,7 +1200,7 @@ - + @@ -2356,7 +2356,7 @@ - + diff --git a/QuantLib.vcxproj.filters b/QuantLib.vcxproj.filters index 82513647d03..077eecb9809 100644 --- a/QuantLib.vcxproj.filters +++ b/QuantLib.vcxproj.filters @@ -4419,7 +4419,7 @@ math\solvers1D - + methods\finitedifferences\operators @@ -7197,7 +7197,7 @@ math\matrixutilities - + methods\finitedifferences\operators diff --git a/ql/methods/finitedifferences/operators/Makefile.am b/ql/methods/finitedifferences/operators/Makefile.am index 755b82ce572..84fee4735bb 100644 --- a/ql/methods/finitedifferences/operators/Makefile.am +++ b/ql/methods/finitedifferences/operators/Makefile.am @@ -20,10 +20,10 @@ this_include_HEADERS = \ fdmlinearopiterator.hpp \ fdmlinearoplayout.hpp \ fdmlocalvolfwdop.hpp \ - fdmndimblackscholesop.hpp \ fdmornsteinuhlenbeckop.hpp \ fdmsabrop.hpp \ fdmsquarerootfwdop.hpp \ + fdmwienerop.hpp \ firstderivativeop.hpp \ modtriplebandlinearop.hpp \ ninepointlinearop.hpp \ @@ -47,10 +47,10 @@ cpp_files = \ fdmhullwhiteop.cpp \ fdmlinearoplayout.cpp \ fdmlocalvolfwdop.cpp \ - fdmndimblackscholesop.cpp \ fdmornsteinuhlenbeckop.cpp \ fdmsabrop.cpp \ fdmsquarerootfwdop.cpp \ + fdmwienerop.hpp \ firstderivativeop.cpp \ ninepointlinearop.cpp \ nthorderderivativeop.cpp \ diff --git a/ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp b/ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp deleted file mode 100644 index d89684d3688..00000000000 --- a/ql/methods/finitedifferences/operators/fdmndimblackscholesop.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ - -/* - Copyright (C) 2024 Klaus Spanderen - - This file is part of QuantLib, a free-software/open-source library - for financial quantitative analysts and developers - http://quantlib.org/ - - QuantLib is free software: you can redistribute it and/or modify it - under the terms of the QuantLib license. You should have received a - copy of the license along with this program; if not, please email - . The license is also available online at - . - - This program is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - FOR A PARTICULAR PURPOSE. See the license for more details. -*/ - - -/*! \file fdmndimblackscholesop.cpp -*/ - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace QuantLib { - - FdmndimBlackScholesOp::FdmndimBlackScholesOp( - ext::shared_ptr mesher, - std::vector > processes, - Matrix rho, - Time maturity) - : mesher_(std::move(mesher)), - processes_(std::move(processes)), - currentForwardRate_(Null()) { - - QL_REQUIRE(!processes_.empty(), "no Black-Scholes process is given."); - QL_REQUIRE(rho.size1() == rho.size2() - && rho.size1() == processes_.size(), - "correlation matrix has the wrong size."); - - for (Size direction = 0; direction < processes_.size(); ++direction) { - const auto process = processes_[direction]; - ops_.push_back( - ext::make_shared( - mesher_, process, process->x0(), false, -Null(), direction - ) - ); - } - - for (Size i=1; i < processes_.size(); ++i) { - const auto p1 = processes_[i]; - const Volatility v1 - = p1->blackVolatility()->blackVol(maturity, p1->x0(), true); - - for (Size j=0; j < i; ++j) { - const auto p2 = processes_[j]; - const Volatility v2 - = p2->blackVolatility()->blackVol(maturity, p2->x0(), true); - - corrMaps_.emplace_back( - new NinePointLinearOp( - SecondOrderMixedDerivativeOp(i, j, mesher_) - .mult(Array(mesher_->layout()->size(), v1*v2*rho[i][j])) - ) - ); - } - } - } - - Size FdmndimBlackScholesOp::size() const { - return processes_.size(); - } - - void FdmndimBlackScholesOp::setTime(Time t1, Time t2) { - for (auto& op: ops_) - op->setTime(t1, t2); - - currentForwardRate_ = 0.0; - for (Size i=1; i < processes_.size(); ++i) - currentForwardRate_ += - processes_[i]->riskFreeRate()->forwardRate(t1, t2, Continuous).rate(); - } - - Array FdmndimBlackScholesOp::apply(const Array& x) const { - Array y = apply_mixed(x); - for (const auto& op: ops_) - y += op->apply(x); - - return y; - } - - Array FdmndimBlackScholesOp::apply_mixed(const Array& x) const { - Array y = currentForwardRate_*x; - for (const auto& m: corrMaps_) - y += m->apply(x); - - return y; - } - - Array FdmndimBlackScholesOp::apply_direction(Size direction, const Array& x) const { - return ops_[direction]->apply(x); - } - - Array FdmndimBlackScholesOp::solve_splitting( - Size direction, const Array& x, Real s) const { - - return ops_[direction]->solve_splitting(direction, x, s); - } - - Array FdmndimBlackScholesOp::preconditioner(const Array& r, Real dt) const { - return solve_splitting(0, r, dt); - } - - std::vector FdmndimBlackScholesOp::toMatrixDecomp() const { - std::vector retVal; - - for (const auto& op: ops_) - retVal.push_back(op->toMatrix()); - - SparseMatrix mixed = - currentForwardRate_*boost::numeric::ublas::identity_matrix( - mesher_->layout()->size()); - for (const auto& m: corrMaps_) - mixed += m->toMatrix(); - - retVal.push_back(mixed); - - return retVal; - } -} diff --git a/ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp b/ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp deleted file mode 100644 index f35771203f6..00000000000 --- a/ql/methods/finitedifferences/operators/fdmndimblackscholesop.hpp +++ /dev/null @@ -1,67 +0,0 @@ -/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ - -/* - Copyright (C) 2024 Klaus Spanderen - - This file is part of QuantLib, a free-software/open-source library - for financial quantitative analysts and developers - http://quantlib.org/ - - QuantLib is free software: you can redistribute it and/or modify it - under the terms of the QuantLib license. You should have received a - copy of the license along with this program; if not, please email - . The license is also available online at - . - - This program is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - FOR A PARTICULAR PURPOSE. See the license for more details. -*/ - - -/*! \file fdmndimblackscholesop.hpp -*/ - -#ifndef quantlib_fdm_ndim_black_scholes_op_hpp -#define quantlib_fdm_ndim_black_scholes_op_hpp - -#include -#include -#include - -namespace QuantLib { - - class FdmMesher; - class FdmBlackScholesOp; - class LocalVolTermStructure; - class GeneralizedBlackScholesProcess; - - class FdmndimBlackScholesOp : public FdmLinearOpComposite { - public: - FdmndimBlackScholesOp( - ext::shared_ptr mesher, - std::vector > processes, - Matrix correlation, - Time maturity); - - Size size() const override; - void setTime(Time t1, Time t2) override; - Array apply(const Array& x) const override; - Array apply_mixed(const Array& x) const override; - - Array apply_direction(Size direction, const Array& x) const override; - - Array solve_splitting(Size direction, const Array& x, Real s) const override; - Array preconditioner(const Array& r, Real s) const override; - - std::vector toMatrixDecomp() const override; - - private: - const ext::shared_ptr mesher_; - const std::vector > processes_; - - Real currentForwardRate_; - std::vector > ops_; - std::vector > corrMaps_; - }; -} -#endif diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp index d1a14336857..9bbe612991e 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -220,67 +220,4 @@ namespace QuantLib { if (isEuropean) results_.value *= pExtractor.getInterestRateDf(maturityDate); } -/* - void FdndimBlackScholesVanillaEngine::calculate() const { - #ifndef PDE_MAX_SUPPORTED_DIM - #define PDE_MAX_SUPPORTED_DIM 4 - #endif - QL_REQUIRE(processes_.size() <= PDE_MAX_SUPPORTED_DIM, - "This engine does not support " << processes_.size() << " underlyings. " - << "Max number of underlyings is " << PDE_MAX_SUPPORTED_DIM << ". " - << "Please change preprocessor constant PDE_MAX_SUPPORTED_DIM and recompile " - << "if a larger number of underlyings is needed."); - - const Time maturity = processes_[0]->time(arguments_.exercise->lastDate()); - - std::vector > meshers; - for (Size i=0; i < processes_.size(); ++i) { - const auto process = processes_[i]; - - meshers.push_back( - ext::make_shared( - xGrids_[i], process, maturity, process->x0(), - Null(), Null(), 0.0001, 1.5, - std::pair(process->x0(), 0.1) - ) - ); - } - const auto mesher = ext::make_shared(meshers); - - const auto payoff - = ext::dynamic_pointer_cast(arguments_.payoff); - const auto calculator - = ext::make_shared(payoff, mesher); - - const auto conditions - = FdmStepConditionComposite::vanillaComposite( - DividendSchedule(), arguments_.exercise, - mesher, calculator, - processes_[0]->riskFreeRate()->referenceDate(), - processes_[0]->riskFreeRate()->dayCounter()); - - const FdmBoundaryConditionSet boundaries; - const FdmSolverDesc solverDesc - = { mesher, boundaries, conditions, calculator, - maturity, tGrid_, dampingSteps_ }; - - const auto op = ext::make_shared( - mesher, processes_, correlation_, maturity - ); - - std::vector logX; - for (const auto& p: processes_) - logX.push_back(std::log(p->x0())); - - switch(processes_.size()) { - #define BOOST_PP_LOCAL_MACRO(n) \ - case n : \ - results_.value = ext::make_shared>( \ - solverDesc, schemeDesc_, op)->interpolateAt(logX); \ - break; - #define BOOST_PP_LOCAL_LIMITS (1, PDE_MAX_SUPPORTED_DIM) - #include BOOST_PP_LOCAL_ITERATE() - } - } - */ } From 095f8620d84b544a3d4f0db1bb52041b3bb2f196 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Wed, 6 Nov 2024 20:21:35 +0100 Subject: [PATCH 26/36] fixed test cases --- ql/CMakeLists.txt | 2 -- ql/pricingengines/basket/choibasketengine.cpp | 1 - test-suite/basketoption.cpp | 31 +++++++++++++------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index f69397222f0..3e1dfb3f909 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -447,7 +447,6 @@ set(QL_SOURCES methods/finitedifferences/meshers/fdmsimpleprocess1dmesher.cpp methods/finitedifferences/meshers/uniformgridmesher.cpp methods/finitedifferences/operators/fdm2dblackscholesop.cpp - methods/finitedifferences/operators/fdmndimblackscholesop.cpp methods/finitedifferences/operators/fdmbatesop.cpp methods/finitedifferences/operators/fdmblackscholesfwdop.cpp methods/finitedifferences/operators/fdmblackscholesop.cpp @@ -1590,7 +1589,6 @@ set(QL_HEADERS methods/finitedifferences/meshers/uniformgridmesher.hpp methods/finitedifferences/mixedscheme.hpp methods/finitedifferences/operators/fdm2dblackscholesop.hpp - methods/finitedifferences/operators/fdmndimblackscholesop.hpp methods/finitedifferences/operators/fdmbatesop.hpp methods/finitedifferences/operators/fdmblackscholesfwdop.hpp methods/finitedifferences/operators/fdmblackscholesop.hpp diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index 92c98b5aaae..709dd9b7335 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -34,7 +34,6 @@ #include #include -#include namespace QuantLib { diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 2fdfc909688..d9fa4ca1c15 100755 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1498,16 +1498,16 @@ BOOST_AUTO_TEST_CASE(testNdimPDEvs2dimPDE) { const Real rho = 0.75; const ext::shared_ptr twoDimEngine - = ext::make_shared(p1, p2, rho, 25, 25, 15); + = ext::make_shared(p1, p2, rho, 25, 25, 100); const ext::shared_ptr nDimEngine = ext::make_shared( std::vector >({p1, p2}), Matrix({{1, rho}, {rho, 1}}), - std::vector({25, 25}), 15 + std::vector({25, 25}), 100 ); - const Real tol = 1e-4; + const Real tol = 0.2; for (const auto& exercise: std::vector >( { ext::make_shared(maturity), ext::make_shared(today, maturity)})) { @@ -1596,7 +1596,7 @@ BOOST_AUTO_TEST_CASE(testNdimPDEinDifferentDims) { Matrix rho(d, d); for (Size i=0; i < d; ++i) for (Size j=0; j < d; ++j) - rho(i, j) = std::exp(-0.5*std::abs(Real(i-j))); + rho(i, j) = rho(j, i) = std::exp(-0.5*std::abs(Real(i-j))); BasketOption option( ext::make_shared( @@ -1606,21 +1606,32 @@ BOOST_AUTO_TEST_CASE(testNdimPDEinDifferentDims) { exercise ); + if (d > 1) + option.setPricingEngine( + ext::make_shared(processes, rho, 15) + ); + else + option.setPricingEngine( + ext::make_shared(processes) + ); + + const Real expected = option.NPV(); + option.setPricingEngine( - ext::make_shared( - processes, rho, std::vector(d, 20), 7 - ) + ext::make_shared(processes, rho, 30, 7) ); const Real calculated = option.NPV(); - const Real diff = std::abs(calculated - expected[d-1]); - const Real tol = 0.047; + const Real diff = std::abs(calculated - expected); + const Real tol = 0.05; + + std::cout << d << " " << diff << std::endl; if (diff > tol) { BOOST_FAIL("failed to reproduce precalculated " << d << "-dim option price" << std::fixed << std::setprecision(5) << "\n calculated: " << calculated - << "\n expected: " << expected[d-1] + << "\n expected: " << expected << "\n diff: " << diff << "\n tolerance : " << tol); } From 3c0b6e2b81eb9460201f5ff06f9836d2bdcb236d Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Wed, 6 Nov 2024 20:22:02 +0100 Subject: [PATCH 27/36] remove print statement --- test-suite/basketoption.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index d9fa4ca1c15..88aa38edcff 100755 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1625,8 +1625,6 @@ BOOST_AUTO_TEST_CASE(testNdimPDEinDifferentDims) { const Real diff = std::abs(calculated - expected); const Real tol = 0.05; - std::cout << d << " " << diff << std::endl; - if (diff > tol) { BOOST_FAIL("failed to reproduce precalculated " << d << "-dim option price" << std::fixed << std::setprecision(5) From 736809e3ba1c129cefa43c0dda0c60aca658b4da Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Wed, 6 Nov 2024 20:57:41 +0100 Subject: [PATCH 28/36] fixed compile bug --- ql/CMakeLists.txt | 2 +- ql/methods/finitedifferences/operators/all.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 3e1dfb3f909..a8df0bb01e8 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -1607,7 +1607,7 @@ set(QL_HEADERS methods/finitedifferences/operators/fdmornsteinuhlenbeckop.hpp methods/finitedifferences/operators/fdmsabrop.hpp methods/finitedifferences/operators/fdmsquarerootfwdop.hpp - methods/finitedifferences/operators/fdmwienerop.cpp + methods/finitedifferences/operators/fdmwienerop.hpp methods/finitedifferences/operators/firstderivativeop.hpp methods/finitedifferences/operators/modtriplebandlinearop.hpp methods/finitedifferences/operators/ninepointlinearop.hpp diff --git a/ql/methods/finitedifferences/operators/all.hpp b/ql/methods/finitedifferences/operators/all.hpp index 21642a5f34a..b7553243e28 100644 --- a/ql/methods/finitedifferences/operators/all.hpp +++ b/ql/methods/finitedifferences/operators/all.hpp @@ -17,10 +17,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include From 932ed9e4215b78cd7445c12aff156879dc84b7c7 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Wed, 6 Nov 2024 21:01:38 +0100 Subject: [PATCH 29/36] fixed compile bug --- ql/methods/finitedifferences/operators/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ql/methods/finitedifferences/operators/Makefile.am b/ql/methods/finitedifferences/operators/Makefile.am index 84fee4735bb..19d55d09dfd 100644 --- a/ql/methods/finitedifferences/operators/Makefile.am +++ b/ql/methods/finitedifferences/operators/Makefile.am @@ -50,7 +50,7 @@ cpp_files = \ fdmornsteinuhlenbeckop.cpp \ fdmsabrop.cpp \ fdmsquarerootfwdop.cpp \ - fdmwienerop.hpp \ + fdmwienerop.cpp \ firstderivativeop.cpp \ ninepointlinearop.cpp \ nthorderderivativeop.cpp \ From fd3850138bfc6ab7ac15706dad7f1c7d5297e1ed Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sat, 9 Nov 2024 13:35:22 +0100 Subject: [PATCH 30/36] fixed test suite runtime --- ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp | 2 +- test-suite/basketoption.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp index 9bbe612991e..fa254c03a81 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.cpp @@ -152,7 +152,7 @@ namespace QuantLib { for (Size i=0; i < processes_.size(); ++i) { const Size xGrid = (xGrids_.size() > 1) ? xGrids_[i] - : std::max(Size(4), Size(xGrids_[0]*std::sqrt(l[i]/l[0]))); + : std::max(Size(4), Size(xGrids_[0]*std::pow(l[i]/l[0], 0.1))); QL_REQUIRE(xGrid >= 4, "minimum grid size is four"); const Real xStepStize = (1.0-2*eps)/(xGrid-1); diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 88aa38edcff..488deb13e18 100755 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1682,7 +1682,7 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouVsPDE) { option.setPricingEngine( ext::make_shared( - processes, rho, std::vector(4, 50), 10 + processes, rho, 20, 10 ) ); const Real expected = option.NPV(); From 313700ac4cb0c8c86924c7be46d077576d6f5634 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sat, 9 Nov 2024 17:15:48 +0100 Subject: [PATCH 31/36] enable examples --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 22133f1d7fd..69e32a2d092 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ set(QL_INSTALL_CMAKEDIR "lib/cmake/${PACKAGE_NAME}" CACHE STRING "Installation directory for CMake scripts") # Options -option(QL_BUILD_EXAMPLES "Build examples" OFF) +option(QL_BUILD_EXAMPLES "Build examples" ON) option(QL_BUILD_TEST_SUITE "Build test suite" ON) option(QL_BUILD_FUZZ_TEST_SUITE "Build fuzz test suite" OFF) option(QL_ENABLE_OPENMP "Detect and use OpenMP" OFF) From 22f4e821e8ebdb5bb0330c413ff2817aaf8ff9c1 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sat, 9 Nov 2024 19:10:04 +0100 Subject: [PATCH 32/36] fixed typos --- .../operators/fdmwienerop.cpp | 2 +- .../operators/fdmwienerop.hpp | 1 - ql/pricingengines/basket/choibasketengine.hpp | 5 +- .../basket/denglizhoubasketengine.cpp | 3 +- .../fdndimblackscholesvanillaengine.hpp | 2 +- .../basket/operatorsplittingspreadengine.cpp | 2 +- .../basket/operatorsplittingspreadengine.hpp | 2 +- .../basket/singlefactorbsmbasketengine.cpp | 1 - .../basket/singlefactorbsmbasketengine.hpp | 32 +++---- test-suite/basketoption.cpp | 84 +++++++++---------- test-suite/matrices.cpp | 66 +++++++-------- 11 files changed, 98 insertions(+), 102 deletions(-) diff --git a/ql/methods/finitedifferences/operators/fdmwienerop.cpp b/ql/methods/finitedifferences/operators/fdmwienerop.cpp index 742e67b6dd1..59cd4e88c96 100644 --- a/ql/methods/finitedifferences/operators/fdmwienerop.cpp +++ b/ql/methods/finitedifferences/operators/fdmwienerop.cpp @@ -18,7 +18,7 @@ */ -/*! \file fdmndimwienerop.cpp +/*! \file fdmwienerop.cpp */ #include diff --git a/ql/methods/finitedifferences/operators/fdmwienerop.hpp b/ql/methods/finitedifferences/operators/fdmwienerop.hpp index 2cdc7f65b3f..85d7dd58bcb 100644 --- a/ql/methods/finitedifferences/operators/fdmwienerop.hpp +++ b/ql/methods/finitedifferences/operators/fdmwienerop.hpp @@ -55,7 +55,6 @@ namespace QuantLib { const ext::shared_ptr rTS_; std::vector > ops_; Rate r_; - }; } #endif diff --git a/ql/pricingengines/basket/choibasketengine.hpp b/ql/pricingengines/basket/choibasketengine.hpp index 799efc0f68a..9e263ad0092 100644 --- a/ql/pricingengines/basket/choibasketengine.hpp +++ b/ql/pricingengines/basket/choibasketengine.hpp @@ -35,7 +35,7 @@ namespace QuantLib { Spread, Basket and Asian Options", Jaehyuk Choi, 2018 https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2913048 - Python implementation from the author of the paper is also available + A Python implementation from the author of the paper is also available https://github.com/PyFE/PyFENG \ingroup basketengines @@ -45,8 +45,7 @@ namespace QuantLib { */ class ChoiBasketEngine : public BasketOption::engine { public: - // lambda controls the precision, - // fast: 4, accurate: 8, high precision: 20 + // lambda controls the integration order and the precision of the result. ChoiBasketEngine( std::vector > processes, Matrix rho, diff --git a/ql/pricingengines/basket/denglizhoubasketengine.cpp b/ql/pricingengines/basket/denglizhoubasketengine.cpp index bb453d5688d..bfcc7be699f 100644 --- a/ql/pricingengines/basket/denglizhoubasketengine.cpp +++ b/ql/pricingengines/basket/denglizhoubasketengine.cpp @@ -17,7 +17,6 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ -#include "denglizhoubasketengine.hpp" #include #include @@ -25,6 +24,7 @@ #include #include #include +#include namespace QuantLib { @@ -62,7 +62,6 @@ namespace QuantLib { QL_REQUIRE(avgPayoff, "average or spread basket payoff expected"); - // sort assets by their weight const Array weights = avgPayoff->weights(); QL_REQUIRE(n_ == weights.size() && n_ > 1, "wrong number of weights arguments in payoff"); diff --git a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp index f8d21740677..a7378f4337d 100644 --- a/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp +++ b/ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp @@ -50,7 +50,7 @@ namespace QuantLib { const FdmSchemeDesc& schemeDesc = FdmSchemeDesc::Douglas()); - // Auto-scaling of grids, larges eigenvalue gets xGrid size. + // Auto-scaling of grids, largest eigenvalue gets xGrid size. FdndimBlackScholesVanillaEngine( std::vector > processes, Matrix rho, diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp index 149cbc8d8a5..d8f876bb25c 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.cpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.cpp @@ -68,7 +68,7 @@ namespace QuantLib { QL_REQUIRE(order_ == Second, "unknown approximation type"); /* - In the original paper the second order was calculated using numerical differentiation. + In the original paper, the second-order approximation was computed using numerical differentiation. The following Mathematica scripts calculates the approximation to the n'th order. vol2Hat[R2_] := vol2*(R2 - K)/R2 diff --git a/ql/pricingengines/basket/operatorsplittingspreadengine.hpp b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp index 5b5e824d712..e5132ab3b2a 100644 --- a/ql/pricingengines/basket/operatorsplittingspreadengine.hpp +++ b/ql/pricingengines/basket/operatorsplittingspreadengine.hpp @@ -28,7 +28,7 @@ namespace QuantLib { - //! Pricing engine for spread option on two futures + //! Pricing engine for spread options with two assets /*! Chi-Fai Lo, Pricing Spread Options by the Operator Splitting Method, https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2429696 diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp index 9ea29061e8a..5d430ea60a8 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -140,7 +140,6 @@ namespace QuantLib { QL_REQUIRE(payoff, "non-plain vanilla payoff given"); const Real strike = payoff->strike(); - // sort assets by their weight const Array weights = avgPayoff->weights(); QL_REQUIRE(n_ == weights.size(), "wrong number of weights arguments in payoff"); diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index 57714582558..f960f189a1d 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -39,27 +39,27 @@ namespace QuantLib { \ingroup basketengines */ - class SumExponentialsRootSolver { - public: - enum Strategy {Ridder, Newton, Brent, Halley}; + class SumExponentialsRootSolver { + public: + enum Strategy {Ridder, Newton, Brent, Halley}; - SumExponentialsRootSolver(Array a, Array sig, Real K); + SumExponentialsRootSolver(Array a, Array sig, Real K); - Real operator()(Real x) const; - Real derivative(Real x) const; - Real secondDerivative(Real x) const; + Real operator()(Real x) const; + Real derivative(Real x) const; + Real secondDerivative(Real x) const; - Real getRoot(Real xTol = 1e6*QL_EPSILON, Strategy strategy = Brent) const; + Real getRoot(Real xTol = 1e6*QL_EPSILON, Strategy strategy = Brent) const; - Size getFCtr() const; - Size getDerivativeCtr() const; - Size getSecondDerivativeCtr() const; + Size getFCtr() const; + Size getDerivativeCtr() const; + Size getSecondDerivativeCtr() const; - private: - const Array a_, sig_; - const Real K_; - mutable Size fCtr_, fPrimeCtr_, fDoublePrimeCtr_; - }; + private: + const Array a_, sig_; + const Real K_; + mutable Size fCtr_, fPrimeCtr_, fDoublePrimeCtr_; + }; class SingleFactorBsmBasketEngine : public BasketOption::engine { public: diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 488deb13e18..ef49b2284c3 100755 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1772,51 +1772,51 @@ BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { MersenneTwisterUniformRng mt(42); for (auto strategy: { - std::make_tuple("Brent", SumExponentialsRootSolver::Brent), - std::make_tuple("Newton", SumExponentialsRootSolver::Newton), - std::make_tuple("Ridder", SumExponentialsRootSolver::Ridder), - std::make_tuple("Halley", SumExponentialsRootSolver::Halley) - }) { - - Size fCtr = 0; - const Size n = 10000; - const Real tol = 1e8*QL_EPSILON; - const Real acc = 1e-4*tol; - IncrementalStatistics stats; - - for (Size i=0; i < n; ++i) { - const Size n = (mt.nextInt32() % 10)+1; - Array a(n), sig(n); - const Real offset = (mt.nextReal() < 0.3)? -1.0 : 0.0; - for (Size j=0; j < n; ++j) { - a[j] = mt.nextReal() + offset; - sig[j] = copysign(1.0, a[j])*mt.nextReal(); - } - const Real kMin = SumExponentialsRootSolver(a, sig, 0.0)(-10.0); - const Real kMax = SumExponentialsRootSolver(a, sig, 0.0)( 10.0); - const Real K = (kMax - kMin)*mt.nextReal() + kMin; - - const Real xValue = SumExponentialsRootSolver(a, sig, K) - .getRoot(acc, SumExponentialsRootSolver::Brent); + std::make_tuple("Brent", SumExponentialsRootSolver::Brent), + std::make_tuple("Newton", SumExponentialsRootSolver::Newton), + std::make_tuple("Ridder", SumExponentialsRootSolver::Ridder), + std::make_tuple("Halley", SumExponentialsRootSolver::Halley) + }) { + + Size fCtr = 0; + const Size n = 10000; + const Real tol = 1e8*QL_EPSILON; + const Real acc = 1e-4*tol; + IncrementalStatistics stats; + + for (Size i=0; i < n; ++i) { + const Size n = (mt.nextInt32() % 10)+1; + Array a(n), sig(n); + const Real offset = (mt.nextReal() < 0.3)? -1.0 : 0.0; + for (Size j=0; j < n; ++j) { + a[j] = mt.nextReal() + offset; + sig[j] = copysign(1.0, a[j])*mt.nextReal(); + } + const Real kMin = SumExponentialsRootSolver(a, sig, 0.0)(-10.0); + const Real kMax = SumExponentialsRootSolver(a, sig, 0.0)( 10.0); + const Real K = (kMax - kMin)*mt.nextReal() + kMin; + + const Real xValue = SumExponentialsRootSolver(a, sig, K) + .getRoot(acc, SumExponentialsRootSolver::Brent); const SumExponentialsRootSolver solver(a, sig, K); - const Real xRoot = solver.getRoot(tol, std::get<1>(strategy)); + const Real xRoot = solver.getRoot(tol, std::get<1>(strategy)); - stats.add(xValue - xRoot); - fCtr += solver.getFCtr() + solver.getDerivativeCtr() + solver.getSecondDerivativeCtr(); - } + stats.add(xValue - xRoot); + fCtr += solver.getFCtr() + solver.getDerivativeCtr() + solver.getSecondDerivativeCtr(); + } - if (fCtr > 15*n) { + if (fCtr > 15*n) { BOOST_FAIL("too many function calls needed for solver " << std::get<0>(strategy)); - } + } - if (stats.standardDeviation() > 10*tol) { + if (stats.standardDeviation() > 10*tol) { BOOST_FAIL("failed to find root of sum of exponentials" << "\n solver : " << std::get<0>(strategy) << std::fixed << std::setprecision(15) << "\n stdev : " << stats.standardDeviation() << "\n tolerance: " << tol); - } + } } } @@ -1949,11 +1949,11 @@ BOOST_AUTO_TEST_CASE(testGoldenChoiBasketEngineExample) { const ext::shared_ptr& spot, Rate q, Volatility vol) -> ext::shared_ptr { return ext::make_shared( - Handle(spot), - Handle(flatRate(today, q, dc)), - rTS, - Handle(flatVol(today, vol, dc)) - ); + Handle(spot), + Handle(flatRate(today, q, dc)), + rTS, + Handle(flatVol(today, vol, dc)) + ); }; const std::vector > @@ -2189,7 +2189,7 @@ BOOST_AUTO_TEST_CASE(testSpreadAndBasketBenchmarks) { {11.5795246248372834, 8.11486124233140238, 5.36890684802773066, 3.35146299782513601, 1.97711593318812251} }, - // unknown + // new { {80, 120, 100, 100}, {0.3, 0.4, 0.2, 0.35}, {0.01, 0.03, 0.07, 0.04}, 0.03, {{{1.0, 0.5, 0.35, 0.35}, @@ -2451,8 +2451,8 @@ BOOST_AUTO_TEST_CASE(testFdmAmericanBasketOptions) { << "\n tolerance: " << tol); } -BOOST_AUTO_TEST_CASE(testPrecisionAmericanBasketOptions) { - BOOST_TEST_MESSAGE("Testing high precision American Options using multi-dim FDM..."); +BOOST_AUTO_TEST_CASE(testAccurateAmericanBasketOptions) { + BOOST_TEST_MESSAGE("Testing high precision American Options Pricing using multi-dim FDM..."); const DayCounter dc = Actual365Fixed(); const Date today = Date(28, October, 2024); diff --git a/test-suite/matrices.cpp b/test-suite/matrices.cpp index 76e45eab6e9..6e42e4accdf 100644 --- a/test-suite/matrices.cpp +++ b/test-suite/matrices.cpp @@ -913,13 +913,13 @@ BOOST_AUTO_TEST_CASE(testCholeskySolverForIncomplete) { } namespace { - void QL_CHECK_CLOSE_ARRAY_TOL( - const Array& actual, const Array& expected, Real tol) { - BOOST_REQUIRE(actual.size() == expected.size()); - for (auto i = 0u; i < actual.size(); i++) { - BOOST_CHECK_SMALL(actual[i] - expected[i], tol); - } - } + void QL_CHECK_CLOSE_ARRAY_TOL( + const Array& actual, const Array& expected, Real tol) { + BOOST_REQUIRE(actual.size() == expected.size()); + for (auto i = 0u; i < actual.size(); i++) { + BOOST_CHECK_SMALL(actual[i] - expected[i], tol); + } + } } BOOST_AUTO_TEST_CASE(testHouseholderTransformation) { @@ -928,23 +928,23 @@ BOOST_AUTO_TEST_CASE(testHouseholderTransformation) { MersenneTwisterUniformRng rng(1234); const auto I = [](Size i) -> Matrix { - Matrix id(i, i, 0.0); - for (Size j=0; j < i; ++j) - id[j][j] = 1.0; + Matrix id(i, i, 0.0); + for (Size j=0; j < i; ++j) + id[j][j] = 1.0; - return id; + return id; }; for (Size i=1; i < 10; ++i) { - Array v(i), x(i); - for (Size j=0; j < i; ++j) { - v[j] = rng.nextReal()-0.5; - x[j] = rng.nextReal()-0.5; - } - - const Array expected = (I(i)- 2.0*outerProduct(v, v))*x; - const Array calculated = HouseholderTransformation(v)(x); - QL_CHECK_CLOSE_ARRAY_TOL(calculated, expected, 1e4*QL_EPSILON); + Array v(i), x(i); + for (Size j=0; j < i; ++j) { + v[j] = rng.nextReal()-0.5; + x[j] = rng.nextReal()-0.5; + } + + const Array expected = (I(i)- 2.0*outerProduct(v, v))*x; + const Array calculated = HouseholderTransformation(v)(x); + QL_CHECK_CLOSE_ARRAY_TOL(calculated, expected, 1e4*QL_EPSILON); } } @@ -954,24 +954,24 @@ BOOST_AUTO_TEST_CASE(testHouseholderReflection) { const Real tol=1e4*QL_EPSILON; const auto e = [](Size n, Size m=0) -> Array { - Array e(n, 0.0); - e[m] = 1.0; - return e; + Array e(n, 0.0); + e[m] = 1.0; + return e; }; for (Size i=0; i < 5; ++i) { - QL_CHECK_CLOSE_ARRAY_TOL( - HouseholderReflection(e(5))(e(5, i)), e(5), tol); - QL_CHECK_CLOSE_ARRAY_TOL( - HouseholderReflection(e(5))(M_PI*e(5, i)), M_PI*e(5), tol); - QL_CHECK_CLOSE_ARRAY_TOL( - HouseholderReflection(e(5))( - e(5, i) + e(5)), - ((i==0)? 2.0 : M_SQRT2)*e(5), tol); + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(5))(e(5, i)), e(5), tol); + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(5))(M_PI*e(5, i)), M_PI*e(5), tol); + QL_CHECK_CLOSE_ARRAY_TOL( + HouseholderReflection(e(5))( + e(5, i) + e(5)), + ((i==0)? 2.0 : M_SQRT2)*e(5), tol); } // limits - for (Real x=10; x > 1e-50; x*=0.1) { + for (Real x=10; x > 1e-50; x*=0.1) { QL_CHECK_CLOSE_ARRAY_TOL( HouseholderReflection(e(3))( Array({10.0, x, 0})), @@ -983,7 +983,7 @@ BOOST_AUTO_TEST_CASE(testHouseholderReflection) { Array({10.0, x, 1e-3})), std::sqrt(10.0*10.0+x*x+1e-3*1e-3)*e(3), tol ); - } + } MersenneTwisterUniformRng rng(1234); From 90cf6714b7f3a39b0c750454e34240791fa0c4dc Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sat, 9 Nov 2024 19:26:46 +0100 Subject: [PATCH 33/36] fixed missing explicit --- ql/math/matrixutilities/householder.hpp | 4 ++-- ql/pricingengines/basket/singlefactorbsmbasketengine.hpp | 2 +- ql/pricingengines/basket/vectorbsmprocessextractor.hpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ql/math/matrixutilities/householder.hpp b/ql/math/matrixutilities/householder.hpp index d98c93fb468..020ab14dd91 100644 --- a/ql/math/matrixutilities/householder.hpp +++ b/ql/math/matrixutilities/householder.hpp @@ -34,7 +34,7 @@ namespace QuantLib { class HouseholderTransformation { public: - HouseholderTransformation(const Array v); + explicit HouseholderTransformation(const Array v); Matrix getMatrix() const; Array operator()(const Array& x) const; @@ -46,7 +46,7 @@ namespace QuantLib { class HouseholderReflection { public: - HouseholderReflection(const Array e); + explicit HouseholderReflection(const Array e); Array operator()(const Array& a) const; Array reflectionVector(const Array& a) const; diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index f960f189a1d..74bdc7b818a 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -63,7 +63,7 @@ namespace QuantLib { class SingleFactorBsmBasketEngine : public BasketOption::engine { public: - SingleFactorBsmBasketEngine( + explicit SingleFactorBsmBasketEngine( std::vector > p, Real xTol = 1e4*QL_EPSILON); diff --git a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp index a640f7a7ae1..649f3cd57e0 100644 --- a/ql/pricingengines/basket/vectorbsmprocessextractor.hpp +++ b/ql/pricingengines/basket/vectorbsmprocessextractor.hpp @@ -31,7 +31,7 @@ namespace QuantLib { namespace detail { class VectorBsmProcessExtractor { public: - VectorBsmProcessExtractor( + explicit VectorBsmProcessExtractor( std::vector > p); Array getSpot() const; From 8c1e9ac074a1b49b7b76f9167a5493f942ea8a63 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sun, 10 Nov 2024 14:11:50 +0100 Subject: [PATCH 34/36] fixed typo --- ql/math/integrals/gaussianquadratures.cpp | 4 ++-- ql/math/integrals/gaussianquadratures.hpp | 4 ++-- ql/pricingengines/basket/choibasketengine.cpp | 2 +- test-suite/gaussianquadratures.cpp | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ql/math/integrals/gaussianquadratures.cpp b/ql/math/integrals/gaussianquadratures.cpp index 884bc7d3b8e..d71f49a0b14 100644 --- a/ql/math/integrals/gaussianquadratures.cpp +++ b/ql/math/integrals/gaussianquadratures.cpp @@ -61,7 +61,7 @@ namespace QuantLib { } - MulitDimGaussianIntegration::MulitDimGaussianIntegration( + MultiDimGaussianIntegration::MultiDimGaussianIntegration( const std::vector& ns, const std::function(Size)>& genQuad) : weights_(std::accumulate(ns.begin(), ns.end(), Size(1), std::multiplies<>()), 1.0), @@ -93,7 +93,7 @@ namespace QuantLib { } } - Real MulitDimGaussianIntegration::operator()( + Real MultiDimGaussianIntegration::operator()( const std::function& f) const { Real s = 0.0; const Size n = x_.size(); diff --git a/ql/math/integrals/gaussianquadratures.hpp b/ql/math/integrals/gaussianquadratures.hpp index 5c6f7d98498..79bbde50b4b 100644 --- a/ql/math/integrals/gaussianquadratures.hpp +++ b/ql/math/integrals/gaussianquadratures.hpp @@ -76,9 +76,9 @@ namespace QuantLib { Array x_, w_; }; - class MulitDimGaussianIntegration { + class MultiDimGaussianIntegration { public: - MulitDimGaussianIntegration( + MultiDimGaussianIntegration( const std::vector& ns, const std::function(Size)>& genQuad); diff --git a/ql/pricingengines/basket/choibasketengine.cpp b/ql/pricingengines/basket/choibasketengine.cpp index 709dd9b7335..84e8d248699 100644 --- a/ql/pricingengines/basket/choibasketengine.cpp +++ b/ql/pricingengines/basket/choibasketengine.cpp @@ -194,7 +194,7 @@ namespace QuantLib { [](Real acc, Real x) -> Real { return acc + x*x; } ); - MulitDimGaussianIntegration ghq( + MultiDimGaussianIntegration ghq( nIntOrder, [](const Size n) { return ext::make_shared(n); } ); diff --git a/test-suite/gaussianquadratures.cpp b/test-suite/gaussianquadratures.cpp index f0b4361ded8..714b150d2b1 100644 --- a/test-suite/gaussianquadratures.cpp +++ b/test-suite/gaussianquadratures.cpp @@ -336,7 +336,7 @@ BOOST_AUTO_TEST_CASE(testMultiDimensionalGaussIntegration) { std::vector ns(n); std::iota(ns.begin(), ns.end(), Size(1)); - MulitDimGaussianIntegration quad( + MultiDimGaussianIntegration quad( ns, [](const Size n) { return ext::make_shared(n); @@ -372,7 +372,7 @@ BOOST_AUTO_TEST_CASE(testMultiDimensionalGaussIntegration) { const Matrix invA = inverse(A); const Real det_2piA = std::sqrt(determinant(M_TWOPI*invA)); - const MulitDimGaussianIntegration quad( + const MultiDimGaussianIntegration quad( std::vector(ns.begin(), ns.begin()+n), [](const Size n) { return ext::make_shared(n); } ); @@ -404,11 +404,11 @@ BOOST_AUTO_TEST_CASE(testMultiDimensionalGaussIntegration) { const Matrix invA = inverse(A); const Real sqrt_det_2piA = std::sqrt(determinant(M_TWOPI*invA)); - const MulitDimGaussianIntegration quadHigh( + const MultiDimGaussianIntegration quadHigh( std::vector({22, 18, 26}), [](const Size n) { return ext::make_shared(n); } ); - const MulitDimGaussianIntegration quad2( + const MultiDimGaussianIntegration quad2( std::vector(3, 2), [](const Size n) { return ext::make_shared(n); } ); From efb1c78cb845764327dc78eaced2f423972421b4 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Sun, 10 Nov 2024 14:12:11 +0100 Subject: [PATCH 35/36] fixed typo --- ql/instruments/basketoption.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ql/instruments/basketoption.hpp b/ql/instruments/basketoption.hpp index fd0ee4bd127..3e8c4837654 100644 --- a/ql/instruments/basketoption.hpp +++ b/ql/instruments/basketoption.hpp @@ -80,7 +80,7 @@ namespace QuantLib { a.begin(), Real(0.0)); } - Array weights() const { return weights_; } + const Array& weights() const { return weights_; } private: Array weights_; From 45292675e3f5a419da60216191e00e38203e21c9 Mon Sep 17 00:00:00 2001 From: klaus spanderen Date: Mon, 11 Nov 2024 20:21:47 +0100 Subject: [PATCH 36/36] moved SumExponentialsRootSolver to detail namespace removed executable bit --- .../basket/singlefactorbsmbasketengine.cpp | 157 +++++++++--------- .../basket/singlefactorbsmbasketengine.hpp | 46 ++--- test-suite/basketoption.cpp | 22 +-- 3 files changed, 114 insertions(+), 111 deletions(-) mode change 100755 => 100644 test-suite/basketoption.cpp diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp index 5d430ea60a8..ebc10550a5c 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.cpp @@ -32,95 +32,96 @@ namespace QuantLib { - SumExponentialsRootSolver::SumExponentialsRootSolver( - Array a, Array sig, Real K) - : a_(std::move(a)), sig_(std::move(sig)), K_(K), - fCtr_(0), fPrimeCtr_(0), fDoublePrimeCtr_(0) { - QL_REQUIRE(a_.size() == sig_.size(), - "Arrays must have the same size"); - } + namespace detail { + SumExponentialsRootSolver::SumExponentialsRootSolver( + Array a, Array sig, Real K) + : a_(std::move(a)), sig_(std::move(sig)), K_(K), + fCtr_(0), fPrimeCtr_(0), fDoublePrimeCtr_(0) { + QL_REQUIRE(a_.size() == sig_.size(), + "Arrays must have the same size"); + } - Real SumExponentialsRootSolver::operator()(Real x) const { - ++fCtr_; + Real SumExponentialsRootSolver::operator()(Real x) const { + ++fCtr_; - Real s = 0.0; - for (Size i=0; i < a_.size(); ++i) - s += a_[i]*std::exp(sig_[i]*x); - return s - K_; - } + Real s = 0.0; + for (Size i=0; i < a_.size(); ++i) + s += a_[i]*std::exp(sig_[i]*x); + return s - K_; + } - Real SumExponentialsRootSolver::derivative(Real x) const { - ++fPrimeCtr_; + Real SumExponentialsRootSolver::derivative(Real x) const { + ++fPrimeCtr_; - Real s = 0.0; - for (Size i=0; i < a_.size(); ++i) - s += a_[i]*sig_[i]*std::exp(sig_[i]*x); - return s; - } + Real s = 0.0; + for (Size i=0; i < a_.size(); ++i) + s += a_[i]*sig_[i]*std::exp(sig_[i]*x); + return s; + } - Real SumExponentialsRootSolver::secondDerivative(Real x) const { - ++fDoublePrimeCtr_; + Real SumExponentialsRootSolver::secondDerivative(Real x) const { + ++fDoublePrimeCtr_; - Real s = 0.0; - for (Size i=0; i < a_.size(); ++i) - s += a_[i]*squared(sig_[i])*std::exp(sig_[i]*x); - return s; - } + Real s = 0.0; + for (Size i=0; i < a_.size(); ++i) + s += a_[i]*squared(sig_[i])*std::exp(sig_[i]*x); + return s; + } - Size SumExponentialsRootSolver::getFCtr() const { - return fCtr_; - } + Size SumExponentialsRootSolver::getFCtr() const { + return fCtr_; + } - Size SumExponentialsRootSolver::getDerivativeCtr() const { - return fPrimeCtr_; - } + Size SumExponentialsRootSolver::getDerivativeCtr() const { + return fPrimeCtr_; + } - Size SumExponentialsRootSolver::getSecondDerivativeCtr() const { - return fDoublePrimeCtr_; - } + Size SumExponentialsRootSolver::getSecondDerivativeCtr() const { + return fDoublePrimeCtr_; + } - Real SumExponentialsRootSolver::getRoot(Real xTol, Strategy strategy) const { - const Array attr = a_*sig_; - QL_REQUIRE( - std::all_of( - attr.begin(), attr.end(), - [](Real x) -> bool { return x >= 0.0; } - ), - "a*sig should not be negative" - ); - - const bool logProb = - std::all_of( - a_.begin(), a_.end(), - [](Real x) -> bool { return x > 0;} - ); - - QL_REQUIRE(K_ > 0 || !logProb, - "non-positive strikes only allowed for spread options"); - - // linear approximation - const Real denom = std::accumulate(attr.begin(), attr.end(), 0.0); - const Real xInit = (std::abs(denom) > 1000*QL_EPSILON) - ? std::min(10.0, std::max(-10.0, - (K_ - std::accumulate(a_.begin(), a_.end(), 0.0))/denom) - ) - : 0.0; - - switch(strategy) { - case Brent: - return QuantLib::Brent().solve(*this, xTol, xInit, 1.0); - case Newton: - return QuantLib::Newton().solve(*this, xTol, xInit, 1.0); - case Ridder: - return QuantLib::Ridder().solve(*this, xTol, xInit, 1.0); - case Halley: - return QuantLib::Halley().solve(*this, xTol, xInit, 1.0); - default: - QL_FAIL("unknown strategy type"); + Real SumExponentialsRootSolver::getRoot(Real xTol, Strategy strategy) const { + const Array attr = a_*sig_; + QL_REQUIRE( + std::all_of( + attr.begin(), attr.end(), + [](Real x) -> bool { return x >= 0.0; } + ), + "a*sig should not be negative" + ); + + const bool logProb = + std::all_of( + a_.begin(), a_.end(), + [](Real x) -> bool { return x > 0;} + ); + + QL_REQUIRE(K_ > 0 || !logProb, + "non-positive strikes only allowed for spread options"); + + // linear approximation + const Real denom = std::accumulate(attr.begin(), attr.end(), 0.0); + const Real xInit = (std::abs(denom) > 1000*QL_EPSILON) + ? std::min(10.0, std::max(-10.0, + (K_ - std::accumulate(a_.begin(), a_.end(), 0.0))/denom) + ) + : 0.0; + + switch(strategy) { + case Brent: + return QuantLib::Brent().solve(*this, xTol, xInit, 1.0); + case Newton: + return QuantLib::Newton().solve(*this, xTol, xInit, 1.0); + case Ridder: + return QuantLib::Ridder().solve(*this, xTol, xInit, 1.0); + case Halley: + return QuantLib::Halley().solve(*this, xTol, xInit, 1.0); + default: + QL_FAIL("unknown strategy type"); + } } } - SingleFactorBsmBasketEngine::SingleFactorBsmBasketEngine( std::vector > p, Real xTol) @@ -168,9 +169,9 @@ namespace QuantLib { std::accumulate(fwdBasket.begin(), fwdBasket.end(), 0.0)); } else { - const Real d = -SumExponentialsRootSolver( + const Real d = -detail::SumExponentialsRootSolver( fwdBasket*Exp(-0.5*v), stdDev, strike) - .getRoot(xTol_, SumExponentialsRootSolver::Brent); + .getRoot(xTol_, detail::SumExponentialsRootSolver::Brent); const CumulativeNormalDistribution N; const Real cp = (payoff->optionType() == Option::Call) ? 1.0 : -1.0; diff --git a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp index 74bdc7b818a..3e571cb3737 100644 --- a/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp +++ b/ql/pricingengines/basket/singlefactorbsmbasketengine.hpp @@ -39,28 +39,6 @@ namespace QuantLib { \ingroup basketengines */ - class SumExponentialsRootSolver { - public: - enum Strategy {Ridder, Newton, Brent, Halley}; - - SumExponentialsRootSolver(Array a, Array sig, Real K); - - Real operator()(Real x) const; - Real derivative(Real x) const; - Real secondDerivative(Real x) const; - - Real getRoot(Real xTol = 1e6*QL_EPSILON, Strategy strategy = Brent) const; - - Size getFCtr() const; - Size getDerivativeCtr() const; - Size getSecondDerivativeCtr() const; - - private: - const Array a_, sig_; - const Real K_; - mutable Size fCtr_, fPrimeCtr_, fDoublePrimeCtr_; - }; - class SingleFactorBsmBasketEngine : public BasketOption::engine { public: explicit SingleFactorBsmBasketEngine( @@ -74,6 +52,30 @@ namespace QuantLib { const Size n_; const std::vector > processes_; }; + + namespace detail { + class SumExponentialsRootSolver { + public: + enum Strategy {Ridder, Newton, Brent, Halley}; + + SumExponentialsRootSolver(Array a, Array sig, Real K); + + Real operator()(Real x) const; + Real derivative(Real x) const; + Real secondDerivative(Real x) const; + + Real getRoot(Real xTol = 1e6*QL_EPSILON, Strategy strategy = Brent) const; + + Size getFCtr() const; + Size getDerivativeCtr() const; + Size getSecondDerivativeCtr() const; + + private: + const Array a_, sig_; + const Real K_; + mutable Size fCtr_, fPrimeCtr_, fDoublePrimeCtr_; + }; + } } diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp old mode 100755 new mode 100644 index ef49b2284c3..a5772f4b57d --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -1762,20 +1762,20 @@ BOOST_AUTO_TEST_CASE(testDengLiZhouWithNegativeStrike) { BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { BOOST_TEST_MESSAGE("Testing the root of a sum of exponentials..."); - BOOST_CHECK_THROW(SumExponentialsRootSolver( + BOOST_CHECK_THROW(detail::SumExponentialsRootSolver( {2.0, 3.0, 4.0}, {0.2, 0.4, -0.1}, 0.0).getRoot() , Error ); - BOOST_CHECK_THROW(SumExponentialsRootSolver( + BOOST_CHECK_THROW(detail::SumExponentialsRootSolver( {2.0, -3.0, 4.0}, {0.2, -0.4, -0.1}, 0.0).getRoot(), Error ); MersenneTwisterUniformRng mt(42); for (auto strategy: { - std::make_tuple("Brent", SumExponentialsRootSolver::Brent), - std::make_tuple("Newton", SumExponentialsRootSolver::Newton), - std::make_tuple("Ridder", SumExponentialsRootSolver::Ridder), - std::make_tuple("Halley", SumExponentialsRootSolver::Halley) + std::make_tuple("Brent", detail::SumExponentialsRootSolver::Brent), + std::make_tuple("Newton", detail::SumExponentialsRootSolver::Newton), + std::make_tuple("Ridder", detail::SumExponentialsRootSolver::Ridder), + std::make_tuple("Halley", detail::SumExponentialsRootSolver::Halley) }) { Size fCtr = 0; @@ -1792,14 +1792,14 @@ BOOST_AUTO_TEST_CASE(testRootOfSumExponentials) { a[j] = mt.nextReal() + offset; sig[j] = copysign(1.0, a[j])*mt.nextReal(); } - const Real kMin = SumExponentialsRootSolver(a, sig, 0.0)(-10.0); - const Real kMax = SumExponentialsRootSolver(a, sig, 0.0)( 10.0); + const Real kMin = detail::SumExponentialsRootSolver(a, sig, 0.0)(-10.0); + const Real kMax = detail::SumExponentialsRootSolver(a, sig, 0.0)( 10.0); const Real K = (kMax - kMin)*mt.nextReal() + kMin; - const Real xValue = SumExponentialsRootSolver(a, sig, K) - .getRoot(acc, SumExponentialsRootSolver::Brent); + const Real xValue = detail::SumExponentialsRootSolver(a, sig, K) + .getRoot(acc, detail::SumExponentialsRootSolver::Brent); - const SumExponentialsRootSolver solver(a, sig, K); + const detail::SumExponentialsRootSolver solver(a, sig, K); const Real xRoot = solver.getRoot(tol, std::get<1>(strategy)); stats.add(xValue - xRoot);