|
From: Jonathan S. <sw...@gm...> - 2022-10-18 12:27:24
|
I'm not sure what your use case is, but again I would stress that "proper" thetas for short-dated options should probably not rely on Black-Scholes pricing assumptions. You can look for articles on modeling short-dated SPX skew to get some ideas for a better approach. If you must use a constant vol surface with American options, then you might also want to check out BaroneAdesiWhaleyApproximationEngine, but be aware that it only provides theta for call options <https://github.com/lballabio/QuantLib/blob/master/ql/pricingengines/vanilla/baroneadesiwhaleyengine.cpp#L185>, not for put options, and it only works with VanillaOption, not DividendVanillaOption (which should be fine for you since you use a continuous dividend yield). On Mon, Oct 17, 2022 at 11:13 PM Dan VolPM <dan...@gm...> wrote: > 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 >>> >> |