Simulations & scenarios#
This page shows how simulation adapters can be added for a simple momentum strategy.
SigTech platform’s flexible data adapter system allows data to be replaced with artificial data. This is useful for scenario analysis, running simulations and checking strategies for possible overfitting.
Learn more: Example notebooks
Environment#
This section will import relevant internal and external libraries, as well as setting up the platform environment.
Learn more: Environment setup
import sigtech.framework as sig
from sigtech.framework.infra.data_adapter.simulations.simulation_adapter import SimAdapter
from sigtech.framework.infra.objects.dtypes import StringType, IntType, DictType
from sigtech.framework.schedules import SchedulePeriodic
from sigtech.framework.schedules.strategy import StrategySchedule
import datetime as dtm
import pandas as pd
import numpy as np
import seaborn as sns
sns.set(rc={'figure.figsize': (18, 6)})
if not sig.config.is_initialised():
env = sig.config.init()
Dates to be used when running analysis and scenarios:
past_date = dtm.datetime(2019, 1, 4, 11, 0)
future_date = dtm.datetime(2021, 10, 1, 11, 0)
The original data & tradable instrument #
Construct a simple momentum signal of the Eurostoxx index and create a tradable version of the index to trade using the signal. The index is plotted in the following example.
Use obj.get
, a generic object retrieving API that can be used on any platform object, such as individual instruments or strategies. This goes to a database, an object cache or builds the object on the fly.
index_ts = sig.obj.get('SX5E INDEX').history()
instr_sx5e = sig.TradableIndex(underlying='SX5E INDEX', currency='EUR')
instr_sx5e.history().loc[:past_date].plot();
A signal is defined in the example below by providing a new signal function, which retrieves the index data and evaluates the average return for the last 21 periods. The negative sign of this average is then used for a mean-reversion signal.
Defining a simple mean-reversion strategy #
def signal_fn():
mean_reversion_signal = - \
np.sign(sig.obj.get('SX5E INDEX').history(
).pct_change().rolling(window=21).mean())
return mean_reversion_signal.dropna().to_frame('SX5E')
signal_obj = sig.Signal(function=lambda: signal_fn())
In the following example the building block SignalStrategy
is used to turn a time series of signals into a tradable strategy which can be backtested.
Learn more: Signal Strategy
start_date = dtm.date(2010, 1, 4)
example_strategy_1 = sig.SignalStrategy(
start_date=start_date,
end_date=past_date,
currency='EUR',
signal_name=signal_obj.name,
rebalance_frequency='EOM',
allocation_function=sig.signal_library.allocation.identity,
instrument_mapping={'SX5E': instr_sx5e.name},
convert_long_short_weight=False,
)
Python:
example_strategy_1.history().plot()
Output:
Modifying the data using a simulation adapter #
As the historic performance has been obtained, the performance that would be achieved if the data were modified is important. This can be achieved by introducing a simulation
adapter, which can be dynamically added or removed from the data service. It obtained the raw data from the underlying data adapters and feed a modified version to the user. This allows various modifications to be performed.
A few examples include:
Moving a window of original data to a new time to be replayed. The replay can either be in absolute, difference or return space.
Perturbing the original data.
Substituting the original data with randomly generated data.
In the following example we obtain a simulation adapter, push it on to the data service and add a modifier. In this case the modifier acts on the Eurostoxx index data by shifted the original level by a normally distributed random quantity.
sim_adapter = SimAdapter()
sim_adapter.push_to_data_service()
sim_adapter.add_normal_timeseries_modifier(
'SX5E INDEX',
original_weight=1.0,
change_type='PRICE',
mean_override=0.0,
std_override=10.0
)
The caches in the platform need clearing to allow the new data to flow through:
sig.config.configured_env().object.clear_object_data()
Now the modifier has been added we can retrieve the data and see a perturbation compared to the original level.
(sig.obj.get('SX5E INDEX').history() - index_ts).loc[:past_date].plot();
The strategy can be run multiple times using different perturbations, which are defined by a seed on the simulation adapter. The returns are all plotted below for 128 runs and the total return each time is recorded.
returns = []
for s in range(128):
sim_adapter.clear_simulation()
sig.config.configured_env().object.clear_object_data()
sim_adapter.seed = s
total_return = example_strategy_1.history().dropna()
returns.append((total_return.iloc[-1] / total_return.iloc[0]) - 1)
np.log(total_return).plot(color='r', alpha=0.01)
The distribution of returns:
pd.Series(returns).hist(bins=10);
As the current simulation adapter is no longer needed, remove it from the data service and clear the caches:
sim_adapter.remove_from_data_service()
sig.config.configured_env().object.clear_object_data()
sig.config.configured_env().fx_cross.clear_cache()
Future scenarios #
In the following example an FX hedged version of the previous strategy is run in to the future.
The FX Forward Hedging Strategy #
The previous strategy is based in EUR. We can wrap it in by using the FXForwardHedgingStrategy
to hedge the FX risk for a USD version.
hedged_instr_nky = sig.FXForwardHedgingStrategy(
start_date=start_date,
end_date=past_date,
strategy_name=example_strategy_1.name,
currency='USD',
hedging_tenor='1M',
exposure_rebalance_threshold=0.02,
hedge_rebalance_threshold=0.02,
use_holdings_for_hedge_ccy = True,
)
The performance of the hedged version is shown against the unhedged version:
hedged_instr_nky.history().plot()
example_strategy_1.history().plot();
Adding a simulation adapter to replay history #
Create a simulation adapter and push it to the data service. In this example, add modifiers to move data from the past in to the future. As forward is now being included in the strategy, move the curves:
future_sim_adapter = SimAdapter()
future_sim_adapter.push_to_data_service()
replay_start = dtm.date(2010, 1, 1)
future_sim_adapter.add_move_timeseries_modifier('SX5E INDEX',
change_type='RETURN',
original_start=replay_start,
fill_start=past_date.date())
future_sim_adapter.add_move_timeseries_modifier('EURUSD CURNCY',
change_type='RETURN',
original_start=replay_start,
fill_start=past_date.date())
future_sim_adapter.add_move_curve_modifier('USD.D CURVE',
original_start=replay_start,
fill_start=past_date.date())
future_sim_adapter.add_move_curve_modifier('USD.FX CURVE',
original_start=replay_start,
fill_start=past_date.date())
future_sim_adapter.add_move_curve_modifier('EUR.FX CURVE',
original_start=replay_start,
fill_start=past_date.date())
sig.config.configured_env().object.clear_object_data()
The retrieved data now shows the returns being replayed in to the future:
ax = sig.obj.get('SX5E INDEX').history().plot()
ax.axvline(past_date, color='k', linestyle='--')
ax.axvline(replay_start, color='k', linestyle='--', alpha=0.5);
ax = sig.obj.get('EURUSD CURNCY').history().plot()
ax.axvline(past_date, color='k', linestyle='--')
ax.axvline(replay_start, color='k', linestyle='--', alpha=0.5);
The strategies can be redefined to run up to this future date. The performance is then calculated:
start_date = dtm.date(2010, 1, 4)
future_example_strategy = sig.SignalStrategy(
start_date=start_date,
end_date=future_date,
currency='EUR',
signal_name=signal_obj.name,
rebalance_frequency='EOM',
allocation_function=sig.signal_library.allocation.identity,
instrument_mapping={'SX5E': instr_sx5e.name},
convert_long_short_weight=False,
)
hedged_instr = sig.FXForwardHedgingStrategy(
start_date=start_date,
end_date=future_date,
strategy_name=future_example_strategy.name,
currency='USD',
hedging_tenor='1M',
exposure_rebalance_threshold=0.02,
hedge_rebalance_threshold=0.02,
)
ax = hedged_instr.history().plot(color='r')
future_example_strategy.history().plot(ax=ax, color='b')
ax.axvline(past_date, color='k', linestyle='--');
FX Barrier Hedging Strategy #
We can also hedge using FX Barrier options. The following example defines a strategy to do this:
class FXBarrierHedgingStrategy(sig.DailyStrategy):
# Input Checking
strategy_name = StringType(required=True)
maturity = StringType(required=True)
rebalance_frequency = StringType(required=True)
group_name = StringType(required=True)
bdc = StringType(default=sig.calendar.BDC_PRECEDING)
def __init__(self):
super(FXBarrierHedgingStrategy, self).__init__()
self.option_group = sig.obj.get(self.group_name)
self._strategy = sig.obj.get(self.strategy_name)
self.over = self._strategy.currency
self.under = self.currency
def schedule_information(self):
""" set the holiday calendar for rebalancing - Here we use the the same holidays as the global trading manager """
return StrategySchedule(holidays=sig.TradingManager.instance().trading_holidays)
def get_rebalance_dates(self):
"""
helper function to get all the rebalance dates based on the rebalance schedule and market holidays
"""
first_date = self.calendar_schedule().next_data_date(self.start_date)
all_dates = sig.SchedulePeriodic(self.start_date, self.calculation_end_date(),
self.history_schedule().approximate_holidays(),
frequency=self.rebalance_frequency, bdc=self.bdc).all_data_dates()[1:]
return [first_date] + all_dates
def strategy_initialization(self, dt):
""" This is the first function to start the strategy building process. """
self.add_method(self.first_entry_date, self.push_strategy)
rebalance_dates = self.get_rebalance_dates()
for d in rebalance_dates:
self.add_method(d, self.enter_barrier_option_positions)
def push_strategy(self, dt):
self.add_position_target(dt, self.strategy_name, 1.0, unit_type='WEIGHT')
self.add_method(self.valuation_dt(dt.date()),
self.clean_up_foreign_cash)
def clean_up_foreign_cash(self, dt):
for cash_instrument, quantity in self.positions.iterate_cash_positions(dt):
if cash_instrument.currency != self.currency:
self.add_fx_spot_trade(dt, self.currency,
cash_instrument.currency, -quantity)
def enter_barrier_option_positions(self, dt):
""" This process determines the barrier option parameters. """
# first close out existing fx forwards
for instrument, quantity in self.positions.iterate_instruments(dt):
if instrument.is_option():
self.add_position_target(dt, instrument.name, 0.0, unit_type='WEIGHT')
size_date = self.size_date_from_decision_dt(dt)
notional_ccy = self.valuation_price(size_date, ccy=self.over)
maturity_date = self.option_group.convert_maturity_tenor_to_date(
size_date, self.maturity, target_maturity_weekday=3)
# Use the spot price
atm = sig.obj.get(self.option_group.underlying).history().asof(pd.to_datetime(size_date))
barrier_option = self.option_group.get_option(
'Put', atm, size_date, maturity_date, barrier_type='KI', barrier=atm * 0.99)
self.add_position_target(dt, barrier_option.name, notional_ccy)
ki_hedged_instr = FXBarrierHedgingStrategy(
currency='USD',
strategy_name=future_example_strategy.name,
start_date=start_date,
end_date=past_date,
group_name='EURUSD OTC OPTION GROUP',
maturity='3M',
rebalance_frequency='3M',
)
ax = ki_hedged_instr.history().plot(color='r')
example_strategy_1.history().plot(ax=ax, color='b')
ax.axvline(past_date, color='k', linestyle='--');
Input:
ki_hedged_instr.timeline_holdings.display(
full=True,
start_dt=ki_hedged_instr.valuation_dt(start_date),
end_dt=ki_hedged_instr.valuation_dt(dtm.date(2010, 5, 1))
)
Output:
----------2010-01-05 07:30:00+00:00----------
+692.6167 Units Buy outright EURUSD 20100331 PUT 1.4437999999999995 KI1.4293619999999996 7F4FF7C2 CURNCY 2010-01-05T16:00:00+00:00
+0.6939 EUR ED8BED6C SS STRATEGY
-693.8728 EUR CASH
+1,000.0014 USD CASH
----------2010-01-05 16:00:00+00:00----------
+692.6167 EURUSD 20100331 PUT 1.4437999999999995 KI1.4293619999999996 7F4FF7C2 CURNCY
+0.6939 EUR ED8BED6C SS STRATEGY
-693.8728 EUR CASH
+977.1270 USD CASH
----------2010-01-05 23:59:59+00:00----------
+692.6167 EURUSD 20100331 PUT 1.4437999999999995 KI1.4293619999999996 7F4FF7C2 CURNCY
+0.6939 EUR ED8BED6C SS STRATEGY
-19.6863 USD CASH
----------2010-01-29 16:00:00+00:00----------
+692.6167 EURUSD 20100331 PUT 1.4437999999999995 KI1.4293619999999996 7F4FF7C2 CURNCY
+0.6939 EUR ED8BED6C SS STRATEGY
-19.6878 USD CASH
----------2010-02-26 16:00:00+00:00----------
+692.6167 EURUSD 20100331 PUT 1.4437999999999995 KI1.4293619999999996 7F4FF7C2 CURNCY
+0.6939 EUR ED8BED6C SS STRATEGY
-19.6897 USD CASH
----------2010-03-31 15:00:00+00:00----------
+0.6939 EUR ED8BED6C SS STRATEGY
+43.0931 USD CASH
----------2010-03-31 16:00:00+00:00----------
+0.6939 EUR ED8BED6C SS STRATEGY
+43.0931 USD CASH
----------2010-04-01 06:30:00+00:00----------
+820.6550 Units Buy outright EURUSD 20100702 PUT 1.3531499999999994 KI1.3396184999999994 0BCCAF85 CURNCY 2010-04-01T15:00:00+00:00
+0.6939 EUR ED8BED6C SS STRATEGY
+43.0933 USD CASH
----------2010-04-01 15:00:00+00:00----------
+820.6550 EURUSD 20100702 PUT 1.3531499999999994 KI1.3396184999999994 0BCCAF85 CURNCY
+0.6939 EUR ED8BED6C SS STRATEGY
+18.9453 USD CASH
----------2010-04-30 16:00:00+00:00----------
+820.6550 EURUSD 20100702 PUT 1.3531499999999994 KI1.3396184999999994 0BCCAF85 CURNCY
+0.6939 EUR ED8BED6C SS STRATEGY
+18.9482 USD CASH