Signal Strategy#

The SignalStrategy building block is a user-friendly class that easily converts a DataFrame of signals to a strategy, while providing generalised strategy options.

The signal strategy works well for most common strategies.

Learn more: Example notebooks

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 pandas as pd
import numpy as np
import datetime as dtm
import seaborn as sns

# default rolling futures strategy objects
from sigtech.framework.default_strategy_objects.rolling_futures import (
    hg_comdty_f_0, ty_comdty_front,  cl_comdty_f_0, ng_comdty_f_0)

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

Learn more: Setting up the environment

SignalStrategy class #

The SignalStrategy is a generic class that converts any signal object to a trading strategy. There are a number of generic inputs that can be used to adjust the strategy behaviour. In most instances, the SignalStrategy class is used to quickly and easily create a robust strategy.

Some options available are:

  • rebalance_frequency: 'EOM' = end of month, 'SOM' = start of month, '1W-FRI' = rebalance weekly every Friday, and'YEARLY' = start of each year.

  • allocation_function: allocation functions can be passed in to the strategy as an extra layer of manipulation to the signal weights as needed. A full list of available, pre-built functions are listed below.

  • threshold_function: this is another final step that determines whether a rebalance should go ahead. It is typically used to avoid unnecessary trades when the current position and the desired position are very close.

  • instrument_mapping: this dictionary provides a mapping from the column name of the signal DataFrame to the name of the instrument we want to trade.

Example#

When using the signal strategy class on a basket of strategies, as above, it is often useful to define a set of allocation rules. The class takes an allocation_function argument that will further manipulate the signal weights as defined by the function. Some pre-built allocation functions are available for easy and quick use, and can be accessed within sig.signal_library.allocation.

Here is an example of creating a signal strategy on a universe consisting of RollingFutureStrategy objects. A simple momentum signal is formed from their rolling three month returns. The long_short_dollar_neutralallocation function is used to transform the input signals into strategy positions. Further elaboration on Allocation Functions:

Learn more: Allocation functions.

universe = {
    'HG': hg_comdty_f_0(),
    'TY': ty_comdty_front(),
    'CL': cl_comdty_f_0(),
    'NG': ng_comdty_f_0()
}

signal_df = pd.concat({
    contract: rf_strategy.history().pct_change(63)
    for contract, rf_strategy in universe.items()
}, axis=1).dropna()

An instrument_mapping argument can be provided to the SignalStrategy to indicate which instruments should be traded by the strategy.

Example: 'ES' is the name of the column in the signal_df representing the signal for the corresponding rolling futures strategy: USD ES INDEX LONG FRONT RF STRATEGY. instrument_mapping['ES'] will yield the string ‘USD ES INDEX LONG FRONT RF STRATEGY'.

Example:

instrument_mapping = {
    contract: rf_strategy.name for contract, rf_strategy in universe.items()
}

This can be passed as an argument to the SignalStrategy, along with an allocation function, the signal’s name, and other required arguments:

long_short_dollar_neutral_strategy = sig.SignalStrategy(
    currency='USD',
    start_date=dtm.datetime(2010, 1, 4),
    signal_name=sig.signal_library.core.from_ts(signal_df).name,
    allocation_function=sig.signal_library.allocation.long_short_dollar_neutral,
    allocation_kwargs={'proportion': 0.25},
    instrument_mapping=instrument_mapping,
    rebalance_frequency='SOM'
)

To see the approximate return of the strategy, without including trading costs, use the following code block:

long_short_dollar_neutral_strategy.approx_return().plot()

This signal strategy will be long and short positions over the top and bottom proportion of signal values, with 100% gross exposure: 50% long exposure and 50% short exposure. In this case, we have passed 0.25 as our proportion, and so the strategy will be long the top 25% of signal values, and short the bottom 25%. That is to say, long 2 strategies and short 2 strategies from this universe of 8 strategies on any given date.

Allocation functions#

When using the Signal Strategy class on a collection of strategies, it is often useful to define a set of allocation rules. The class takes an allocation_function argument that will further transform the signal value inputs before converting to portfolio weights. A range of pre-built allocation functions are available for quick and easy use on the platform. These can be accessed from sig.SignalStrategy.AVAILABLE_ALLOCATION_FUNCTIONS.

These are:

1) Identity (default)

Theidentity allocation function simply returns the unchanged allocations as given by the signal strategy output. This method is chosen by default if no allocation function is passed to the object.

2) Equal signal driven

The equal_signal_drivenallocation function weights a list of instruments with the same weights. This function takes a list of instruments and returns a DataFrame, with each instrument signal multiplied by an equal factor.

