Closed feribg closed 4 years ago
Hi
it seems that some sort of explicit finite difference scheme is assumed here to calculate the lattice sizes, which defeats the object (see definition of L1, L2 ff) and leads to rather ill-conditioned grids. You are better off by letting QL decide on the lattice details and set
self.engine = ql.FdBlackScholesVanillaEngine(self.bsm_process, 100, 500, 1)
Increase t and x dimension further on if you need high precision results. I'd use a symmetric scheme for the vega calculation
def numeric_first_order(self, option, quote, h=0.0001):
sigma0 = quote.value()
quote.setValue(sigma0 - h)
P_minus = option.NPV()
quote.setValue(sigma0 + h)
P_plus = option.NPV()
quote.setValue(sigma0)
return (P_plus - P_minus) / (2*h)
Thanks that helped a bit but still negative values about half, im using a 1200x600 grid with the LHS sample in the above code.
Can you please post a parameter example, which gives negative values? 1200 is a large number of time steps, which is normally not necessary as the schemas are order (dt)^2. Thanks.
Here are some examples attached for the parameter space. I've also updated the pricing code in the issue above to include your suggestions for numeric derivatives and a fixed grid size of 200x1000 which is roughly 2x what you suggested symmetric. Is the smaller size a result of the interpolation you're doing, Im struggling to understand how a smaller step would help it converge better?
Here's a concrete example as well:
div_rate 0.012945
rf_rate 0.132976
vol 0.214675
strike 7.672765
put_call 1.000000 # 1= put, 0 = call
dte 601.000000
spot 7.050932
price 0.732451
delta -0.607794
gamma -0.774791
theta -0.036603
vega 2.624307
Also I'm wondering is there a way to get the matrices from the solver rather than the final answer. Would like to try different approximations of the Solution matrix so would be nice to get both sides of the system rather than just the point answers?
As an aside are day fractions supported, I couldn't find a way to pass a fraction of years not rounded to a day. My sampling space includes negative dividend rates but that shouldnt really be an issue and both negative and positive rates result in wrong gamma / vega.
Hi
first, thanks for the comprehensive example, it makes the discussion much easier. You are pricing American options, I've to admit that I've overseen this in my first response.
QuantLib is using the Crank-Nicolson scheme by default. This scheme is prone to suffer under spurious oscillations, especially for American ATM options (problem in your example). In addition even though the CN-scheme is of order (dt^2) the embedded dynamic programming step for the American condition is only of order (dt), hence more time steps are needed for American options than for European (but I still think as a rule of thumb 200 steps per year should be enough for non-pathological cases.).
Back to the spurious oscillations, I see three solutions here:
self.engine = ql.FdBlackScholesVanillaEngine(self.bsm_process, M, N, int(M * 0.2))
self.engine = ql.FdBlackScholesVanillaEngine(self.bsm_process, M, N, 0, ql.FdmSchemeDesc.ImplicitEuler())
self.engine = ql.FdBlackScholesVanillaEngine(self.bsm_process, M, N, 0, ql.FdmSchemeDesc.TrBDF2())
I'd be very interest in seeing your benchmark suite running with the TrBDF2 scheme. @klausspanderen Updated the code here to match your suggestions. Here's a more detailed notebook on Collab for analysis, feel free to update and pivot the data:
https://colab.research.google.com/drive/1ozqesYlsf_VWD8aMO9a3xOnIsGPupF4T
Did you have any feedback on getting the FD matrices out of QL rather than the solution point estimates (ideally in Python) or using fractional dates.
@feribg At the time being only C++ users can access the internals. The internals will be exposed with the next release 1.18 to Python users (you can already build it from Luigi's branch.). W.r.t. fractional dates, you'll have to compile QuantLib with --enable-intraday and the SWIG layer yourself to get fractional dates.
Back to your data, thanks for providing your results. I've looked into trbdf_neg_gamma.csv. IMO the majority of negative values are coming from scenarios with very small dte (<5). Here the new scaling method
N = min(1000, int(1000 * T))
M = min(200, int(200 *T))
leads to grids with only a handful of points in x and t direction, which is too little. I'd suggest to have at least 200 in x and 20 in t direction, e.g.
N = max(int(2000*T), 200)
M = max(int(400*T), 20)
the remaining ~350 scenarios with gamma < -1e-4 are interesting. I've looked into a few examples and the spot values were already very close to the exercise boundary for small times, hence the value on the grid has a kink in the neighbourhood of the spot for small t, which is a problem for the spline interpolation. You can either go to large grids to overcome this problem or use bump and reval in this case, typical bump size is 1% of the spot value.
def numeric_gamma(self, option, price, quote, h=0.01):
spot = quote.value()
quote.setValue(spot - h*spot)
P_minus = option.NPV()
quote.setValue(spot + h*spot)
P_plus = option.NPV()
quote.setValue(spot)
return (P_plus + P_minus - 2*price) / (h*h*spot*spot)
Got it, thanks @klausspanderen I've updated the colab with the new finer grid:
N = max(600, int(1000 * T))
M = max(200, int(200 *T))
but the results are non conclusive I can't see one scheme being better than another. I've also included the non negative samples as well. Given that CN seems the fastest and the results seem pretty decent I assume the default is actually the best.
It would be great if one of you (or both) wrote this up as documentation for the FdBlackScholesVanillaEngine
class.
@lballabio This one? https://github.com/lballabio/QuantLib/blob/f918e1a34a542d528a55844610950413fb645983/ql/pricingengines/vanilla/fdblackscholesvanillaengine.hpp
I believe this is just setting parameters mostly the documentation is likely around the different schemes and solvers? I can document the implicit one since that's what im trying to replicate and speed up for American options.
Yes, that would be the one. But you're right, it can be in some generic documentation for the framework. We can move it around later.
@lballabio Sure I will write something and submit a PR once we iron out some details. I've updated the code in the first post to include the BS price for the equivalent european option. Is theta that's output when available by the engine an absolute value? I don't think this is a good idea in general for example if we use an engine to price a single option vs a basket it would be very confusing, I think it's better to just return the correct true value without any transformations to it?
div = 0.089432
r = 0.096223
sigma = 1.151749
K = 5904.483856
put_call = 1.0
T = 711.0
S = 2200.588359
pricer.price_and_greeks(div,r,sigma,S,K,put_call, T)
FD NPV: 4252.408934951205,
FD delta: -0.40313953851438644,
FD gamma: 0.00016221961096990784,
FD theta: -105.83075529002323,
FD vega: 11.214407480338195,
BS NPV: 3630.6282605333213,
BS delta -0.24721236611460154,
BS gamma 6.485864122680303e-05,
BS theta 144.72424014909117,
BS vega 10.267629837079248
@klausspanderen Why would an equivalent American option cost less than it's European counterpart ever? Here's a CSV ran through the pricer above that outputs substantially cheaper American options than the same parameter European ones (american premium column being negative). Is this an artefact of the FD scheme or there's another explanation for it ?
More often than one might think the value of an American option is equal to the European option. In your comparison you have measured the difference between the analytic Black-Scholes formula and a numerical finite difference schema. The "negative American premium" is the small inaccuracy (avg ~3e-4, have you used the implicit schema?) of the numerical method due to finite grids. The true American premium for these option is zero. Use large grids to reduce the figures even more or remove all options from your example set, which won't take advantage from an early exercise.
Yep using the implicit with > 1000 sizes. I was digging deeper into how the FD code works an I had a few questions, hoping you could shed some light as there's 0 comments. @klausspanderen This is all for American Vanilla options
Seems like for FDVanillaBlackScholes there's something like the Kim method being done by the boundary condition being estimated with an integral solver. I see a class called FDAvgCellValue that uses the Simpson method. Seems to borrow this idea https://demonstrations.wolfram.com/KimsMethodWithNonuniformTimeGridForPricingAmericanOptions/ and then runs bicgstab solver on top. Why do we do the boundary estimate to begin with when even the author says the solution of the integral likely outweighs all the benefits? Do you know any good reason of doing this vs something like a more traditional solver like Gauss Siedel approach?
Is there any reason that interpolation is being used for American when the grid sizes need to be very fine anyways, does it not just slow you down for no real gain?
In the Heston case the same condition matrix is calculated with this avg cell value method, but it's basically empty - only contains one value, which kind of makes sense as I'm not sure how would you estimate the boundary for Heston (so in this case is this condition estimator just sitting there for consistency purposes in Heston).
Just trying to wrap my head around it before writing some docs as it's not using the typical tridiagonal approach for the BS PDE as far as I can tell.
Thanks!
Hi
Thanks fo the info:
Cab you maybe point me in the right direction or correct my understanding below as to what hapens in what files:
I went through the code with the debugger but it's still very confusing because of all the abstractions.
w.r.t. 1: Cell-averaging means to calculate the average of the final payoff at the first backward step for every grid cell. The code is generic and should work for all payoffs, hence the average is calculate via numerical integration. But this is only done once for the very first backward step and does not take long. Cell-averaging of the initial payoff is very effective in reducing numerical glitches, definitely worth the effort! Sure, you can make the implementation payoff dependent (e.g. call/put) and you can get ride of the numerical integration and replace it by the analytical solution.
w.r.t. 4: The bigstab algorithm is only used for higher dimensional problems, e.g. implicit scheme for the Heston model because here the matrix is no longer tri-diagonal. In the one dimensional BS case the matrix is tri-diagonal and the usual Thomson algorithm is used to solve the linear system. The cell averaging does not come into play here anymore (only used to map the option payoff at maturity onto the grid). Boundary conditions are implied in the definition of the matrix, here the so called free boundary condition.
wrt 1 - Thanks a lot that clarifies perfectly
wrt 4 - Im not clear how can you apply thomas for a path dependend option like American, I only see it applicable for European? But when I step through the code for BS american pricer it goes hit the Bicgstab solver, that's why im wondering how is the penalty defined, because the only reference I see in applying any of the shortcut solvers like BicGstab to american is by introducing a penalty term (ref. p 13 - https://cs.uwaterloo.ca/research/tr/2006/CS-2006-09.pdf ). So i was wondering where is that term introduced in the QL code I couldn't find it and I can't see another way of solving this problem, it either has to have the penalty or has to have the analytical boundary from Kim's method. Mind you my understanding of PDEs is limited.
Thanks for all the feedback.
The BS solver does not use the BiCGSTAB algorithm. The BiCGSTAB algorithm is only used for higher dimensional problems within the implicit schema, e.g. the Heston model. Independent of the solver (Thomas or BiCSTAB), American options are priced using "dynamic programming" together with PDEs to solve the early exercise problem. The dynamic programming step is encoded in FdmAmericanStepCondition.
@klausspanderen Thanks a lot, it's all clear now, this explains why you suggested a relatively large timestep for American, under this scheme the errors seem to converge to an iterative solution for fine grids only.
My head exploded reading this thread (too much good knowledge)!
Here are some screenshots of parameters and the corresponding impossible gamma and vega (very negative) values which makes me question the rest of the greeks as well. I'm attaching the full scheme used to price using those parameters.