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

SABR calibration #58

Open
Macfly opened this issue Jul 10, 2021 · 7 comments
Open

SABR calibration #58

Macfly opened this issue Jul 10, 2021 · 7 comments

Comments

@Macfly
Copy link

Macfly commented Jul 10, 2021

Hello,
I'm trying to use the SABR calibration with real BTC option prices but it stops right away without any error message (is_converged is False).
If anyone could help to see what is wrong with this pretty simple code:
Colab notebook (created a wheel with the latest code)

import pandas as pd
import requests
import tf_quant_finance as tff
import time
from datetime import datetime
import numpy

vals = pd.DataFrame(requests.get(
  'https://www.deribit.com/api/v2/public/get_instruments?currency=BTC&expired=false&kind=option').json()['result'])
jul_exp = vals[(vals['expiration_timestamp'] == 1627632000000)]
jul_exp['price'] = [requests.get(f'https://www.deribit.com/api/v2/public/ticker?instrument_name={x}').json()['result']['last_price'] for x in jul_exp['instrument_name']]
jul_exp.dropna(subset=['price'], inplace = True)
underlying = requests.get('https://www.deribit.com/api/v2/public/ticker?instrument_name=BTC-30JUL21').json()['result']
jul_exp['price'] = jul_exp['price'] * underlying['index_price']
is_call = numpy.array([[t == 'call' for t in jul_exp['option_type']]], dtype=numpy.bool)
delta =(( jul_exp.iloc[0]['expiration_timestamp']/1000 - time.time())  / 60 / 60 / 24) / 365
dtype = numpy.float64
models, is_converged, it = tff.models.sabr.approximations.calibration(
    prices=numpy.array([jul_exp['price'].to_numpy()], dtype=dtype),
    strikes=numpy.array([jul_exp['strike'].to_numpy()], dtype=dtype),
    expiries=numpy.array([delta], dtype=dtype),
    forwards=numpy.array([underlying['index_price']], dtype=dtype),
    is_call_options=is_call,
    beta=numpy.array([0.5], dtype=dtype),
    calibrate_beta=True,
    nu=numpy.array([0.1], dtype=dtype),
    nu_lower_bound=0.0,
    nu_upper_bound=10.0,
    rho=numpy.array([0.0], dtype=dtype),
    rho_lower_bound=-0.75,
    rho_upper_bound=0.75,
    maximum_iterations=1000)
@Macfly Macfly changed the title SBR calibration SABR calibration Jul 10, 2021
@Macfly
Copy link
Author

Macfly commented Jul 10, 2021

found the issue, you need to pass only call or put options to the function, with a mix of both it does not work.
Not sure why as you the downside presision comes from the puts and the upside from the call.
With call and puts, we would be able to fit the forward also.

so if you want to use the code, just replace:
jul_exp = vals[(vals['expiration_timestamp'] == 1627632000000)]
by
jul_exp = vals[(vals['expiration_timestamp'] == 1627632000000) & (vals['option_type'] == 'call')]

@cyrilchim
Copy link
Contributor

cyrilchim commented Jul 10, 2021

Hi @Macfly,

Thanks for reporting. I can see that your prices have Nan values. Can be fixed with something like
prices = numpy.array([jul_exp['price'].to_numpy()], dtype=dtype) prices = tf.where(tf.math.is_nan(prices), dtype(0.0), prices)

Now, calibration fails again. Feel free to file a bug for a more detailed calibration output. Basically, volatility calibration is performed and the underlying volatility fails to compute. You can try setting volatility_based_calibration=False and the procedure will converge.

The fit to the data is very poor. I am guessing that there are some issues with the input data. You should be able to compute implied volatilities with tff.black_scholes.implied_vols as I remember that this function is quite robust.

I am looking at jul_exp.loc[20]

tick_size                              0.0005
taker_commission                       0.0003
strike                                  45000
settlement_period                       month
quote_currency                            BTC
option_type                               put
min_trade_amount                          0.1
maker_commission                       0.0003
kind                                   option
is_active                                True
instrument_name           BTC-30JUL21-45000-P
expiration_timestamp            1627632000000
creation_timestamp              1619690416000
contract_size                               1
block_trade_commission                0.00015
base_currency                             BTC
price                                  0.3075

I see that the price is in BTC but the strike is in USD. You should convert price to USD by multiplying it by the underlying. Next, your expiration is around 20 , which I assume is measured in days. You need to convert that into years (divide by 365). And finally, since this is a put option, even for zero volatility strike - spot > price which, assuming the data is correct, seems to lead to arbitrage. (I estimate price to be 0.3075 * 33402.59 ~ 10271 which is less than strike - spot ~ 11597)

Please let me know if I understood the data correctly.

Best,
Cyril

@Macfly
Copy link
Author

Macfly commented Jul 11, 2021

thanks a lot for your help.
You were right, I was not taking the right spot and forward.
I have changed some stuff and now it is converging.
the working colab if someone want to have a look, plotted the fit price vs the actual price.

But if I take call and put, it is not converging with volatility_based_calibration.

However it is working with price_based_calibration convergence but the fit is not that great:
colab call + put with price calibration

I'll file a bug report to try to understand why the volatility calibration fails for the puts.

Regards,
Corentin

@cyrilchim
Copy link
Contributor

cyrilchim commented Jul 11, 2021

volatility_based_calibration =True calibrates on implied volatilities instead of prices. That means, it calculates tff.black_scholes.implied_vols from the prices and calibrates on those (see here)

As I mentioned above, there is arbitrage in your data. In this case implied volatility is Nan as there is no such value of volatility that produces the option prices. Simply calculate implied vols for your data to see that there Nan values:

tff.black_scholes.implied_vol(
      prices=prices,
      strikes=strikes,
      expiries=expiries,
      forwards=forwards,
      is_call_options=is_call)

I think that this really indicates that you should include transaction fees in your pricing. You need to make sure that the function above does not produce Nans for input values.

Please let me know if this makes sense.

@cyrilchim
Copy link
Contributor

Just played a bit with the colabs. The fit is not perfect, but if you include fee into the quoted price (I read fee to be 3% from the website) as

prices = numpy.array([jul_exp['price'].to_numpy()], dtype=dtype) + underlying['index_price'] * 0.03

then volatility based calibration should work for both call and put options.

@cyrilchim
Copy link
Contributor

As a side comment, if you are willing to run this for streaming data, consider wrapping calibration function into a tf.function and try enabling JIT compilation, like calibration_fn = tf.function(tff.models.sabr.calibration, jit_compile=True) and use those for various inputs (keep input shapes the same though to avoid recompilation). Also, note that we can calibrate a batch of models at the same time (example), which should be useful if you are to run stuff on a GPU.

@Macfly
Copy link
Author

Macfly commented Jul 11, 2021

EDIT: I was making a mistake and taking the wrong mid price (las_price instead of mark_price), now the fit is not bad.

thanks a lot again for the help!
I was thinking about the fee but didn't have time to try before your comments.
Updated colab with vol and price plots

The other thing I am not a fan of is to get the option prices sequentially, I haven’t found a way to get a snapshot yet.
Right now it is more a study phase, but I'll keep your comment in my head once I'll move to live fitting.

image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants