Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heston analytics #30

Merged
merged 2 commits into from
May 10, 2020
Merged

Conversation

fabrice-deschatres
Copy link
Contributor

I have added Heston semi-analytical formula for European options.

I've put the analytics in tf_quant_finance/volatility/heston_approximation.py as discussed in #9. But contrary to what the path name suggests this isn't an approximation (this is exact) and this doesn't calculate volatility (it calculates prices, but volatility could certainly be backed out from the prices using a solver).

The Fourrier transform is done using tff.math.integration.integrate, which relies on Simpsons integration, which can lead to significant numerical error (when compared to scipy.integrate.quad). The unittest passes with the current tested parameters. But it would fail for different set of parameters (ex. non zero correlation). Better numerical integration would be needed in tensorflow, and the Heston analytics provides an interesting test case (integral of a function from zero to +infty which diverges in zero).

Copy link
Contributor

@cyrilchim cyrilchim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Fabrice! Sorry for the slow review.

http://faculty.baruch.cuny.edu/lwu/890/Heston93.pdf
"""

def heston_price(*,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please create a folder in models and call it heston, like we did for hull white. Then move the heston model into that folder. Inside heston you should create approximations folder like we did for Black-Scholes (see here). You should put this file there. Thanks!

Also, should we call this function eu_option_price? User then can access it via tff.models.heston.approximations.eu_option_price

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I have renamed module/folder accordingly

Comment on lines 88 to 89


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many blank lines. One is enough

thetas=None,
sigmas=None,
rhos=None,
variances=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please make the arguments in a similar order as in option_price for consistency? I.e., move variance to the top and forwards right after spots?
Also, is it possible to add discount_rates and dividend yield?

Copy link
Contributor Author

@fabrice-deschatres fabrice-deschatres Apr 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reordered arguments a little bit, but may not be exactly what you were asking. I think in general it makes sense to group on one side the option contract parameters together and on the other side the model parameters. But agree with you that keeping consistency with other functions like the black-scholes option price needs to be kept in mind.

I have added spots, discount rates, continuous dividends like in black scholes option prices, but duplicating a lot of code along the way, so I am not sure if the extra flexibility is worth it. I guess moving the spot/discount factor/dividends/discount rate/... to a single place would help, and ideally make it object oriented, but that depends on how the code is going to be used in practise, so I can't comment on that.

)

# Expected print output of prices:
# [24.82219619]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the result not of the same shape as kappas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, the docstring was incorrect

return tf.math.exp(C + D * variances_real)

def integrand_function(u, k):
""" Note that with Attari, integrand is in 1 / u**2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[2] instead of Attari

Comment on lines 197 to 205
""" WARNING ABOUT CURRENT NUMERICAL ISSUES WITH CURRENT INTEGRATION METHOD
PLEASE READ:

The integral should be performed from 0 to +infty if we could integrate
with a tensorflow equivalent of scipy.integrate.quad, but this isn't
currently available:
- lower bound has to be strictly positive as integrand not defined in 0 as
current integration method can't deal with that
- upper bound has to be finite as current integration method can't deal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be part of the docstring

Comment on lines 28 to 30
theta=None,
sigma=None,
rho=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename to kappas, thetas, .. for consistency

Copy link
Contributor Author

@fabrice-deschatres fabrice-deschatres Apr 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be consistent with the other function, but wouldn't reflect what that function is doing though as that one is not vectorized. Wouldn't it better to leave like that?

for strike in strikes:
for discount_factor in discount_factors:
tff_prices = self.evaluate(
tff.volatility.heston_price(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't heston_price vectorized?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is, but not the testing function, so I figured it was easier to do the test like that

import numpy as np
import tensorflow.compat.v2 as tf
from absl.testing import parameterized
from scipy import integrate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid using scipy as it fails some of the internal tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, there may be other python packages that can do that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had some issues with scipy before and made a decision to not include it as a dependency unless it is needed inside the library itself.

The test should be for the code inside our library and not for scipy. You should use scipy to get the expected result and test the output against that. See, e.g., conjugate gradient test:

Here the minimum value was computed using scipy but it is not computed inside the test.

import tf_quant_finance as tff


def get_heston_prices(kappa=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, compute the correct prices and test for those.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes obviously... but, for that we'll need to wait to have an adaptive gaussian quadrature implemented for performing integrals in tensorflow

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see the comment above.

Copy link
Contributor

@cyrilchim cyrilchim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, Fabrice!

Just a few more minor comments

Comment on lines +51 to +55
variances=None,
kappas=None,
thetas=None,
sigmas=None,
rhos=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to move model parameters to the top?

'may be supplied')


with tf.compat.v1.name_scope(name, default_name='eu_option_price'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are importing TF2, this one can be replaced with
tf.name_scope(name or 'eu_option_price')

Comment on lines +238 to +241
"""Using 'second formula' for the (first) characteristic function of
log( spot_T / forwards )
(noted 'phi_2' in 'The Little Heston Trap', (Albrecher))
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docstring is a bit hard to understand. What is 'second formula'? Also, [1] instead or Albrecher.

