Long-only Trend Strategy#

New user? The best way to learn SigTech is to follow the steps in our user guide.

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.

Introduction#

The purpose of thise notebook is to show how to construct a strategy on the SigTech platform - in this case with a basket of cross asset rolling futures.

Environment#

This section will import relevant internal and external libraries, as well as setting up the platform 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(2024, 2, 22))

Universe#

Below a helper function is defined which creates RollingFutureStrategy objects based on relevant input parameters.

[ ]:
def create_rfs(
    asset_params: dict[str, str],
    start_date: dtm.date,
    end_date: dtm.date
) -> sig.RollingFutureStrategy:

    if asset_params['roll_rule'] == 'front':
        st = sig.RollingFutureStrategy(
            contract_code = asset_params['symbol'],
            contract_sector = asset_params['sector'],
            currency = 'USD',
            rolling_rule = 'front',
            front_offset = asset_params['front_offset'],
            start_date = start_date,
            end_date = end_date
        )
    elif asset_params['roll_rule'] == 'F_0':
        st = sig.RollingFutureStrategy(
            contract_code = asset_params['symbol'],
            contract_sector = asset_params['sector'],
            currency = 'USD',
            rolling_rule = 'F_0',
            monthly_roll_days = asset_params['monthly_roll_days'],
            start_date = start_date,
            end_date = end_date
        )

    return st

Below are the instruments and asset classes defined which will form the strategy universe.

[ ]:
start_date = dtm.date(2010, 1, 4)
end_date = dtm.date(2020, 12, 4)

