|
From: Dan V. <dan...@gm...> - 2022-10-17 14:13:32
|
Thank you Jonathan.
My example was on SPX to make it easier to read but I do need the
FdBlackScholesVanillaEngine for american options on dividend paying single
stocks and I want to make sure that the convergence between american and
european remains pure hence the checking.
Otherwise for SPX, you are correct that analytic is cleaner and faster. In
fact we even use the regular analytic engine without dividend and instead
we "yieldify" the dividends to pass them in the process instead which makes
it even faster.
In any case it would be great to get proper thetas on single stocks
american options and it looks like it's not possible at the moment.
On Sat, Oct 15, 2022 at 8:02 AM Jonathan Sweemer <sw...@gm...> wrote:
> 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
>>
>
|