DiscreteHedging.cpp
This is an example of using the QuantLib Monte Carlo framework.
00001 /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ 00002 00021 /* This example computes profit and loss of a discrete interval hedging 00022 strategy and compares with the results of Derman & Kamal's (Goldman Sachs 00023 Equity Derivatives Research) Research Note: "When You Cannot Hedge 00024 Continuously: The Corrections to Black-Scholes" 00025 http://www.ederman.com/emanuelderman/GSQSpapers/when_you_cannot_hedge.pdf 00026 00027 Suppose an option hedger sells an European option and receives the 00028 Black-Scholes value as the options premium. 00029 Then he follows a Black-Scholes hedging strategy, rehedging at discrete, 00030 evenly spaced time intervals as the underlying stock changes. At 00031 expiration, the hedger delivers the option payoff to the option holder, 00032 and unwinds the hedge. We are interested in understanding the final 00033 profit or loss of this strategy. 00034 00035 If the hedger had followed the exact Black-Scholes replication strategy, 00036 re-hedging continuously as the underlying stock evolved towards its final 00037 value at expiration, then, no matter what path the stock took, the final 00038 P&L would be exactly zero. When the replication strategy deviates from 00039 the exact Black-Scholes method, the final P&L may deviate from zero. This 00040 deviation is called the replication error. When the hedger rebalances at 00041 discrete rather than continuous intervals, the hedge is imperfect and the 00042 replication is inexact. The more often hedging occurs, the smaller the 00043 replication error. 00044 00045 We examine the range of possibilities, computing the replication error. 00046 */ 00047 00048 // the only header you need to use QuantLib 00049 #define BOOST_LIB_DIAGNOSTIC 00050 # include <ql/quantlib.hpp> 00051 #undef BOOST_LIB_DIAGNOSTIC 00052 00053 #ifdef BOOST_MSVC 00054 /* Uncomment the following lines to unmask floating-point 00055 exceptions. Warning: unpredictable results can arise... 00056 00057 See http://www.wilmott.com/messageview.cfm?catid=10&threadid=9481 00058 Is there anyone with a definitive word about this? 00059 */ 00060 // #include <float.h> 00061 // namespace { unsigned int u = _controlfp(_EM_INEXACT, _MCW_EM); } 00062 #endif 00063 00064 #include <boost/timer.hpp> 00065 #include <iostream> 00066 #include <iomanip> 00067 00068 using namespace QuantLib; 00069 00070 #if defined(QL_ENABLE_SESSIONS) 00071 namespace QuantLib { 00072 00073 Integer sessionId() { return 0; } 00074 00075 } 00076 #endif 00077 00078 00079 /* The ReplicationError class carries out Monte Carlo simulations to evaluate 00080 the outcome (the replication error) of the discrete hedging strategy over 00081 different, randomly generated scenarios of future stock price evolution. 00082 */ 00083 class ReplicationError 00084 { 00085 public: 00086 ReplicationError(Option::Type type, 00087 Time maturity, 00088 Real strike, 00089 Real s0, 00090 Volatility sigma, 00091 Rate r) 00092 : maturity_(maturity), payoff_(type, strike), s0_(s0), 00093 sigma_(sigma), r_(r) { 00094 00095 // value of the option 00096 DiscountFactor rDiscount = std::exp(-r_*maturity_); 00097 DiscountFactor qDiscount = 1.0; 00098 Real forward = s0_*qDiscount/rDiscount; 00099 Real variance = sigma_*sigma_*maturity_; 00100 boost::shared_ptr<StrikedTypePayoff> payoff( 00101 new PlainVanillaPayoff(payoff_)); 00102 BlackFormula black(forward,rDiscount,variance,payoff); 00103 std::cout << "Option value: " << black.value() << std::endl; 00104 00105 // store option's vega, since Derman and Kamal's formula needs it 00106 vega_ = black.vega(maturity_); 00107 00108 std::cout << std::endl; 00109 std::cout << 00110 " | | P&L \t| P&L | Derman&Kamal | P&L" 00111 " \t| P&L" << std::endl; 00112 00113 std::cout << 00114 "samples | trades | Mean \t| Std Dev | Formula |" 00115 " skewness \t| kurt." << std::endl; 00116 00117 std::cout << "---------------------------------" 00118 "----------------------------------------------" << std::endl; 00119 } 00120 00121 // the actual replication error computation 00122 void compute(Size nTimeSteps, Size nSamples); 00123 private: 00124 Time maturity_; 00125 PlainVanillaPayoff payoff_; 00126 Real s0_; 00127 Volatility sigma_; 00128 Rate r_; 00129 Real vega_; 00130 }; 00131 00132 // The key for the MonteCarlo simulation is to have a PathPricer that 00133 // implements a value(const Path& path) method. 00134 // This method prices the portfolio for each Path of the random variable 00135 class ReplicationPathPricer : public PathPricer<Path> { 00136 public: 00137 // real constructor 00138 ReplicationPathPricer(Option::Type type, 00139 Real strike, 00140 Rate r, 00141 Time maturity, 00142 Volatility sigma) 00143 : type_(type), strike_(strike), 00144 r_(r), maturity_(maturity), sigma_(sigma) { 00145 QL_REQUIRE(strike_ > 0.0, "strike must be positive"); 00146 QL_REQUIRE(r_ >= 0.0, 00147 "risk free rate (r) must be positive or zero"); 00148 QL_REQUIRE(maturity_ > 0.0, "maturity must be positive"); 00149 QL_REQUIRE(sigma_ >= 0.0, 00150 "volatility (sigma) must be positive or zero"); 00151 00152 } 00153 // The value() method encapsulates the pricing code 00154 Real operator()(const Path& path) const; 00155 00156 private: 00157 Option::Type type_; 00158 Real strike_; 00159 Rate r_; 00160 Time maturity_; 00161 Volatility sigma_; 00162 }; 00163 00164 00165 // Compute Replication Error as in the Derman and Kamal's research note 00166 int main(int, char* []) 00167 { 00168 try { 00169 QL_IO_INIT 00170 00171 boost::timer timer; 00172 std::cout << std::endl; 00173 00174 Time maturity = 1.0/12.0; // 1 month 00175 Real strike = 100; 00176 Real underlying = 100; 00177 Volatility volatility = 0.20; // 20% 00178 Rate riskFreeRate = 0.05; // 5% 00179 ReplicationError rp(Option::Call, maturity, strike, underlying, 00180 volatility, riskFreeRate); 00181 00182 Size scenarios = 50000; 00183 Size hedgesNum; 00184 00185 hedgesNum = 21; 00186 rp.compute(hedgesNum, scenarios); 00187 00188 hedgesNum = 84; 00189 rp.compute(hedgesNum, scenarios); 00190 00191 Real seconds = timer.elapsed(); 00192 Integer hours = int(seconds/3600); 00193 seconds -= hours * 3600; 00194 Integer minutes = int(seconds/60); 00195 seconds -= minutes * 60; 00196 std::cout << " \nRun completed in "; 00197 if (hours > 0) 00198 std::cout << hours << " h "; 00199 if (hours > 0 || minutes > 0) 00200 std::cout << minutes << " m "; 00201 std::cout << std::fixed << std::setprecision(0) 00202 << seconds << " s\n" << std::endl; 00203 00204 return 0; 00205 } catch (std::exception& e) { 00206 std::cout << e.what() << std::endl; 00207 return 1; 00208 } catch (...) { 00209 std::cout << "unknown error" << std::endl; 00210 return 1; 00211 } 00212 } 00213 00214 00215 /* The actual computation of the Profit&Loss for each single path. 00216 00217 In each scenario N rehedging trades spaced evenly in time over 00218 the life of the option are carried out, using the Black-Scholes 00219 hedge ratio. 00220 */ 00221 Real ReplicationPathPricer::operator()(const Path& path) const { 00222 00223 Size n = path.length()-1; 00224 QL_REQUIRE(n>0, "the path cannot be empty"); 00225 00226 // discrete hedging interval 00227 Time dt = maturity_/n; 00228 00229 // For simplicity, we assume the stock pays no dividends. 00230 Rate stockDividendYield = 0.0; 00231 00232 // let's start 00233 Time t = 0; 00234 00235 // stock value at t=0 00236 Real stock = path.front(); 00237 00238 // money account at t=0 00239 Real money_account = 0.0; 00240 00241 /************************/ 00242 /*** the initial deal ***/ 00243 /************************/ 00244 // option fair price (Black-Scholes) at t=0 00245 DiscountFactor rDiscount = std::exp(-r_*maturity_); 00246 DiscountFactor qDiscount = std::exp(-stockDividendYield*maturity_); 00247 Real forward = stock*qDiscount/rDiscount; 00248 Real variance = sigma_*sigma_*maturity_; 00249 boost::shared_ptr<StrikedTypePayoff> payoff( 00250 new PlainVanillaPayoff(type_,strike_)); 00251 BlackFormula black(forward,rDiscount,variance,payoff); 00252 // sell the option, cash in its premium 00253 money_account += black.value(); 00254 // compute delta 00255 Real delta = black.delta(stock); 00256 // delta-hedge the option buying stock 00257 Real stockAmount = delta; 00258 money_account -= stockAmount*stock; 00259 00260 /**********************************/ 00261 /*** hedging during option life ***/ 00262 /**********************************/ 00263 for (Size step = 0; step < n-1; step++){ 00264 00265 // time flows 00266 t += dt; 00267 00268 // accruing on the money account 00269 money_account *= std::exp( r_*dt ); 00270 00271 // stock growth: 00272 stock = path[step+1]; 00273 00274 // recalculate option value at the current stock value, 00275 // and the current time to maturity 00276 rDiscount = std::exp(-r_*(maturity_-t)); 00277 qDiscount = std::exp(-stockDividendYield*(maturity_-t)); 00278 forward = stock*qDiscount/rDiscount; 00279 variance = sigma_*sigma_*(maturity_-t); 00280 BlackFormula black(forward,rDiscount,variance,payoff); 00281 00282 // recalculate delta 00283 delta = black.delta(stock); 00284 00285 // re-hedging 00286 money_account -= (delta - stockAmount)*stock; 00287 stockAmount = delta; 00288 } 00289 00290 /*************************/ 00291 /*** option expiration ***/ 00292 /*************************/ 00293 // last accrual on my money account 00294 money_account *= std::exp( r_*dt ); 00295 // last stock growth 00296 stock = path[n]; 00297 00298 // the hedger delivers the option payoff to the option holder 00299 Real optionPayoff = PlainVanillaPayoff(type_, strike_)(stock); 00300 money_account -= optionPayoff; 00301 00302 // and unwinds the hedge selling his stock position 00303 money_account += stockAmount*stock; 00304 00305 // final Profit&Loss 00306 return money_account; 00307 } 00308 00309 00310 // The computation over nSamples paths of the P&L distribution 00311 void ReplicationError::compute(Size nTimeSteps, Size nSamples) 00312 { 00313 QL_REQUIRE(nTimeSteps>0, "the number of steps must be > 0"); 00314 00315 // hedging interval 00316 // Time tau = maturity_ / nTimeSteps; 00317 00318 /* Black-Scholes framework: the underlying stock price evolves 00319 lognormally with a fixed known volatility that stays constant 00320 throughout time. 00321 */ 00322 Date today = Date::todaysDate(); 00323 DayCounter dayCount = Actual365Fixed(); 00324 Handle<Quote> stateVariable( 00325 boost::shared_ptr<Quote>(new SimpleQuote(s0_))); 00326 Handle<YieldTermStructure> riskFreeRate( 00327 boost::shared_ptr<YieldTermStructure>( 00328 new FlatForward(today, r_, dayCount))); 00329 Handle<YieldTermStructure> dividendYield( 00330 boost::shared_ptr<YieldTermStructure>( 00331 new FlatForward(today, 0.0, dayCount))); 00332 Handle<BlackVolTermStructure> volatility( 00333 boost::shared_ptr<BlackVolTermStructure>( 00334 new BlackConstantVol(today, sigma_, dayCount))); 00335 boost::shared_ptr<StochasticProcess1D> diffusion( 00336 new BlackScholesMertonProcess(stateVariable, dividendYield, 00337 riskFreeRate, volatility)); 00338 00339 // Black Scholes equation rules the path generator: 00340 // at each step the log of the stock 00341 // will have drift and sigma^2 variance 00342 PseudoRandom::rsg_type rsg = 00343 PseudoRandom::make_sequence_generator(nTimeSteps, 0); 00344 00345 bool brownianBridge = false; 00346 00347 typedef SingleVariate<PseudoRandom>::path_generator_type generator_type; 00348 boost::shared_ptr<generator_type> myPathGenerator(new 00349 generator_type(diffusion, maturity_, nTimeSteps, 00350 rsg, brownianBridge)); 00351 00352 // The replication strategy's Profit&Loss is computed for each path 00353 // of the stock. The path pricer knows how to price a path using its 00354 // value() method 00355 boost::shared_ptr<PathPricer<Path> > myPathPricer(new 00356 ReplicationPathPricer(payoff_.optionType(), payoff_.strike(), 00357 r_, maturity_, sigma_)); 00358 00359 // a statistics accumulator for the path-dependant Profit&Loss values 00360 Statistics statisticsAccumulator; 00361 00362 // The OneFactorMontecarloModel generates paths using myPathGenerator 00363 // each path is priced using myPathPricer 00364 // prices will be accumulated into statisticsAccumulator 00365 OneFactorMonteCarloOption MCSimulation(myPathGenerator, 00366 myPathPricer, 00367 statisticsAccumulator, 00368 false); 00369 00370 // the model simulates nSamples paths 00371 MCSimulation.addSamples(nSamples); 00372 00373 // the sampleAccumulator method of OneFactorMonteCarloOption_old 00374 // gives access to all the methods of statisticsAccumulator 00375 Real PLMean = MCSimulation.sampleAccumulator().mean(); 00376 Real PLStDev = MCSimulation.sampleAccumulator().standardDeviation(); 00377 Real PLSkew = MCSimulation.sampleAccumulator().skewness(); 00378 Real PLKurt = MCSimulation.sampleAccumulator().kurtosis(); 00379 00380 // Derman and Kamal's formula 00381 Real theorStD = std::sqrt(M_PI/4/nTimeSteps)*vega_*sigma_; 00382 00383 00384 std::cout << std::fixed 00385 << nSamples << "\t| " 00386 << nTimeSteps << "\t | " 00387 << std::setprecision(3) << PLMean << " \t| " 00388 << std::setprecision(2) << PLStDev << " \t | " 00389 << std::setprecision(2) << theorStD << " \t | " 00390 << std::setprecision(2) << PLSkew << " \t| " 00391 << std::setprecision(2) << PLKurt << std::endl; 00392 }