Gold Options Volatility Skew
Contents
Gold Options Volatility Skew#
New user? The best way to learn SigTech is to follow the steps in our user guide.
Restricted data: You will only have access to the data used in this notebook if your organisation has specifically purchased it. To check your current data access, view Data. If you would like to access more data, please contact sales@sigtech.com.
Changes will not be saved: Edits made to SigTech’s example notebooks like this will only persist for the duration of your session. When you restart the research environment, this notebook will have been restored to its original state, and any changes you have made will have been lost. To make permanent changes, copy and paste the content to a new notebook in one of your workspaces.
Introcution#
This notebook showcases how relative changes in volatility skew for Gold options can be used to construct a trading strategy in the SigTech Platform.
Environment#
This section imports the relevant internal and external libraries, and sets up the platform environment.
[ ]:
import sigtech.framework as sig
import uuid
import numpy as np
import pandas as pd
import seaborn as sns
import datetime as dtm
sns.set(rc={'figure.figsize': (18, 6)})
env = sig.init(env_date=dtm.date(2020, 9, 2), log_level='ERROR')
Universe#
A delta hedged 3 month 25 delta risk reversal instrument will be created using the platform’s building blocks.
To price our options basket, a function is defined to return the vol surface of commodity options with tenors closest to three months from date
, and deltas closest to a set of pre-defined delta_pillars
.
[ ]:
sig.obj.get('GC COMDTY VOLSURFACE').time_interpolate=True
[ ]:
def get_commodity_option_surface(
date: dtm.date,
group_ticker: str,
delta_pillars: list[float] = [-0.25, 0.25]
) -> pd.DataFrame:
# Get vol surface data
group = sig.obj.get(group_ticker)
surface_obj = group.get_vol_surface().get_handle(date)
surface_df = surface_obj.market_quotes
# Get the tenor date closest to 3M from date
three_months = date + dtm.timedelta(3 * 30)
surface_tenor_dates = [d for d in surface_df.index.unique() if d.date() > date]
closest_date = min(surface_tenor_dates, key=lambda d: abs(d.date() - three_months))
# Get the delta for each option on the surface
surface = {closest_date: {}}
for tenor_date, row in surface_df.loc[closest_date].iterrows():
for opt in ('Call', 'Put'):
option = group.get_option(opt, row['strike'], date, tenor_date.date())
delta = option.metrics("Delta", data_dates=[date]).iloc[0, 0]
surface[tenor_date][delta] = (row['vol'], row['strike'])
surface_df = pd.DataFrame.from_dict(surface).sort_index()
# Get options with delta closest to the defined pillars
filtered_surface = {}
for tenor_date in surface_df.columns:
filtered_surface[tenor_date] = {}
for delta in delta_pillars:
filtered_surface[tenor_date][delta] = surface_df[tenor_date].asof(delta)
filtered_surface = pd.DataFrame.from_dict(filtered_surface)
return filtered_surface
[ ]:
get_commodity_option_surface(dtm.date(2020, 9, 1), "GC COMDTY OTC OPTION GROUP", [-0.25, 0.25])
The risk reversal (long call, short put) is constructed via the DynamicOptionsStrategy
building block.
We create a callback method, which will get called by the strategy for each rebalance date to define a basket of options to trade. This method accepts strategy, decision time and positions as inputs, and returns a dictionary of option names.
Exchange traded data can be misrepresented by high/low trading volume, therefore the OTC vol surface is used as a more accurate and smoother data source to price our options. The basket creation method will use the get_commodity_option_surface
function defined above to calculate 3M 25D call and put strikes. These strikes calculated from the OTC vol surface are used by the exchange traded get_option
method to return the listed options with the closest strike and 3M tenor. Finally, the
unpriced listed options are converted into OTC versions, priced by the vol surface via model_priced_option
.
[ ]:
def create_risk_reversal_basket(strategy, dt, positions):
"""Creates a 3M 25D risk reversal and returns the option names we want to trade."""
surface = get_commodity_option_surface(dt.date(), 'GC COMDTY OTC OPTION GROUP')
call_strike = surface.loc[0.25][0][1]
put_strike = surface.loc[-0.25][0][1]
size_date = strategy.size_date_from_decision_dt(dt)
call = strategy.group.get_option("Call", call_strike, size_date, '3M')
put = strategy.group.get_option("Put", put_strike, size_date, '3M')
# Get vol surface OTC priced version of option
surface_call = call.model_priced_option(start_date=size_date, imply_vol_from_price=False)
surface_put = put.model_priced_option(start_date=size_date, imply_vol_from_price=False)
return {surface_call: 1, surface_put: -1}
To build the risk reversal, the create_risk_reversal_basket
callback is passed to the basket_creation_method
parameter. We specify a notional of USD 100,000 through the target_type
and initial_cash
parameters. close_out_at_roll
True
automatically closes out all positions on the roll dates, which are specified by rolling_frequencies
.
[ ]:
risk_reversal = sig.DynamicOptionsStrategy(
currency='USD',
start_date=dtm.date(2020, 1, 6),
group_name='GC XCME FUTURES OPTION GROUP',
rolling_frequencies=['1W-MON'],
target_type='SpotNotional',
target_quantity=100000,
basket_creation_method=create_risk_reversal_basket,
close_out_at_roll=True,
include_trading_costs=False,
initial_cash=100000,
total_return=False,
db_ticker=f'DAILY GC 3M 25D RISK REVERSAL {str(uuid.uuid4())[:4]}'
)
The risk reversal is delta hedged by wrapping it in the DeltaHedingStrategy
building block. hedging_instrument_type="UNDERLYING"
is specified to delta hedge using the actual underlying of the options. The delta_threshold
is the minimum difference between held delta and current delta (in delta terms) needed to trigger a rebalance.
[ ]:
dh_risk_reversal = sig.DeltaHedgingStrategy(
currency="USD",
start_date=risk_reversal.history_start_date(),
option_strategy_name=risk_reversal.name,
option_group_name='GC COMDTY OTC OPTION GROUP',
include_option_strategy=True,
delta_multiplier=-1,
hedging_instrument_type="UNDERLYING",
hedging_frequency="1BD",
delta_threshold=0,
include_trading_costs=False,
total_return=False,
initial_cash=100000
)
[ ]:
dh_risk_reversal.history().plot(label=dh_risk_reversal.name, legend=True);
risk_reversal.history().plot(label=risk_reversal.name, legend=True);
Strategy#
This mean reversion strategy will go long/short the instrument created above, based on the rolling Z score of volatlity skew between the 25D call and 25D put. We reuse our get_commodity_option_surface
method to calculate skew for each date in the strategy.
[ ]:
skew = {}
for d in sig.obj.get('GC COMDTY VOLSURFACE').history_dates():
if d >= pd.Timestamp(risk_reversal.history_start_date()):
surface = get_commodity_option_surface(d.date(), 'GC COMDTY OTC OPTION GROUP', [-0.25, 0.25])
skew[d] = surface.loc[0.25][0][0] - surface.loc[-0.25][0][0]
[ ]:
df = pd.Series(skew).to_frame('Skew')
df['RollingStd'] = df['Skew'].rolling(window=30).std()
df['RollingMean'] = df['Skew'].rolling(window=30).mean()
df['RollingZ'] = (df['Skew'] - df['RollingMean']) / df['RollingStd']
df = df.dropna()
def z_conditions(z):
if z > 1:
return -1
elif z < -1:
return 1
else:
return 0
func = np.vectorize(z_conditions)
df['Signal'] = func(df["RollingZ"])
df.head()
[ ]:
df[['Skew']].plot()
df[['Signal']].plot()
The signal column is renamed to the instrument to trade and the dataframe is used to create a signal object using the sig.signal_library.from_ts
method. Note that the dataframe must be indexed by date.
[ ]:
signal_df = df[['Signal']].rename(columns={'Signal': dh_risk_reversal.name})
signal_obj = sig.signal_library.from_ts(signal_df)
signal_df.head()
Portfolio#
To build our portoflio, the SignalStrategy
building block is used. By default, the identity
allocation function is used, which returns the unchanged allocations as given by the signal strategy output.
[ ]:
skew_strategy = sig.SignalStrategy(
start_date=risk_reversal.history_start_date(),
currency=dh_risk_reversal.currency,
signal_name=signal_obj.name,
use_signal_for_rebalance_dates=True,
convert_long_short_weight=False,
include_trading_costs=False,
total_return=False
)
[ ]:
skew_strategy.history().plot(title=skew_strategy.name);
Performance#
[ ]:
sig.PerformanceReport(pd.concat({'Gold Options Skew Strategy': skew_strategy.history(), 'Delta Hedged Risk Reversal': dh_risk_reversal.history()}, axis=1),
cash=sig.CashIndex.from_currency('USD')).report()