import QuantLib as ql
Numerical Greeks
In this notebook, I’ll build on the facilities provided by the Instrument
class (that is, its ability to detect changes in its inputs and recalculate accordingly) to show how to calculate numerical Greeks when a given engine doesn’t provide them.
As usual, we import the QuantLib module and set the evaluation date:
= ql.Date(8, ql.October, 2014)
today = today ql.Settings.instance().evaluationDate
A somewhat exotic option
As an example, we’ll use a knock-in barrier option:
= 100.0
strike = 120.0
barrier = 30.0
rebate
= ql.BarrierOption(
option
ql.Barrier.UpIn,
barrier,
rebate,
ql.PlainVanillaPayoff(ql.Option.Call, strike),8, ql.January, 2015)),
ql.EuropeanExercise(ql.Date( )
For the purpose of this example, the market data are the underlying value, the risk-free rate and the volatility. We wrap them in quotes, so that the instrument will be notified of any changes…
= ql.SimpleQuote(100.0)
u = ql.SimpleQuote(0.01)
r = ql.SimpleQuote(0.20) σ
…and from the quotes we build the flat curves and the process that the engine requires. As explained in another notebook, we build the term structures so that they move with the evaluation date; this will be useful further on.
= ql.FlatForward(
riskFreeCurve 0, ql.TARGET(), ql.QuoteHandle(r), ql.Actual360()
)= ql.BlackConstantVol(
volatility 0, ql.TARGET(), ql.QuoteHandle(σ), ql.Actual360()
)
= ql.BlackScholesProcess(
process
ql.QuoteHandle(u),
ql.YieldTermStructureHandle(riskFreeCurve),
ql.BlackVolTermStructureHandle(volatility), )
Finally, we build the engine (the library provides one based on an analytic formula) and set it to the option.
option.setPricingEngine(ql.AnalyticBarrierEngine(process))
Now we can ask the option for its value…
print(option.NPV())
29.24999528163375
…but we’re not so lucky when it comes to Greeks:
try:
print(option.delta())
except Exception as e:
print(f"{type(e).__name__}: {e}")
RuntimeError: delta not provided
The engine doesn’t provide the delta, so asking for it raises an error.
Numerical calculation
What does a quant have to do? We can use numerical differentiation to approximate the Greeks: that is, we can approximate the derivative by calculating the option value for two slightly different values of the underlying and by taking the slope between the resulting points.
The relevant formulas are:
\[ \Delta = \frac{P(u_0+h)-P(u_0-h)}{2h} \; \; \; \; \; \; \Gamma = \frac{P(u_0+h)-2P(u_0)+P(u_0-h)}{h^2} \]
where \(P(u)\) is the price of the option for a given value of the underlying \(u\), \(u_0\) is the current value of the underlying and \(h\) is a tiny increment.
Thanks to the framework we set in place, getting the perturbed prices is easy enough: we can set the relevant quote to the new value and ask the option for its price again. Thus, we choose a value for \(h\) and start. First, we save the current value of the option…
= u.value()
u0 = 0.01 h
= option.NPV()
P0 print(P0)
29.24999528163375
…then we increase the underlying value and get the new option value…
+ h)
u.setValue(u0 = option.NPV()
P_plus print(P_plus)
29.24851583696518
…then we do the same after decreasing the underlying value.
- h)
u.setValue(u0 = option.NPV()
P_minus print(P_minus)
29.25147220877793
Finally, we set the underlying value back to its current value.
u.setValue(u0)
Applying the formulas above give us the desired Greeks:
= (P_plus - P_minus) / (2 * h)
Δ = (P_plus - 2 * P0 + P_minus) / (h * h)
Γ print(Δ)
print(Γ)
-0.14781859063752734
-0.025175243933972524
The approach works for almost any Greeks. We can use the two-sided formula above, or the one-sided formula below if we want to minimize the number of evaluations:
\[ \frac{\partial P}{\partial x} = \frac{P(x_0+h)-P(x_0)}{h} \]
For instance, here we calculate Rho and Vega:
= r.value()
r0 = 0.0001
h
+ h)
r.setValue(r0 = option.NPV()
P_plus
r.setValue(r0)
= (P_plus - P0) / h
ρ print(ρ)
-9.984440333461464
0 = σ.value()
σ= 0.0001
h
0 + h)
σ.setValue(σ= option.NPV()
P_plus 0)
σ.setValue(σ
= (P_plus - P0) / h
Vega print(Vega)
-12.997818541258255
The approach for the Theta is a bit different, although it still relies on the fact that the option reacts to the change in the market data. The difference comes from the fact that we don’t have the time to maturity available as a quote, as was the case for the other quantities. Instead, since we set up the term structures so that they move with the evaluation date, we can set it to tomorrow’s date to get the corresponding option value. In this case, the value of the time increment \(h\) should be equivalent to one day:
= today + 1
ql.Settings.instance().evaluationDate = option.NPV()
P1 = today
ql.Settings.instance().evaluationDate
= 1.0 / 365
h = (P1 - P0) / h
Θ print(Θ)
5.548998890566246
Why not in the engine?
We just saw that it’s possible to calculate the missing ones numerically. So why don’t engines use this method when Greeks are not available otherwise?
It’s a good question; there are a few layers to it, and it can lead us to some insights into the architecture of the library.
The first answer is that numerical Greeks are costly: each one needs to reprice the instrument once or twice, depending on the accuracy we want. Therefore, we don’t want them to be calculated by default. You should incur the cost only if you need them.
That doesn’t need to stop us, though. We could configure the engine by passing argument to its constructor that specify which Greeks to calculate. It would add a bit of housekeeping, but it’s doable, so let’s assume we went ahead with the plan. (I won’t bother showing you the code keeping track of the configuration. That’s boring stuff, and moreover, I don’t have it.)
How would we write the calculation of the Greeks, then? As an example, let’s take the MCEuropeanEngine
class. It takes as its first input a shared_ptr
to a GeneralizedBlackScholesProcess
(I’m talking of the C++ class, of course, since that’s where we would implement the change); in turn, this contains a Handle<Quote>
containing the value of the underlying, two Handle<YieldTermStructure>
holding the risk-free rate and dividend yield curves, and a Handle<BlackVolTermStructure>
holding the volatility. Those are the inputs we want to bump before repricing the option.
It turns out that it’s fairly difficult to modify them. Even in the simplest case, that is, the Handle<Quote>
containing the underlying, we have no guarantee that the contained Quote
can be cast to a SimpleQuote
and set a new value; it couls be another user-defined class. The same holds for the other handles; without knowledge of the actual derived class of the contained YieldTermStructure
or BlackVolTermStructure
instances, there’s no way we can modify them.
We don’t actually need to do it, though, right? Let’s take, e.g., the risk-free curve; we can wrap the current instance in, say, a ZeroSpreadedTermStructure
instance to shift it upwards or backwards a given increment. Something like:
<Quote> dr(make_shared<SimpleQuote>(1e-4)); // 1 bp
Handle
auto new_curve =
<ZeroSpreadedTermStructure>(process_->riskFreeRate(), dr); make_shared
The same goes for the underlying quote: we can ask for the current value, perturb it, and store it in a new SimpleQuote
instance, as in:
= 0.001;
Real du
auto new_u =
<SimpleQuote>(process_->x0() + du); make_shared
However, the problem now is that we can’t put the new inputs into the process. It contains Handle
instances, and those can’t be relinked so that they store the new curve and quote; only RelinkableHandle
instances can. Hmm. It seems like we went out of our way to make this operation difficult, doesn’t it?
Well, in fact, we did—we wrote the code explicitly so that the inputs couldn’t be changed. We had a reason for that, though: the instrument might not be the only one currently using them. If we had a bunch of options on the same underlying, for instance, they could be sharing the same process instance; and instruments in the same currency would probably share the same risk-free curve. There’s no point creating and maintaining an instance for each instrument.
This means that any one instrument isn’t free to modify its inputs: it could affect other, unrelated instruments. That’s why inputs are only ever stored and made available through handles. (As a historical side note, we were once bitten by this before we started coding this way. In fact, we were bitten so badly we had to retire a release because it was too dangerous to use: version 0.3.5 was removed from the download page and version 0.3.6 replaced it as soon as we found out. You can check the fix here if you’re curious.)
If you’re determined enough and really want to add numerical Greeks to your engine, you can still find a way. Instead of modifying the process, you’ll have to clone it. For instance, if you want to shift the risk-free rate, you can create a shifted curve as above and write:
auto new_process =
<GeneralizedBlackScholesProcess>(
make_sharedprocess_->stateVariable(),
process_->dividendYield(),
<YieldTermStructure>(new_curve),
Handleprocess_->blackVolatility());
after which the engine can perform the calculations using the cloned process.
You might also want to think what should happen if the Greeks calculations were to raise an exception. Do you throw away the calculated price, or keep it and just bail on the Greeks, and maybe try the next one? Granted, this requires some thought even for ordinary Greeks; but those are usually calculated together with the price, and it’s more likely that they succeed or fail together.
In the end, though, we preferred to leave it to users to calculate missing Greeks. On the one hand, they can do it much more easily by perturbing the inputs that they set up and probably still hold on to; and on the other hand, unlike us, they’re also able to optimize the calculation when multiple instruments depend on the same process. There’s no need for each of them to create a new process; users can modify the relevant input once, read the new prices from all the instruments, and restore the input to its former value.