Commodities Momentum Strategy#

Understand this code: Read our step-by-step explanation of this notebook: guided walkthrough.

Unrestricted data: This notebook only uses data in our core offer to all users. Go ahead and run it.

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. ## 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)})

env = sig.init(env_date=dtm.date(2022, 5, 1))

Define the investment universe#

[ ]:
# 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',
}
[ ]:
start_date = dtm.date(2015, 1, 4)
[ ]:
def create_rfs(contract_code: str, start_date: dtm.date) -> str:
    '''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
            rolling_rule = 'F_0',

            # Start date of the strategy
            start_date = start_date,
        )
        return rfs.name
    except:
        pass
[ ]:
BASKETS = {
    asset: create_rfs(contract_code, start_date)
    for asset, contract_code in assets.items()
}
list(BASKETS.keys())

Create the strategy#

[ ]:
def generate_signals_momentum(assets: list[str], days_window: int = 63) -> pd.DataFrame:
    '''Generates a dataframe of signals for 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
[ ]:
signal_mom = generate_signals_momentum(assets = list(BASKETS.values()))
signal_mom.head()

Construct the portfolio#

[ ]:
signal_obj = sig.signal_library.from_ts(signal_mom)
[ ]:
mom_strat = sig.SignalStrategy(
    # Start date of strategy
    start_date = start_date,

    # Currency in which the strategy is denominated in
    currency = 'USD',

    # Reference to the signal object which was created above
    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 is 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#

[ ]:
sig.PerformanceReport(mom_strat.history()).report()
[ ]:
pnl, weight = mom_strat.analytics.pnl_breakdown(levels=1)
pnl.plot();