Also, the docstring should start with a single line followed by a blank space with args description. Since, this is an internal function, the easy way is to have a 1-line dosctring and then a comment about what you are trying to achieve.

    def char_fun(u):
      """Characteristic function of `log( spot_T / forwards )`."""\
      # The characteristic function is given in [1]. The notations are as in the paper.

a = kappas_real * thetas_real
h = g * tf.math.exp(-d * expiries_real)
m = 2 * tf.math.log((1 - h) / (1 - g))
C = (a / sigmas_real ** 2) * ((kappas_real - s - d) * expiries_real - m)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usually we are not allowed to use 1-symbol variables unless they are the same as in the reference paper. So I trust you this is the case.
Unfortunately, you can't use capital letters for variable names (e.g., "C" or "D"). Please come up with a sensible replacement

Comment on lines +308 to +326
if __name__ == "__main__":
import os

os.environ["CUDA_VISIBLE_DEVICES"] = "-1"

import numpy as np
prices = eu_option_price(
variances=np.asarray([[0.11]]),
strikes=np.asarray([[102.0]]),
expiries=np.asarray([[1.2]]),
forwards=np.asarray([[100.0]]),
is_call_options=np.asarray([True], dtype=np.bool),
kappas=np.asarray([[2.0]]),
thetas=np.asarray([[0.5]]),
sigmas=np.asarray([[0.15]]),
rhos=np.asarray([[0.3]]),
discount_factors=np.asarray([[1.0]]),
)
print(prices)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is a debugging leftover? Please remove

Comment on lines +154 to +156
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed

Comment on lines +106 to +139
for kappa in kappas:
for theta in thetas:
for sigma in sigmas:
for rho in rhos:
for v0 in v0s:
for forward in forwards:
for expiriy in expiries:
for strike in strikes:
for discount_factor in discount_factors:
tff_prices = self.evaluate(
tff.models.heston.approximations.eu_option_price(
kappas=np.asarray([kappa]),
thetas=np.asarray([theta]),
sigmas=np.asarray([sigma]),
rhos=np.asarray([rho]),
variances=np.asarray([v0]),
forwards=np.asarray([forward]),
expiries=np.asarray([expiriy]),
strikes=np.asarray([strike]),
discount_factors=np.asarray([discount_factor]),
is_call_options=np.asarray([True, False], dtype=np.bool)
))

params = {
"kappa": kappa,
"theta": theta,
"sigma": sigma,
"rho": rho,
"v0": v0,
"forward": forward,
"expiry": expiriy,
"strike": strike,
"discount_factor": discount_factor,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit dense. I have addressed our previous conversation. Could you please remove get_heston_prices and just hardcode the exact answer?

As for me, computing tff.models.heston.approximations.eu_option_price(kappas=kappas, ..) is better (making sure that the inputs are broadcastable). It also checks that the vectorization is supported.

@@ -74,13 +74,15 @@ def _ensure_tf_install(): # pylint: disable=g-statement-before-imports
from tf_quant_finance import math
from tf_quant_finance import models
from tf_quant_finance import rates
from tf_quant_finance import volatility
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this module exists?

@cyrilchim cyrilchim merged commit 6130a23 into google:master May 10, 2020
cyrilchim pushed a commit that referenced this pull request May 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants