Tutorial: commodity momentum strategy

Beginner tutorial on creating a systematic strategy using futures contracts.

This tutorial shows new SigTech users how to construct a systematic investment strategy where the strategy involves a number of different commodities, and where the investment decision is based on a simple momentum signal.

Learn more: Example notebooks

Workflow overview

When creating an investment strategy on the SigTech platform, the workflow tends to follow these steps:

  1. Set up the environment

  2. Define the investment universe

  3. Create the strategy

  4. Construct the portfolio

  5. Generate the performance report

Environment

Setting up your environment takes three steps:

  • Import the relevant internal and external libraries

  • Configure the environment parameters

  • Initialise the environment

import sigtech.framework as sig

import datetime as dtm
import pandas as pd
import numpy as np
import seaborn as sns

sns.set(rc = {'figure.figsize': (18, 6)})
sig.config.init()

Define the investment universe

In this step, you will define the futures contracts that will form the universe of the investment strategy. When defining the strategy's futures contracts it's important to use each contract's exact ContractCode.

Example: the ContractCode of corn futures is 'C '

Browse SigTech's Market Data to find futures contracts and their corresponding ContractCode.

Note: all futures contracts issued by a particular futures exchange, such as CBOT for the same underlying asset, such as corn, will share the same ContractCode.

# Dictionary of assets and their ContractCode
assets = {
    'Corn Futures (C)': 'C ',
    'Soybean Futures (S)': 'S ',
    'Sugar Futures (SB)': 'SB',
    'Cotton Futures (CT)': 'CT',
    'Live Cattle Futures (LC)': 'LC',
    'Soybean Oil Futures (BO)': 'BO',
    'Gas Oil Futures (QS)': 'QS',
    'Heating Oil Futures (HO)': 'HO',
}

Define the relevant start and end date for the strategy over which the historical performance will be simulated:

start_date = dtm.date(2000, 1, 8)
end_date = dtm.date(2020, 12, 8)

When the investment universe contains futures contracts, it's of high importance to handle the rolling of futures—since a given futures contract has an expiry date, it's crucial to systematically handle how the strategy trades in and out of futures contracts to maintain an exposure to the underlying asset. The SigTech platform offers the RollingFutureStrategy class which allows the user to easily handle the rolling of contracts along a futures strip.

In this tutorial we will create eight RollingFutureStrategy objects. The function below allows you to easily create these RollingFutureStrategy objects by just passing in a few parameters.

The RollingFutureStrategy objects are instantiated using a few different parameters, such as rolling_rule and contract_sector.

Note: the RollingFutureStrategy objects assumes a constant long exposure from start_date to end_date.

def create_rfs(contract_code, start_date, end_date):
    '''Creates RollingFutureStrategy objects and
    returns the name of the object.'''
    try:
        rfs = sig.RollingFutureStrategy(
            # The contract_code can be found in the
            # Market Data Browser under 'ContractCode'
            contract_code=contract_code,

            # The contract_sector can be found in the
            # Market Data Browser under 'BBGMarketSector'
            contract_sector='COMDTY',

            # Specify the currency the strategy will be denominated in
            currency='USD',

            # The behaviour of the rolling between contracts,
            # F_0 means adjusted front contract
            # See the link specified above for more alternatives and explanations
            rolling_rule='F_0',

            # Start date of the strategy
            start_date=start_date,

            # End date of the strategy
            end_date=end_date
        )
        return rfs.name
    except:
        pass

Using a dictionary comprehension, all of the previously defined instruments will be passed into the function defined above, creating a RollingFutureStrategy object for each of the instruments:

BASKETS = {
    asset: create_rfs(contract_code, start_date, end_date) 
    for asset, contract_code in assets.items()
}
list(BASKETS.keys())

Create the strategy

The strategy consists of a simple momentum strategy, which in this case will mean that an asset is bought if it has had a positive price movement over a three month period, and be sold if it has had a negative price movement over a three month period.

The function below, generate_signals_momentum , will not perform any buying or selling actions. Instead it will generate a pandas DataFrame with a buying or selling signal for each data point with a given timeframe, such as daily, for each RollingFutureStrategy. The signals will be expressed as +1 or -1 depending on if it is a buying or selling signal.

def generate_signals_momentum(assets, days_window=63):
    '''Generates a dataframe of signals for a provided list of strategies'''

    # List to store signals for all assets
    frames = []

    # Loop through all provided assets
    for asset in assets:
        # Get the performance of the RollingFutureStrategy as a time series
        ts_data = sig.obj.get(asset).history()

        # Convert prices to percentage change
        pct_change_ts = ts_data.pct_change()

        # Create a rolling mean of the percentage price change over
        # a backward looking window of 63 days
        pct_change_mean_window_ts = pct_change_ts.rolling(days_window).mean()

        # Convert the percentage numbers to +1 or -1 if it's higher or lower
        # than zero, i.e. only maintain the sign of the percentage change.
        mom_signal = np.sign(pct_change_mean_window_ts)

        # Add the signal for this asset to the list of stored signals
        frames.append(mom_signal.to_frame(asset))

    # Convert the list to a DataFrame and remove all NaN values
    all_signals = pd.concat(frames, axis=1).dropna()

    return all_signals

In the code block below, all the instruments in the universe get passed in as a list to the function defined above, to convert the performance history of the RollingFutureStrategy objects to a pandas DataFrame holding signals. The signals will be +1 or -1 on each date for each asset, which indicates a buying or selling signal in that given asset.

signal_mom = generate_signals_momentum(assets=list(BASKETS.values()))
signal_mom.head()

Construct the portfolio

To apply the strategy to the universe and combine all the assets into a portfolio, the SignalStrategy class is used. But before we can use the SignalStrategy class, we need to convert the pandas DataFrame of signals into a signal object:

signal_obj = sig.signal_library.from_ts(signal_mom)

Once the signal object is created, a SignalStrategy object is created. There are many different parameters and alternatives that the user can make use of, such as different allocation functions between the assets in the strategy.

Learn more: Signal Strategy

mom_strat = sig.SignalStrategy(
    # Start date of strategy
    start_date=start_date,
    
    # End date of strategy
    end_date=end_date,
    
    # Currency in which the strategy is denominated in
    currency='USD',
    
    # Reference to the signal object
    signal_name=signal_obj.name,
    
    # The rebalance frequency of the strategy. The strategy will only
    # switch positions (buying or selling) of the underlying assets on 
    # the rebalance dates, e.g. EOM (end-of-month). This means that 
    # it is only the signals at the end-of-month that is relevant to
    # this strategy, and all the signals in between will not have an
    # impact on buying/selling of the underlying assets. 
    rebalance_frequency='EOM',
    
    # Allocation function for the strategy, in this case the allocation function
    # normalizes the number of short and long positions, i.e. holds the same number
    # of long and short positions at any given time.
    allocation_function=sig.signal_library.allocation.normalize_weights,
    
    # The reference name of the strategy
    ticker=f'Momentum Strategy'
)

Generate the performance report

After having created the strategy, the code block below can be used to create a performance report which contains information on:

  • Metrics

  • Rolling performance charts

  • Performance calendar

There are a wide range of different performance views and reports available.

sig.PerformanceReport(mom_strat).report()

Another useful tool is to delve into the different strategy constituents' performance attribution to the overall strategy. The strategy is built up in a layered way, and each layer can be further investigated in terms of performance attribution.

The different layers can be viewed using the .pnl_breakdown method, and by changing the parameter levels.

The different levels can be view—levels=0 is the top level, and levels=1 is the next one.

pnl, weight = mom_strat.analytics.pnl_breakdown(levels=1)
pnl.plot();

Next steps

Last updated

© 2023 SIG Technologies Limited