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_neutral
allocation 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_driven
allocation 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_weights
allocation 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_weighted
allocation 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_driven
allocation function where the weighting is multiplied by the original signal value.
5) Static weighted
The static_weighted
allocation 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_weighted
allocation function where only static weights are supplied.
7) Long - short
The long_short
allocation function takes two arguments, a long_instrument_name
as 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 theFactorExposures
class which implements estimation methods for exposures i.e. performing a series of multivariate time-series regressions based on a configuration.optimization_problem
: uses theFactorOptimizationProblem
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 throughset_optimizer_objective
andset_optimizer_bounds
.optimizer (Optional)
: uses theFactorOptimizer
class which stores a portfolio optimisation problem configuration. To run an optimisation problem, aFactorOptimizationProblem
object needs to be provided together with theFactorExposure
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:
Value a collection of rolling future strategies from all asset classes.
Calculate a signal that is long if the prior three month return is positive, and short when it is negative.
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.