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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 65 additions & 11 deletions ql/pricingengines/exotic/analyticholderextensibleoptionengine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
*/

#include <ql/exercise.hpp>
#include <ql/math/distributions/bivariatenormaldistribution.hpp>
#include <ql/math/distributions/normaldistribution.hpp>
#include <ql/math/functional.hpp>
#include <ql/pricingengines/exotic/analyticholderextensibleoptionengine.hpp>
#include <ql/math/distributions/bivariatenormaldistribution.hpp>
#include <cmath>
#include <utility>

using std::pow;
Expand All @@ -30,6 +32,23 @@ using std::sqrt;

namespace QuantLib {

namespace {

// maximum number of Newton-Raphson steps to find a critical price
const Size maxIterations = 100;

Real nextCriticalPrice(Real spot, Real value, Real derivative) {
QL_REQUIRE(std::isfinite(value), "critical price: non-finite residual");
QL_REQUIRE(std::isfinite(derivative) && derivative != 0.0,
"critical price: zero or non-finite derivative");
Real next = spot - value / derivative;
QL_REQUIRE(std::isfinite(next) && next > 0.0,
"critical price: non-positive or non-finite iterate");
return next;
}

}

AnalyticHolderExtensibleOptionEngine::AnalyticHolderExtensibleOptionEngine(
ext::shared_ptr<GeneralizedBlackScholesProcess> process)
: process_(std::move(process)) {
Expand Down Expand Up @@ -68,8 +87,11 @@ namespace QuantLib {
Real result = 0;
constexpr double minusInf = -std::numeric_limits<double>::infinity();

// extension never pays if it is not worth the premium at X1
// Check whether the standard extension region is empty.
if (bsCalculator(X1, payoff->optionType()).value() <= A) {
QL_REQUIRE(payoff->optionType() != Option::Call ||
dividendDiscount(T2 - t1) <= 1.0,
"holder-extensible call extension only at high spots is not supported");
results_.value = BlackScholesCalculator(payoff, S, growth, vol*sqrt(t1), discount).value();
return;
}
Expand Down Expand Up @@ -119,8 +141,11 @@ namespace QuantLib {
Real epsilon = 0.001;

//Newton-Raphson process
while (std::fabs(yi) > epsilon){
Sv = Sv - yi / di;
Size iterations = 0;
while (std::fabs(yi) > epsilon || !std::isfinite(yi)) {
QL_REQUIRE(++iterations <= maxIterations,
"lower critical price: max iterations exceeded");
Sv = nextCriticalPrice(Sv, yi, di);

bs = bsCalculator(Sv, Option::Call);
ci = bs.value();
Expand All @@ -143,10 +168,30 @@ namespace QuantLib {
Real r=riskFreeRate();

Real val=X1-X2*std::exp(-r*(T2-t1));
DiscountFactor growth = dividendDiscount(T2 - t1);
// with dividends, exercise beats extension at a high enough spot
if(A< val && dividendDiscount(T2-t1) >= 1.0){
if (A < val && growth >= 1.0) {
return std::numeric_limits<Real>::infinity();
} else {
if (growth > 1.0) {
/* With a negative dividend yield, the function solved for below, c(S) - A - S + X1,
is convex but its slope tends to growth - 1 > 0, so it can have no zero. Its
minimum is where the call delta, growth*N(d1), equals 1; since c(S) is then
S - X2*D_r*N(d2), the minimum is X1 - A - X2*D_r*N(d2). If it is not negative,
or if X1 (where the function is positive) lies to the right of the minimum and
thus of any zero, exercise never beats extension. */
Real stdDev = volatility() * std::sqrt(T2 - t1);
DiscountFactor discount = riskFreeDiscount(T2 - t1);
Real d1 = InverseCumulativeNormal()(1.0 / growth);
Real Smin = X2 * std::exp(d1 * stdDev - 0.5 * stdDev * stdDev) * discount / growth;
Real minimum = X1 - A - X2 * discount * CumulativeNormalDistribution()(d1 - stdDev);
if (minimum >= 0.0 || X1 >= Smin)
return std::numeric_limits<Real>::infinity();

// The second extension region needs an additional critical price.
QL_FAIL("holder-extensible call with two roots is not supported");
}

BlackScholesCalculator bs = bsCalculator(Sv, Option::Call);
Real ci = bs.value();
Real dc = bs.delta();
Expand All @@ -157,8 +202,11 @@ namespace QuantLib {
Real epsilon = 0.001;

//Newton-Raphson process
while (std::fabs(yi) > epsilon){
Sv = Sv - yi / di;
Size iterations = 0;
while (std::fabs(yi) > epsilon || !std::isfinite(yi)) {
QL_REQUIRE(++iterations <= maxIterations,
"upper critical price: max iterations exceeded");
Sv = nextCriticalPrice(Sv, yi, di);

bs = bsCalculator(Sv, Option::Call);
ci = bs.value();
Expand Down Expand Up @@ -195,8 +243,11 @@ namespace QuantLib {
Real epsilon = 0.001;

//Newton-Raphson prosess
while (std::fabs(yi) > epsilon){
Sv = Sv - yi / di;
Size iterations = 0;
while (std::fabs(yi) > epsilon || !std::isfinite(yi)) {
QL_REQUIRE(++iterations <= maxIterations,
"lower critical price: max iterations exceeded");
Sv = nextCriticalPrice(Sv, yi, di);

bs = bsCalculator(Sv, Option::Put);
pi = bs.value();
Expand Down Expand Up @@ -225,8 +276,11 @@ namespace QuantLib {
Real epsilon = 0.001;

//Newton-Raphson prosess
while (std::fabs(yi) > epsilon){
Sv = Sv - yi / di;
Size iterations = 0;
while (std::fabs(yi) > epsilon || !std::isfinite(yi)) {
QL_REQUIRE(++iterations <= maxIterations,
"upper critical price: max iterations exceeded");
Sv = nextCriticalPrice(Sv, yi, di);

bs = bsCalculator(Sv, Option::Put);
pi = bs.value();
Expand Down
84 changes: 84 additions & 0 deletions test-suite/extensibleoptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <ql/utilities/dataformatters.hpp>
#include <ql/time/calendars/target.hpp>
#include <ql/time/daycounters/actual360.hpp>
#include <ql/time/daycounters/actual365fixed.hpp>

using namespace QuantLib;
using namespace boost::unit_test_framework;
Expand Down Expand Up @@ -213,6 +214,89 @@ BOOST_AUTO_TEST_CASE(testHolderExtensibleOptionCriticalPrices) {
}
}

BOOST_AUTO_TEST_CASE(testHolderExtensibleOptionNegativeDividendYield) {
BOOST_TEST_MESSAGE("Testing holder-extensible options with a negative dividend yield...");

DayCounter dc = Actual365Fixed();
Date today = Settings::instance().evaluationDate();

auto spot = ext::make_shared<SimpleQuote>(0.0);
auto qRate = ext::make_shared<SimpleQuote>(0.0);
auto rRate = ext::make_shared<SimpleQuote>(0.0);
auto vol = ext::make_shared<SimpleQuote>(0.0);

auto process = ext::make_shared<BlackScholesMertonProcess>(
Handle<Quote>(spot), Handle<YieldTermStructure>(flatRate(today, qRate, dc)),
Handle<YieldTermStructure>(flatRate(today, rRate, dc)),
Handle<BlackVolTermStructure>(flatVol(today, vol, dc)));
auto engine = ext::make_shared<AnalyticHolderExtensibleOptionEngine>(process);

struct Case {
const char* description;
Option::Type type;
Real spot, strike1, strike2, premium, volatility, riskFreeRate, dividendYield;
Integer days1, days2;
Real expected;
const char* expectedError = nullptr;
};

/* Reference prices: discounted expectation at the first expiry of
max(exercise, extension - premium, 0), by numerical integration in
mpmath. With a negative yield, the extension value of a call minus its
exercise value is convex in the spot but does not decrease for large
spots. The cases below include no zero and two zeros. */
Case cases[] = {
// exercise never beats extension: no zero
{"no zero", Option::Call, 118.0, 119.08, 117.523, 2.30157, 0.0864588, 0.00138423,
-0.00971198, 185, 424, 3.513400085302},
{"no zero", Option::Call, 118.0, 117.852, 118.726, 1.59195, 0.179851, 0.00232909,
-0.0246219, 545, 870, 15.845257803666},
// two zeros, the first strike beyond the minimum: extension dominates exercise
{"two zeros, strike beyond the minimum", Option::Call, 100.0, 100.0, 100.0, 36.0, 0.2, 0.02,
-0.3, 182, 547, 25.363356673445},
// two zeros before and after an exercise region: an additional boundary is required
{"two zeros, strike before the minimum", Option::Call, 118.0, 119.08, 117.523, 2.30157,
0.0864588, 0.00138423, -0.005, 185, 424, 3.1862655186463,
"holder-extensible call with two roots is not supported"},
// the premium is below X1 - X2*exp(-r*(T2-t1)), so there is no zero
{"low premium", Option::Call, 100.0, 100.0, 95.0, 1.0, 0.25, 0.08, -0.02, 180, 270,
14.590398720627},
{"low premium, zero yield", Option::Call, 100.0, 100.0, 95.0, 1.0, 0.25, 0.08, 0.0, 180,
270, 13.49148769505},
{"put", Option::Put, 100.0, 100.0, 105.0, 1.0, 0.25, 0.08, -0.01, 180, 270,
6.8762827333313},
{"extension only at high spots", Option::Call, 100.0, 100.0, 100.0, 40.0,
0.2, 0.02, -0.3, 182, 547, 0.0,
"holder-extensible call extension only at high spots is not supported"},
// Saturated deltas must not produce infinite Newton steps.
{"zero call derivative", Option::Call, 100.0, 100.0, 100.0, 1.0, 100.0, 0.0, 0.0,
365, 730, 0.0, "critical price: zero or non-finite derivative"},
{"zero put derivative", Option::Put, 100.0, 100.0, 100.0, 1.0, 100.0, 0.0, 0.0,
365, 730, 0.0, "critical price: zero or non-finite derivative"},
};

Real tolerance = 1e-4;
for (const auto& c : cases) {
spot->setValue(c.spot);
qRate->setValue(c.dividendYield);
rRate->setValue(c.riskFreeRate);
vol->setValue(c.volatility);

HolderExtensibleOption option(c.type, c.premium, today + c.days2, c.strike2,
ext::make_shared<PlainVanillaPayoff>(c.type, c.strike1),
ext::make_shared<EuropeanExercise>(today + c.days1));
option.setPricingEngine(engine);

BOOST_TEST_CONTEXT(c.description << ", dividend yield " << c.dividendYield) {
if (c.expectedError)
BOOST_CHECK_EXCEPTION(option.NPV(), Error,
ExpectedErrorMessage(c.expectedError));
else
QL_CHECK_SMALL(option.NPV() - c.expected, tolerance);
}
}
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()