3) Normalise weights

The normalize_weightsallocation function normalises all weights to have 100% gross notional exposure. The total sum of all weights after normalisation is equal to 100% of the gross notional exposure of the strategy.

4) Equally weighted

The equally_weightedallocation function divides the allocations by the number of instruments. All instrument signals are equal to each other. This is slightly different from the equal_signal_drivenallocation function where the weighting is multiplied by the original signal value.

5) Static weighted

The static_weightedallocation function takes a set of weights in dictionary format and supplies this to the signal time-series. Each weight within the dictionary is multiplied by the signal value.

6) Time varying weighted

The time_varying_weighted allocation function takes a set of time-series weights in dictionary format and supplies this to the signal time-series. Each weight within the dictionary is multiplied by the signal value. This is different to the static_weightedallocation function where only static weights are supplied.

7) Long - short

The long_short allocation function takes two arguments, a long_instrument_nameas well as a short_instrument_name. The purpose of this allocation function is to trade a series signal long on one instrument and short on another.

8) Long - short dollar neutral

The long_short_dollar_neutral allocation function takes a proportion argument (value between 0-1) whereby the strategy will take long and short positions over the top and bottom proportion of signal values, asserting 100% gross exposure.

9) Factor optimised allocations

The factor_optimised_allocations function runs a factor optimisation task for each signal entry. The allocation function takes a number of arguments:

  • factor_exposure_object : uses the FactorExposures class which implements estimation methods for exposures i.e. performing a series of multivariate time-series regressions based on a configuration.

  • optimization_problem: uses the FactorOptimizationProblem class which stores a portfolio optimisation problem configuration. The problem is composed of two parts: objectives and constraints. These can be added to the instance through set_optimizer_objective andset_optimizer_bounds.

  • optimizer (Optional): uses the FactorOptimizer class which stores a portfolio optimisation problem configuration. To run an optimisation problem, a FactorOptimizationProblem object needs to be provided together with the FactorExposure object that contains the data.

  • periods (Optional): chosen time window.

  • base (Optional): optimiser base.

  • instrument_mapping: mapping in dictionary format from the column name of the signal to the instrument to trade.

Threshold function#

The SignalStrategy building block rebalances its weights periodically, as defined by the user-supplied rebalance_frequency parameter.

There could be instances where a trading strategy does not necessarily need to rebalance under a certain set of conditions. You can specify a threshold_function within the SignalStrategy object that will dictate whether rebalancing takes place.

Within the platform, it is possible to use the sparse_threshold_function found under sig.signal_library.threshold_fns.

Note: the full list of threshold functions is accessible through sig.SignalStrategy.AVAILABLE_THRESHOLD_FUNCTIONS.

The sparse_threshold_function will not trigger rebalancing to a zero weight unless there is a current position. So the signal can be primarily composed of zeros which will only be used for closing positions.

It is possible to create your own customised threshold function which can be used to determine whether rebalancing takes place.

Example#

If we have a DataFrame of signals for a universe of strategies, all varying between 0-1. This DataFrame, named weights_df, can be used to determine the position weighting of the strategy the signal is mapped to. In this instance, we can define our own threshold function that looks like the following:

def custom_threshold_function(instrument_name, dt, size_date, positions,
                              allocation_ts, strategy, threshold):
    weight = allocation_ts.loc[:size_date].iloc[-1]
    if abs(weight) > threshold:
        return True
    else:
        return False

This function will take the weight, given by the signal, and check against a threshold supplied by threshold_kwargs. You can implement this through the SignalStrategy building block:

threshold_strategy = sig.SignalStrategy(
    start_date=dtm.datetime(2010, 1, 4),
    currency='USD',
    signal_name=sig.signal_library.core.from_ts(weights_df).name,
    rebalance_frequency='EOM',
    threshold_function=custom_threshold_function,
    threshold_kwargs={'threshold': 0.7},
    instrument_mapping=universe,
)

Note: in this case, only signals greater than 0.7 will trigger a rebalance.

Creating SignalStrategy objects#

This section will demonstrate how to build a simple momentum strategy trading global futures using the SignalStrategy building block.

The strategy will work as follows:

  1. Value a collection of rolling future strategies from all asset classes.

  2. Calculate a signal that is long if the prior three month return is positive, and short when it is negative.

  3. Rebalance the portfolio at the end of each month.

Build signal #

To start, construct a signal for each future. This will be a very simple signal that will put an equal nominal weight on each future. It will go long when the return over a prior window is positive, and short when it is negative. In the code below a window of 63 business days, approximately three months, is used.

Individual future signal example

The signal is calculated on the future strategy object pulled in above:

def create_rfs(grp):
    return sig.RollingFutureStrategy(
        start_date=dtm.date(2010, 1, 5),
        contract_code=grp,
        contract_sector='COMDTY', # assumes commodity futures
        currency='USD',
        rolling_rule='front',
        front_offset='-3:-2'
    )

Python:

future_strategy_object = create_rfs('C ')
future_strategy_object

Output:

USD C  COMDTY LONG FRONT 5E57194C RFS STRATEGY <class 'sigtech.framework.strategies.rolling_future_strategy.RollingFutureStrategy'>[139953927918672]

Input:

future_strategy_object.history().plot(
    title='Corn Future Strategy Performance');

Output:

# Evaluate rolling future total return series
future_history = future_strategy_object.history().to_frame('C ')

# Evaluate absolute return for a manually defined window of 63 business days
future_returns = future_history.diff(63)

# Evaluate the sign of the return for the signal
future_signal = np.sign(future_returns)

Input:

ax = future_signal.plot(
    title='Corn Signal', ylim=[-1.1, 1.1], legend=False)
ax.axhline(0.0, linestyle='--', color='k');

Output:

Future selection #

A dictionary mapping each future code to a rolling future strategy ticker is given below. The entries to this dictionary define the universe of futures used. Use the helper functions provided in sigtech.framework.default_strategy_objects.rolling_futures.

Input:

FUTURES_LST = [
    'C ', 'SB', 'CO', 'QS', 'HO', 'LC'
]

#Use COMDTY to build commodity futures
LONG_FUTURE_DICT = {
    future: create_rfs(future).name for future in FUTURES_LST
}

len(LONG_FUTURE_DICT)

Output:

6

Full strategy signal

For the full strategy signal the process above is repeated on all of the futures to result in a DataFrame of weights:

Input:

# Evaluate rolling future total return series and absolute returns
strategy_3m_returns_df = pd.DataFrame({
    future_code: sig.obj.get(strategy_name).history().diff(63)
    for future_code, strategy_name in LONG_FUTURE_DICT.items()
}).ffill()

# Evaluate the sign of the return for the signal
# we drop the NaN values to clean the signal's early history
signal_df = np.sign(strategy_3m_returns_df).dropna()

signal_df.tail()

Output:

A signal object is then created using sig.signal_library.core.from_ts. Like all instruments and strategies, this signal object and its history can be retrieved with its unique name using obj.get("...").history().

Input:

example_signal_obj = sig.signal_library.from_ts(signal_df)
example_signal_obj_name = example_signal_obj.name
example_signal_obj_name

Output:

'IDENTITY 6FEFA14F SIGNAL'

Input:

sig.obj.get(example_signal_obj_name).history().tail()

Output:

With all the relevant parameters, you can now define the strategy object:

example_strategy_1 = sig.SignalStrategy(
    start_date=dtm.date(2011, 1, 4),
    currency='USD',
    signal_name=example_signal_obj.name,
    rebalance_frequency='EOM',
    allocation_function=sig.signal_library.allocation.normalize_weights,
    instrument_mapping=LONG_FUTURE_DICT,
)

Note: the initial signal had negative weights on the long futures to go short. By default the signal strategy converts these negative long positions to corresponding positive short positions.

.history() runs the backtest and provides a pandas series of the performance of the strategy:

Input:

example_strategy_1.history().plot(title='Strategy Performance')

Output:

Intraday Signal Strategy building block#

The IntradaySignalStrategy building block clones the functionality of the existing SignalStrategy class but sets the defaults of certain parameters to intraday-focused values. This offers a more efficient way of creating intraday strategies.

The default parameters are: use_signal_for_rebalance_dates=True, t0_execution=True, execution_delay='5 minutes'.

The following code block demonstrates the use of this new building block.

intraday_signal = pd.DataFrame(
    {'ECZ22 CURNCY': [1, 0, 1]},
    index = [dtm.datetime(2021,1,5, 21, 0, 0),
             dtm.datetime(2021,1,5, 21, 0, 1),
             dtm.datetime(2021,1,5, 21, 0, 2)]
)

intraday_strategy = sig.IntradaySignalStrategy(
    currency='USD',
    signal_name=sig.signal_library.from_ts(intraday_signal).name,
    start_date=intraday_signal.first_valid_index()
)
intraday_strategy.build()

API documentation#

Learn more: API documentation.