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()