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

BSEuropeanBinaryOption.gamma returns nan #528

Open
masanorihirano opened this issue Mar 8, 2022 · 7 comments
Open

BSEuropeanBinaryOption.gamma returns nan #528

masanorihirano opened this issue Mar 8, 2022 · 7 comments
Labels
black-scholes Related to Black-Scholes model bug Something isn't working

Comments

@masanorihirano
Copy link
Collaborator

from pfhedge.instruments import EuropeanBinaryOption
from pfhedge.nn import BSEuropeanBinaryOption
from pfhedge.instruments import BrownianStock
from pfhedge.instruments import EuropeanOption
from pfhedge.nn import BSEuropeanOption

derivative = EuropeanBinaryOption(BrownianStock())
m = BSEuropeanBinaryOption.from_derivative(derivative)

torch.manual_seed(42)
derivative.simulate(n_paths=1)

m.gamma(derivative.log_moneyness(), derivative.time_to_maturity(), derivative.underlier.volatility,)
# tensor([[  -3.5248,  -44.2232,  -66.3963,  -13.1013,  -38.5988,  -75.9209,
#           -98.5359, -103.0389, -101.6409, -122.1414, -140.5751, -146.6521,
#         -182.5579, -207.5661, -243.5686, -227.4299, -280.6607, -374.9843,
#         -143.4726, -121.1908,       nan]])

# c.f.)
derivative2 = EuropeanOption(BrownianStock())
m2 = BSEuropeanOption.from_derivative(derivative)

m2.gamma(derivative.log_moneyness(), derivative.time_to_maturity(), derivative.underlier.volatility,)
tensor([[7.0495, 6.6380, 6.0732, 7.5931, 7.5346, 6.8515, 6.0366, 3.7168, 3.1474,
         4.0439, 4.8103, 4.2532, 6.5935, 6.4921, 7.2021, 4.2133, 4.5933, 5.3033,
         1.0448, 0.5398, 0.0000]])

Is it correctly working?

Returning tensor including nan seems a little bit harmful. (But, I haven't face this issue and have noticed when I make additional tests.)

@masanorihirano
Copy link
Collaborator Author

m.gamma(torch.tensor(1.0), torch.tensor(0.0), torch.tensor(0.1))
# tensor(nan)
m.gamma(torch.tensor(1.0), torch.tensor(1e-7), torch.tensor(0.1))
# tensor(-0.)

I think it should be 0

@masanorihirano
Copy link
Collaborator Author

It seems error of autogreek

m.delta(log_moneyness=torch.tensor(1.0), time_to_maturity=torch.tensor(0), volatility=torch.tensor(0.0))
# tensor(0.)

from pfhedge.autogreek import delta
delta(m.price, log_moneyness=torch.tensor(1.0), time_to_maturity=torch.tensor(0), volatility=torch.tensor(0.0))
# tensor(nan)

@ghost ghost added black-scholes Related to Black-Scholes model bug Something isn't working labels Mar 9, 2022
@ghost
Copy link

ghost commented Mar 15, 2022

Update: This is due to the anomalous gradient of normal cdf and/or d2.

The BS European binary price is given by ncdf(d2):

def bs_european_binary_price(
log_moneyness: Tensor,
time_to_maturity: Tensor,
volatility: Tensor,
call: bool = True,
) -> Tensor:
"""Returns Black-Scholes price of a European binary option.
See :func:`pfhedge.nn.BSEuropeanBinaryOption.price` for details.
"""
s, t, v = broadcast_all(log_moneyness, time_to_maturity, volatility)
price = ncdf(d2(s, t, v))
price = 1.0 - price if not call else price # put-call parity
return price

Gradient of d2 returns inf and gradient of ncdf(d2) returns nan.

import torch
from pfhedge.nn.functional import ncdf
from pfhedge.nn.functional import d2
from pfhedge.nn.functional import bs_european_binary_price
import pfhedge.autogreek as autogreek


s = torch.tensor(1.0)
t = torch.tensor(0.0)
v = torch.tensor(1.0)

price = bs_european_binary_price(s, t, v)
print(price)
# tensor(1.)

d2_grad = autogreek.delta(
    d2,
    strike=1.0,
    log_moneyness=s,
    time_to_maturity=t,
    volatility=v,
)
print(d2_grad)
# tensor(inf)

delta = autogreek.delta(
    bs_european_binary_price,
    strike=1.0,
    log_moneyness=s,
    time_to_maturity=t,
    volatility=v,
)
print(delta)
# tensor(nan)

@ghost
Copy link

ghost commented Mar 15, 2022

d2_grad = inf seems to be correct behavior: grad d2 at t=0 is indeed d(logS) / 0=+inf.
The problem boils down to computing grad ncdf for inf (supposed to be 0)

@ghost
Copy link

ghost commented Mar 15, 2022

Now I see the cause.

The pricing function is ncdf(d2(s, t=0)) and we want its gradient wrt s (Technically wrt exp(s) but does not really matter).
The gradient is given by npdf(d2) * d2' = 0 * inf = nan.

@ghost
Copy link

ghost commented Mar 15, 2022

Thus the grad of ncdf(d2(s, t=0)) being nan is a "correct" consequence of computing grad at d2=inf.
Similar problems would continue to pop up as long as we allow x/0 = inf to sneak in d1/d2.

There are two ways to take care of this:

  1. Keep x/0 = inf: Pro is that this is "mathematically correct", con is that we would get nan
  2. "Regularize" so that x/0 -> x / epsilon = (large but finite number): Not mathematically correct but no nan anymore.

I'm not sure which is better. Any thoughts? @masanorihirano

@masanorihirano
Copy link
Collaborator Author

According to the design concept of #494, I think

  1. Keep x/0 = inf: Pro is that this is "mathematically correct", con is that we would get nan

is coherent.

If the gradient for them is important, it should be reconsidered.
But, the situation occurring inf is very extreme and it is inappropriate to take gradient and learn.
Moreover, the large but finite numbers can cause another issue: lnf/inf could be a non-large finite number even when it should be inf (for example, lim_{x->inf} x^2/x).
Thus, I think mathematical correctness is primary and we should make other workarounds for avoiding nan in each modules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
black-scholes Related to Black-Scholes model bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant