Strategy
The
Strategy
class is the basis for all backtesting in the SigTech platform. Functionality embedded within the Strategy
class is also accessible via the classes inheriting from it. Since all building blocks inherit from
Strategy
, using the full functionality of the SigTech platform’s building blocks requires a familiarity with the base class. This page is relevant to all users of the SigTech platform—learn how to:
- Build and backtest a strategy: Plot the value of a strategy through time.
- Name a strategy: Specify a name for a strategy via the ticker parameter.
- Retrieve a strategy: Fetch strategy objects from your cache via the
sig.obj.get
API. - Clone a strategy: Create a copy of a strategy and change its parameters.
- Fix the parameters of a strategy: Set the initial cash held by a strategy, determine if that cash accrues interest, and decide whether or not transaction costs are included in the backtest.
Set up your environment using the following three steps:
- Import the relevant internal and external libraries
- Configure the environment parameters
- Initialise the environment
import pandas as pd
import datetime as dtm
from uuid import uuid4
import seaborn as sns
sns.set(rc={'figure.figsize': (18, 6)})
​
import sigtech.framework as sig
​
currency = 'USD'
start_date = dtm.date(2020, 1, 2)
end_date = dtm.date(2021, 11, 30)
​
sig.init(env_date=end_date)
Backtests are conducted by creating an instance of an object that inherits from the
Strategy
class and calling build
on it.The following code block cycles through some commonly used building blocks and checks that they inherit from
Strategy
:Input
Output
example_building_blocks = [
sig.RollingFutureStrategy,
sig.RollingFXForwardStrategy,
sig.DynamicOptionsStrategy,
sig.RollingSwapStrategy,
sig.ReinvestmentStrategy,
sig.RollingBondStrategy,
sig.BasketStrategy,
sig.SignalStrategy,
]
for building_block in example_building_blocks:
print(f'{building_block.__name__}: {issubclass(building_block, sig.Strategy)}')
RollingFutureStrategy: True
RollingFXForwardStrategy: True
DynamicOptionsStrategy: True
RollingSwapStrategy: True
ReinvestmentStrategy: True
RollingBondStrategy: True
BasketStrategy: True
SignalStrategy: True
Once a strategy has been built, the details of the backtest are stored within it and are accessible for further analysis. This includes the value of the strategy and holdings through time, and lower level details, such as the generation and execution of orders.
Note: calling
build
on a strategy that has already been built has no additional effect.The following instance of a
RollingFutureStrategy
object is an example:Note: strategies generally require a
currency
and start_date
. If an end_date
isn't used, the asofdate
for the configured environment is used.rfs = sig.RollingFutureStrategy(
currency=currency,
start_date=start_date,
contract_code='ES',
contract_sector='INDEX',
)
Working through the build cycle:
Input
Output
rfs.built
False
rfs.build()
Input
Output
rfs.built
True
Once a strategy has been built, calling
history
retrieves a pd.Series
of its values or prices:Note: this is the equivalent of calling
history
on an instrument to return its value through time.​
Input
Output
rfs.history().head()
2020-01-02 1000.000000
2020-01-03 999.992522
2020-01-06 1002.576428
2020-01-07 1000.088143
2020-01-08 1007.802267
Name: (LastPrice, EOD, USD ES INDEX LONG NONE 4C2AE1C5 RFS STRATEGY), dtype: float64
Tip: it is unnecessary to call
build
and history
separately. Calling history
on an unbuilt strategy calls build
automatically.When a strategy object is constructed you can assign a
name
to it, using the ticker
parameter. Once constructed, it is added to the cache of the environment.If a
ticker
is not provided on construction, the strategy name
is determined using the strategy details:- Strategy type: such as
S
forStrategy
orBS
forBasketStrategy
. - Currency.
- A random, eight-digit, alphanumeric combination.
To create a
Strategy
and display its automatically generated name:Input
Output
s = sig.Strategy(
currency=currency,
start_date=start_date,
end_date=end_date
)
s.name
'USD 16454872 S STRATEGY'
To create a
Strategy
called TEST STRATEGY
:Input
Output
t = sig.Strategy(
currency=currency,
start_date=start_date,
end_date=end_date,
ticker='test'
)
t.name
'TEST STRATEGY'
Note: strategy names are always written in upper case, with
STRATEGY
added to the end.In Backtesting, we built a
RollingFutureStrategy
called rfs
. But we did not assign a ticker
value. To display the automatically generated name of rfs
:Input
Output
rfs.name
'USD ES INDEX LONG NONE 4C2AE1C5 RFS STRATEGY'
When a strategy object is created, its name is added to the cache. You can access the cache directly via the configured environment:
env_cache = sig.env().object.cache
Any object that has been accessed is also added to the cache. We can check the size of the cache with the following call:
Input
Output
print(len(env_cache))
135
You can filter the objects in the cache for those whose name ends with
STRATEGY
:Input
Output
sorted([o for o in env_cache.keys() if o.endswith('STRATEGY')])
['TEST STRATEGY',
'USD 16454872 S STRATEGY',
'USD CASH STRATEGY',
'USD ES INDEX LONG NONE 4C2AE1C5 RFS STRATEGY']
Note: The results of the previous two cells will vary at different points as you progress through the notebook and add more items to the cache.
Any object listed in the cache is retrieved using the
sig.obj.get
API. This includes strategies created by users:Input
Output
test_strategy = sig.obj.get('TEST STRATEGY')
test_strategy
TEST STRATEGY <class 'sigtech.framework.strategies.strategy.Strategy'>[139995990091088]
Check this with the original version:
Input
Output
test_strategy == t
True
To rebuild
rfs
and specify a ticker:Input
Output
rfs_ticker = sig.RollingFutureStrategy(
currency=currency,
start_date=start_date,
contract_code='ES',
contract_sector='INDEX',
​
# specify ticker upon construction
ticker='ES RFS'
)
rfs_ticker.name
'ES RFS STRATEGY'
Object name clashes result in an
ObjectError
, unless the new strategy has identical characteristics to the existing strategy.In the following code block, despite
rfs_duplicate
having the same ticker
as rfs_ticker
, there is no ObjectError
as the characteristics of both strategies are identical:Input
Output
rfs_duplicate = sig.RollingFutureStrategy(
currency=currency,
start_date=start_date,
contract_code='ES',
contract_sector='INDEX',
ticker='ES RFS'
)
rfs_duplicate.name
'ES RFS STRATEGY'
In contrast, the following example displays an error being produced due to
rfs_error
and rfs_ticker
sharing the same ticker
, but not the same start_date
:Input
Output
from sigtech.framework.internal.infra.utils.exceptions import ObjectError
​
try:
rfs_error = sig.RollingFutureStrategy(
currency=currency,
# start date is altered
start_date=start_date + dtm.timedelta(days=1),
contract_code='ES',
contract_sector='INDEX',
ticker='ES RFS'
)
except ObjectError as e:
print(e)
Creating new object contradicting existing one!
ES RFS STRATEGY <class 'sigtech.framework.strategies.rolling_future_strategy.RollingFutureStrategy'>[139995990168784]
ES RFS STRATEGY <class 'sigtech.framework.strategies.rolling_future_strategy.RollingFutureStrategy'>[139995990128336]
The
clone_strategy
method allows you to create a copy of an existing strategy and vary some of its parameters.A dictionary of
{parameter: new_value}
pairs should be supplied. The following example creates a version of rfs
in 'EUR'
with an updated ticker
:Input
Output
eur_rfs = rfs.clone_strategy({'currency': 'EUR', 'ticker': 'EUR RFS'})
eur_rfs
EUR RFS STRATEGY <class 'sigtech.framework.strategies.rolling_future_strategy.RollingFutureStrategy'>[139995990170832]
Input
Output
print(f'Currency: {eur_rfs.currency}')
print(f'Name: {eur_rfs.name}')
Currency: EUR
Name: EUR RFS STRATEGY
A strategy begins by holding a number of cash units, denominated in the
currency
of the strategy. The number of units is determined by the initial_cash
parameter, with a default value of 1000
.Using the
RollingFutureStrategy
in Backtesting, plotting the history of rfs
results in an initial value of 1000:rfs.history().plot();

The portfolio table also records an initial 1000 units of USD CASH:
rfs.plot.portfolio_table(
dts='TOP_ORDER_PTS',
end_dt=dtm.date(2020, 1, 3)
)

To create a variant of
rfs
whose initial_cash
level is 100
:Input
Output
rfs_initial_cash = rfs.clone_strategy({'ticker':'ES RFS $100', 'initial_cash': 100})
rfs_initial_cash.name
'ES RFS $100 STRATEGY'
As expected, the history series begins at
100
:Input
Output
rfs_initial_cash.history().head()
2020-01-02 100.000000
2020-01-03 99.999252
2020-01-06 100.257643
2020-01-07 100.008814
2020-01-08 100.780227
Name: (LastPrice, EOD, ES RFS $100 STRATEGY), dtype: float64
Assuming that the transaction cost model is AUM independent, varying
initial_cash
in this way is purely a matter of scaling and will not impact performance characteristics:Input
Output
from sigtech.framework.analytics.performance.metrics import annualised_return
​
rfs_perf = annualised_return(rfs.history().dropna())
rfs_ic_perf = annualised_return(rfs_initial_cash.history().dropna())
​
print(f'{round(100 * rfs_perf, 3)}%')
print(f'{round(100 * rfs_ic_perf, 3)}%')
22.723%
22.723%
An exception to this rule is when
initial_cash
is set to zero. In this case no positions are opened. It is not possible to compute common metrics, such as return, for such a strategy.Input
Output
rfs_zero_cash = rfs.clone_strategy({'ticker':'ES RFS ZERO', 'initial_cash': 0})
rfs_zero_cash.name
'ES RFS ZERO STRATEGY'
rfs_zero_cash.history().plot();

There are multiple ways to remove cash from a strategy whilst still investing in underlyings. In the case of the
RollingFutureStrategy
, you will need to provide a fixed_contracts
parameter and set the initial_cash
to zero. Similar functionality is available for
ReinvestmentStrategy
objects, using the initial_shares
parameter. Accruing interest on the cash held within a strategy is optional and can be controlled with the boolean input
total_return
.To create a variant of
rfs
that does not accrue interest on cash held:rfs_er = rfs.clone_strategy({'ticker':'ES RFS ER', 'total_return': False})
rfs_er.build()
​
df = pd.DataFrame({'TotalReturn': rfs.history(),
'ExcessReturn': rfs_er.history()}).dropna()
df.plot();

The difference in annualised return between the
TotalReturn
version of the strategy and the ExcessReturn
version is approximately 32 basis points:Input
Output
round(10000 * (rfs_perf - annualised_return(df['ExcessReturn'])))
32.0
As a further example, a custom strategy that simply holds its initial cash is created:
class ExampleStrategy(sig.Strategy):
def strategy_initialization(self, dt):
pass
​
example_strategy = ExampleStrategy(
currency=currency,
start_date=dtm.date(2000, 1, 4),
initial_cash=100
)
To create an alternative version with
total_return
set to False
and plot the performance of both versions of the strategy:er_example_strategy = example_strategy.clone_strategy({
'total_return': False
})
​
pd.DataFrame({
'TR': example_strategy.history(),
'ER': er_example_strategy.history()
}).plot();

To switch off
total_return
at the global level when configuring the environment, use the command: sig.env()[sig.config.EXCESS_RETURN_ONLY] = True
.The inclusion of transaction costs in a backtest can be controlled with the
include_trading_costs
parameter.You can create a variant of
rfs
that does not assume any trading costs:rfs_no_tc = rfs.clone_strategy({
'ticker': 'RFS NO TRADING COSTS',
'include_trading_costs': False
})
To compute the impact of the transaction cost assumptions, you can compare the performance of the two strategy variants:
rfs_no_tc_perf = annualised_return(rfs_no_tc.history().dropna())
The impact here is 2bp annualised. This can vary considerably, depending on the underlying and the transaction cost model.
Input
Output
round(10000 * (rfs_no_tc_perf - rfs_perf))
2.0
To switch off transaction costs at the global level when configuring the environment, use the following command:
sig.env()[sig.config.IGNORE_T_COSTS] = True
.You can fix the amount of cash used to determine the position of a strategy at its inception.
Example: signify a fixed capital allocation for a PM regardless of performance.
rfs_initial = sig.RollingFutureStrategy(
currency=currency,
start_date=dtm.date(2016, 1, 4),
contract_code='ES',
contract_sector='INDEX',
# relevant parameters
initial_cash=1e6,
set_weight_from_initial_cash=True,
ticker='ES RFS FIXED INITIAL CASH'
)
rfs_initial.build()
​
rfs_varying = rfs_initial.clone_strategy({
'set_weight_from_initial_cash': False
})
​
pd.DataFrame({
'VARYING CASH': rfs_varying.history(),
'FIXED CASH': rfs_initial.history()
}).plot();

The following portfolio table displays a position of USD 1MM targeted at rebalance, despite the strategy holding close to USD 2MM in cash:
rfs_initial.plot.portfolio_table(
dts='TOP_ORDER_PTS',
start_dt=dtm.date(2021, 3, 1),
end_dt=dtm.date(2021, 4, 1)
)

Short versions of strategies are created by passing the
direction
parameter with a value of 'short'
, as opposed to its default of 'long'
:rfs_short = sig.RollingFutureStrategy(
currency=currency,
start_date=start_date,
contract_code='ES',
contract_sector='INDEX',
# specify 'short' version
direction='short'
)
rfs_short.build()
In the case of a
RollingFutureStrategy
, the 'short'
version coincides with the negative of the long version:pd.DataFrame({
'LONG': rfs.history(),
'SHORT': -rfs_short.history()
}).plot();

Going short on the long version of the
RollingFutureStrategy
through a basket delivers the same result as going long on the short version of the RollingFutureStrategy
through a basket:basket_short_long_rfs = sig.BasketStrategy(
currency=currency,
start_date=start_date,
end_date=end_date,
weights=[-1.0],
constituent_names=[rfs.name],
rebalance_frequency='EOM'
)
basket_short_long_rfs.build()
​
basket_long_short_rfs = sig.BasketStrategy(
currency=currency,
start_date=start_date,
end_date=end_date,
weights=[1.0],
constituent_names=[rfs_short.name],
rebalance_frequency='EOM'
)
basket_long_short_rfs.build()
​
pd.DataFrame({
'LONG SHORT-RFS': basket_long_short_rfs.history(),
'SHORT LONG-RFS': basket_short_long_rfs.history(),
}).plot();

For certain assets there are expected differences caused by market realities. An example is the treatment of dividends when creating a
ReinvestmentStrategy
:rs_long = sig.ReinvestmentStrategy(
currency=currency,
start_date=start_date,
end_date=end_date,
underlyer='1000045.SINGLE_STOCK.TRADABLE',
ticker='AAPL RS LONG'
)
rs_long.build()
​
rs_short = rs_long.clone_strategy({'direction': 'short'})
​
pd.DataFrame({
'RS LONG': rs_long.history(),
'RS SHORT': -rs_short.history()
}).plot();

Note: These examples illustrate how to specify a specific number of units of underlying to trade whilst using a Building Block wrapper. The precise syntax can vary by asset class.
By default the number of contracts held by a
RollingFutureStrategy
will be determined by the strategy's value and the value of each futures contract. It is possible to specify that a fixed number of contracts be traded by using the fixed_contracts
parameter.Input
Output
rfs_ticker = sig.RollingFutureStrategy(
currency=currency,
start_date=start_date,
contract_code='ES',
contract_sector='INDEX',
ticker='ES RFS'
)
rfs_ticker.name
'ES RFS STRATEGY'
rfs_contract_only = sig.RollingFutureStrategy(
currency=currency,
start_date=start_date,
end_date=end_date,
contract_code='ES',
contract_sector='INDEX',
# specific parameters
initial_cash=0,
include_trading_costs=False,
fixed_contracts=1,
ticker='ES RFS CONTRACT ONLY'
)
rfs_contract_only.build()
​
rfs_contract_only.history().plot();

rfs_contract_only.plot.portfolio_table(
dts='VALUATION_PTS',
start_dt=start_date,
end_dt=dtm.date(2020, 1, 14),
unit_type='TRADE'
)

To create a
ReinvestmentStrategy
that holds precisely one unit of the underlying stock the initial_shares
parameter can be used. You can also choose whether to include an initial cash balance on the strategy.rs = sig.ReinvestmentStrategy(
currency=currency,
start_date=start_date,
end_date=end_date,
underlyer='1000045.SINGLE_STOCK.TRADABLE',
# specific parameters
initial_cash=0,
include_trading_costs=False,
initial_shares=1,
ticker='AAPL RS STOCK ONLY'
)
rs.build()
​
rs.history().plot();

rs.plot.portfolio_table(
dts='VALUATION_PTS',
start_dt=start_date,
end_dt=dtm.date(2020, 1, 14),
unit_type='TRADE'
)

Last modified 1yr ago