|
From: Jonathan S. <sw...@gm...> - 2022-10-15 12:02:40
|
Looks like it's due to some numerical error in
the FdBlackScholesVanillaEngine. If I replace FdBlackScholesVanillaEngine
with AnalyticDividendEuropeanEngine in your code then I get 0.37 as the
breakeven vol for both the constant vol case as well as the vol surface
case as expected.
Is there any reason that you need to use the FdBlackScholesVanillaEngine?
If not then I suppose you can use the AnalyticDividendEuropeanEngine
instead. But if your question is what is causing the numerical error in the
FdBlackScholesVanillaEngine then that will require more debugging.
I'm sure you know already, but a constant vol surface is not suitable for
real-world pricing, especially for short maturities where the skew in SPX
is most extreme. You'll want to try out other pricing engines to obtain
meaningful results on real-world data.
On Tue, Oct 11, 2022 at 11:31 PM Dan VolPM <dan...@gm...> wrote:
> Hi,
>
> I am running into an issue where quantlib outputs different thetas whether
> i price with a blackvariancesurface or with the constant vol extracted from
> the surface.
>
> I have a vol surface from the market on the SPX index. Not all
> strikes/maturities are populated. In addition, there could potentially be
> arbitrages in short maturities since we have lots of daily options and I am
> not correcting yet for arbitrages.
>
> Yet when building a BlackVarianceSurface with this data and price a
> vanilla option, I expect that option to be priced with a constant vol
> interpolated from the grid.
>
> When I pass the surface object to the pricer I am getting a very different
> theta (and therefore breakeven vol) from simply passing the constant vol
> taken from the surface.
>
> Could you help me understand exactly how QLIB handles the surface and how
> is theta computed ?
>
> Thank you very very much.
>
> Here's some sample code that compares a constant vol with a surface vol
> pricing. Notice how the theta changes between the 2 cases leading to a very
> different break even vol.
>
>
> import pandas as pd, QuantLib as ql, math, numpy as npfrom datetime import datetime
>
>
> def from_YYYYMMDD_to_ql_date(ymd):
> ymd = int(ymd)
> year = math.floor(ymd / 10000)
> month = int(ymd / 100) % 100
> day = ymd % 100
>
> ql_date = ql.Date(day, month, year)
> return ql_date
>
>
> def run_test():
>
> ####################################################################
> #### Global Variables
> ####################################################################
> pricing_date = datetime.strptime('20220916', '%Y%m%d')
> ql_pricing_date = ql.Date(16, 9, 2022)
> calendar = ql.UnitedStates(ql.UnitedStates.Settlement)
> day_counter = ql.ActualActual()
> spot = 3876.00
> spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot))
> flat_ts = ql.YieldTermStructureHandle(ql.FlatForward(ql_pricing_date, 0, day_counter))
> dividend_yield = ql.YieldTermStructureHandle(ql.FlatForward(ql_pricing_date, 0, day_counter))
> K = 3800
> ql_date_expiry = ql.Date(16, 12, 2022)
> exercise = ql.EuropeanExercise(ql_date_expiry)
> payoff = ql.PlainVanillaPayoff(ql.Option.Put, strike=K)
>
>
> ####################################################################
> #### Build Vol surface
> ####################################################################
> list_opts = []
> expirations = [datetime.strptime('20220921', '%Y%m%d'), datetime.strptime('20221118', '%Y%m%d'), datetime.strptime('20221216', '%Y%m%d'), ]
> list_opts.append([pricing_date, expirations[0], 3800., 45.])
> list_opts.append([pricing_date, expirations[0], 3900., 40.])
>
> list_opts.append([pricing_date, expirations[1], 3800., 27.])
> list_opts.append([pricing_date, expirations[1], 3900., 25.])
>
> list_opts.append([pricing_date, expirations[2], 3800., 37.])
> list_opts.append([pricing_date, expirations[2], 3900., 31.])
>
> df = pd.DataFrame(list_opts, columns=['date', 'maturity', 'strike', 'vol'])
>
> df['ymd'] = df['maturity'].dt.strftime('%Y%m%d')
> df['q_exp_dates'] = df['ymd'].apply(from_YYYYMMDD_to_ql_date)
>
> strikes = sorted(df.strike.unique())
> expirations = sorted(df['q_exp_dates'].unique())
>
> df['strike_idx'] = df['strike'].apply(lambda x: strikes.index(x))
> df['expiry_idx'] = df['q_exp_dates'].apply(lambda x: expirations.index(x))
>
> volMat_np = np.full((len(strikes), len(expirations)), np.nan)
>
> for strk, exp, vol in zip(df['strike_idx'], df['expiry_idx'], df['vol']):
> volMat_np[strk][exp] = vol / 100
>
> df_vals = pd.DataFrame(volMat_np, index=strikes, columns=expirations)
> print(df_vals)
>
> ql_Matrix = ql.Matrix(len(strikes), len(expirations), np.nan)
> for i in range(len(strikes)):
> for j in range(len(expirations)):
> ql_Matrix[i][j] = volMat_np[i, j]
>
>
> ####################################################################
> #### We build 1 vol surface and a constant vol = BlackVol(surface)
> ####################################################################
>
> vol_process = ql.BlackVarianceSurface(ql_pricing_date, calendar, expirations, strikes,
> ql_Matrix, day_counter)
>
> option_vol = vol_process.blackVol(ql_date_expiry,K)
> constant_vol = ql.BlackConstantVol(ql_pricing_date, calendar, option_vol, day_counter)
>
> ####################################################################
> #### Build Process
> ####################################################################
>
> process_surface = ql.BlackScholesMertonProcess(spot_handle, dividendTS=dividend_yield, riskFreeTS=flat_ts,
> volTS=ql.BlackVolTermStructureHandle(vol_process))
>
> process_constant = ql.BlackScholesMertonProcess(spot_handle, dividendTS=dividend_yield, riskFreeTS=flat_ts,
> volTS=ql.BlackVolTermStructureHandle(constant_vol))
>
>
> ####################################################################
> #### Build Option to price
> ####################################################################
> option_surface = ql.DividendVanillaOption(payoff, exercise, [], [])
> engine_surface = ql.FdBlackScholesVanillaEngine(process_surface, 200, 200)
> option_surface.setPricingEngine(engine_surface)
>
> option_constant = ql.DividendVanillaOption(payoff, exercise, [], [])
> engine_constant = ql.FdBlackScholesVanillaEngine(process_constant, 200, 200)
> option_constant.setPricingEngine(engine_constant)
>
> prc_cst = option_constant.NPV()
> gamma_cst = option_constant.gamma()
> theta_cst = option_constant.theta() / 252. # biz day theta
> vol_be_sqrt252_cst = round(math.sqrt(-2. * theta_cst / (gamma_cst * spot ** 2)) * math.sqrt(252),4) # calculate theta-gamma break even vol to estimate what vol is being used by quantlib
> print(f'Option details')
> print(f'-------------------------------------------------')
> print(f'Maturity: {ql_date_expiry}')
> print(f'Strike: {K}')
> print(f'Vol from surface: {option_vol}')
> print(f'')
> print(f'-------------------------------------------------')
> print(f'Constant vol case')
> print(f'-----------------')
> print(f'Option price: {prc_cst}')
> print(f'Option gamma: {gamma_cst}')
> print(f'Option theta: {theta_cst}')
> print(f'Break even Vol = {vol_be_sqrt252_cst} should be {option_vol}')
>
> prc_surface = option_surface.NPV()
> gamma_surface = option_surface.gamma()
> theta_surface = option_surface.theta() / 252. # biz day theta
> vol_be_sqrt252_surface = round(math.sqrt(-2. * theta_surface / (gamma_surface * spot ** 2)) * math.sqrt(252),4) # calculate theta-gamma break even vol to estimate what vol is being used by quantlib
> print(f'-------------------------------------------------')
> print(f'Vol Surface case')
> print(f'----------------')
> print(f'Option price: {prc_surface}')
> print(f'Option gamma: {gamma_surface}')
> print(f'Option theta: {theta_surface}')
> print(f'Break even Vol = {vol_be_sqrt252_surface} should be {option_vol}')
>
>
> if __name__ == '__main__':
> run_test()
>
>
>
> We get the following results:
>
> September 21st, 2022 November 18th, 2022 December 16th, 2022
> 3800.0 0.45 0.27 0.37
> 3900.0 0.40 0.25 0.31
> Option details
> -------------------------------------------------
> Maturity: December 16th, 2022
> Strike: 3800
> Vol from surface: 0.37
>
> -------------------------------------------------
> Constant vol case
> -----------------
> Option price: 246.10555275972283
> Option gamma: 0.0005462073912642451
> Option theta: -2.2348817578128877
> Break even Vol = 0.3705 should be 0.37
> -------------------------------------------------
> Vol Surface case
> ----------------
> Option price: 246.10595637993455
> Option gamma: 0.0005462035022605052
> Option theta: -3.3101198905503284
> Break even Vol = 0.4509 should be 0.37
>
> If you change the vol surface to something which is not arbitrable but
> still has high very short term vol, the problem persists and even
> accentuates. In all cases the difference appears on the theta. The price of
> the option does not change.
>
> list_opts.append([pricing_date, expirations[0], 3800., 45.])
> list_opts.append([pricing_date, expirations[0], 3900., 40.])
>
> list_opts.append([pricing_date, expirations[1], 3800., 27.])
> list_opts.append([pricing_date, expirations[1], 3900., 25.])
>
> list_opts.append([pricing_date, expirations[2], 3800., 27.])
> list_opts.append([pricing_date, expirations[2], 3900., 25.])
>
> We get the following:
>
> Option details
> -------------------------------------------------
> Maturity: December 16th, 2022
> Strike: 3800
> Vol from surface: 0.27
>
> -------------------------------------------------Constant vol case
> -----------------
> Option price: 170.495477037466
> Option gamma: 0.0007462620578669159
> Option theta: -1.6258437988242298Break even Vol = 0.2703 should be 0.27
> -------------------------------------------------Vol Surface case
> ----------------
> Option price: 170.49560335893932
> Option gamma: 0.0007462597949764497
> Option theta: -4.538109308076177
> Break even Vol = 0.4517 should be 0.27
>
>
> Thank you very much
>
> _______________________________________________
> QuantLib-users mailing list
> Qua...@li...
> https://lists.sourceforge.net/lists/listinfo/quantlib-users
>
|