# Nested dict of assets and their symbol
assets = {
    'RATES': {
        'Treasury Futures (TY)': {'symbol': 'TY', 'roll_rule': 'front', 'front_offset': '-3,-3', 'sector': 'COMDTY'}
    },
    'COMMODITIES': {
        'Corn Futures (C)': {'symbol': 'C ', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Soybean Futures (S)': {'symbol': 'S ', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Sugar Futures (SB)': {'symbol': 'SB', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Cotton Futures (CT)': {'symbol': 'CT', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Live Cattle Futures (LC)': {'symbol': 'LC', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Soybean Oil Futures (BO)': {'symbol': 'BO', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Brent Crude Futures (CO)': {'symbol': 'CO', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Gas Oil Futures (QS)': {'symbol': 'QS', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
        'Heating Oil Futures (HO)': {'symbol': 'HO', 'roll_rule': 'F_0', 'monthly_roll_days': '1,2', 'sector': 'COMDTY'},
    },
    'EQUITY_INDICES': {
        'S&P Futures (ES)': {'symbol': 'ES', 'roll_rule': 'front', 'front_offset': '-6,-5', 'sector': 'INDEX'},
        'Nasdaq Futures (NQ)': {'symbol': 'NQ', 'roll_rule': 'front', 'front_offset': '-7,-6', 'sector': 'INDEX'}
    }
}
# Create basket of rolling future strategies
BASKETS = {
    asset_class: {
        contract: create_rfs(contract_params, start_date, end_date)
        for contract, contract_params in contracts.items()
    } for asset_class, contracts in assets.items()
}
[ ]:
# Add ETF for EM Rates
BASKETS['RATES']['Emerging Markets bond ETF'] = sig.ReinvestmentStrategy(
    start_date = start_date,
    underlyer = 'EMB UP EQUITY',
    currency = 'USD',
)

Show the price time series of the RollingFutureStrategy for the Treasury Futures (TY) contract group.

[ ]:
ty = BASKETS['RATES']['Treasury Futures (TY)']
ty.history().plot(title="Rolling Treasury Future Return (TY)");

Strategy#

[ ]:
def basket_weights(ts, weight):
    """ Replace with provided weight - gives an equal weight to each strategy """

    return 0 * ts + weight

def vol_target_weights(ts, vol_target=0.08):
    """ Calculate a weight to target a specified annual volatility """

    ann_vol = np.sqrt(252) * ts.pct_change().rolling(window=126).std().dropna()

    return vol_target / ann_vol

def momentum_overlay_weights(ts, short_span=63, long_span=252):
    """ Calculate a long momentum signal : 1 if the short span ewma is greater than long span, 0 otherwise """

    momentum_signal = ts.ewm(span=short_span).mean() > ts.ewm(span=long_span).mean()

    # Convert from true/false to 1/0.
    return momentum_signal.dropna().astype(float)

In this step we are utilizing the building block SignalStrategy in order to convert signals into a tradeable strategy. To find further information on SignalStrategy, see the following page Signal Strategy.

[ ]:
def create_new_adjusted_strategy(strategy_list, adjustment_func, name=None, **kwargs) -> sig.SignalStrategy:
    """
    Create a new platform Signal Strategy driven by a signal
    The signal is calculated by applying a supplied function on to the given strategies return
    If a list of strategies are provided, the method is applied individually and then combined in to one basket strategy.
    """

    signal_df = pd.concat({x.name : adjustment_func(x.history(), **kwargs) for x in strategy_list}, axis=1).dropna()

    st = sig.SignalStrategy(
        currency = 'USD',
        signal_name = sig.signal_library.from_ts(signal_df).name,
        rebalance_frequency = '1W-FRI',
        start_date = signal_df.first_valid_index(),
        end_date = signal_df.index[-1],
        ticker = name
    )
    return st
[ ]:
# Apply vol targeting to treasuries
example_ty_vol_targeted = create_new_adjusted_strategy([ty], vol_target_weights)

# Apply momentum overlay on to vol targeted results
example_ty_momentum_overlay = create_new_adjusted_strategy([example_ty_vol_targeted], momentum_overlay_weights)

# Plot return of treasury futures / treasurues with vol targeting / treasuries with vol targeting & momentum.
ty_history = ty.history()
ax=ty_history.plot(label='Treasuries (TY)', legend=True);
example_ty_vol_targeted.history().plot(label='TY Vol Targeted', legend=True);
example_ty_momentum_overlay.history().plot(label='TY Momentum Overlay', legend=True);

# Extract signal from the momentum strategy and overlay in plot.
example_momentum_overlay_signal = example_ty_momentum_overlay.signal.history().iloc[:,0].reindex(ty_history.index)
example_momentum_overlay_signal.plot(label='TY Momentum Overlay Signal',
                                     legend=True, secondary_y=True, color='k', alpha=0.5);

Portfolio#

Creating the asset-class baskets#

[ ]:
def create_asset_class_baskets(basket_name):
    """ loop over asset class constituents creating momentum-driven strategies, then combine into a basket"""

    asset_class_instruments = BASKETS[basket_name]

    momentum_overlays = []
    for instr in asset_class_instruments:

        # Apply vol targeting and then momentum overlay.
        vol_targeted_strategy = create_new_adjusted_strategy([asset_class_instruments[instr]], vol_target_weights)
        momentum_overlay_strategy = create_new_adjusted_strategy([vol_targeted_strategy], momentum_overlay_weights, name = instr + '.M_Overlay')

        momentum_overlays.append(momentum_overlay_strategy)

    # Generate asset class basket using equal weights.
    basket = create_new_adjusted_strategy(momentum_overlays, basket_weights, name=basket_name, weight = 1.0 / len(momentum_overlays))
    return basket

# Run over asset classes to create the three baskets.
asset_class_baskets = {basket_name : create_asset_class_baskets(basket_name) for basket_name in BASKETS.keys()}
[ ]:
for basket_name, basket in asset_class_baskets.items():
    basket.history().rename(basket_name).plot(legend=True)

Creating the top portfolio#

[ ]:
# Use equal weightings between the asset class baskets to form portfolio.
portfolio = create_new_adjusted_strategy(list(asset_class_baskets.values()), basket_weights, weight = 1.0 / len(asset_class_baskets))
[ ]:
portfolio.history().plot(title='Portfolio Total Return');
[ ]:
# Evaluate the trades at one point in time
portfolio.inspect.evaluate_trades(dtm.datetime(2017, 11, 10, 9), 100e6, formatted=True)

Performance#

Overall performance#

[ ]:
sig.PerformanceReport(portfolio.history().rename('PORTFOLIO'), sig.CashIndex.from_currency('USD').history()).report()
[ ]:
return_df = pd.concat({na : st.history() for na, st in asset_class_baskets.items()}, axis=1).dropna()
sig.PerformanceReport(return_df, sig.CashIndex.from_currency('USD').history()).report()

Return attributions#

[ ]:
# Get p&l contributions from level 1 - one level below portfolio where we have the asset class baskets.
pnl, weight = portfolio.analytics.pnl_breakdown(levels=1)
[ ]:
ax=(100 * pnl[[st.name for st in asset_class_baskets.values()]]).plot()
ax.set_ylabel("% Return");
[ ]:
# Get p&l contributions from level 2 - one level below the asset class baskets where we have the momentum overlays.
pnl, weight = portfolio.analytics.pnl_breakdown(levels=2)

# Only show contributions from the rates.
ax=(100 * pnl[[(st + '.M_Overlay STRATEGY').upper() for st in BASKETS['RATES']]]).plot()
ax.set_ylabel("% Return");
[ ]:
# Obtain the annual turnover values for instrument types.
portfolio.analytics.turnover_stats()

The transaction cost impact#

[ ]:
# Create an updated portfolio excluding transaction costs
portfolio_with_no_tcosts = portfolio.clone_object({'include_trading_costs': False})
[ ]:
return_df = pd.concat({'PORTFOLIO' : portfolio.history(),
                       'PORTFOLIO - NO COSTS' : portfolio_with_no_tcosts.history()}, axis=1).dropna()
sig.PerformanceReport(return_df, sig.CashIndex.from_currency('USD').history()).stats_display()
[ ]:
return_df.plot();