menu
Pricing and Risk

Backtesting

The backtesting framework is a language for describing backtests that's (1) intuitive (2) cross-asset and instrument agnostic (3) available natively in python for users to change and extend. It's compatible with a variety of calculation engines - eliminating need for specific syntax knowledge - and uses the gs risk API as the default calculation engine.

The language consists of several basic components:

  • Trigger
  • Action
  • Strategy
  • Calculation Engine
  • Backtest

In this tutorial, we will examine the components in the context of looking at a basic vol selling strategy where we sell a 1m10y USD straddle daily.

info

Note

Examples require an initialized GsSession and relevant entitlements. Please refer to Sessions for details.

Trigger

A trigger can be used to amend a strategy based on a set of requirements and the current state of the backtest. Each trigger has an associated action or set of actions that are applied when the trigger is active. A backtest can have multiple triggers.

Below are the triggers we currently offer:

  • PeriodicTrigger: triggers at a regular interval (time/date). e.g.: every month end
  • MktTrigger: triggers when market data, absolute or change, hits some target e.g. whenever SPX 1m vol crosses 10. Check out our data catalog for sources of market data that can be leveraged here
  • StrategyRiskTrigger: triggers when the current risk of the strategy hits some target e.g. delta crosses $10m

Since in our example, we are selling an instrument periodically e.g. (daily), we will use a single PeriodicTrigger. Let's define the PeriodicTriggerRequirements first and then talk about actions, since we need to specify at least a single action to happen when this trigger is triggered.

from gs_quant.backtests.triggers import PeriodicTrigger, PeriodicTriggerRequirements
from datetime import date

start_date, end_date = date(2020, 1, 1), date(2020, 12, 1)
# define dates on which actions will be triggered (B=every business day here)
trig_req = PeriodicTriggerRequirements(start_date=start_date, end_date=end_date, frequency='B')

Action

Actions define how the portfolio is updated when a trigger is triggered. Below are the actions we currently offer:

  • AddTradeAction: adds trade to a strategy
  • HedgeAction: add trade that's a hedge for a risk

Since in our example we are selling a straddle, we only want a single AddTradeAction to supply to our trigger. Let's define it.

from gs_quant.backtests.actions import AddTradeAction
from gs_quant.common import PayReceive, Currency
from gs_quant.instrument import IRSwaption

# straddle is the position we'll be adding (note it's a short position since buy_sell='Sell')
straddle = IRSwaption(PayReceive.Straddle, '10y', Currency.USD, expiration_date='1m', notional_amount=1e8, buy_sell='Sell')

# this action specifies we will add the straddle when this action is triggered and hold it until expiration_date
action = AddTradeAction(straddle, 'expiration_date')

# we will now combine our trigger requirement and action to produce a PeriodicTrigge
# Note, action can be a list if there are multiple actions
trigger = PeriodicTrigger(trig_req, action)

Strategy

A Strategy combines all the information needed to run a backtest. It has 2 components: an initial trade or portfolio and a trigger or set of triggers.

In our example, we don't have a starting portfolio and we have a single trigger we defined in the previous step. Let's put these together.

from gs_quant.backtests.strategy import Strategy

# in this case we have a single trigger but this can be a list of triggers as well
strategy = Strategy(None, trigger)

Calculation engine

The final piece is calculate engine - the underlying engine/source of calculations you want to run your backtest.

Once you have defined your backtest in the steps above, you can view which available engines support your strategy. There may be multiple as different engines have been developed and optimized for certain types of backtesting. For example, the EquityVolEngine is optimized for fast volatility backtesting for a specific set of equity underlyers. That said, GenericEngine will support majority of use cases, running calculations using gs-quant's risk apis. You can check which available engines support your strategy using the function below:

strategy.get_available_engines()

Output:

[gs_quant.backtests.generic_engine.GenericEngine,
gs_quant.backtests.generic_price_engine.GenericPriceEngine]

Alternatively, you check whether a particular engine supports your strategy:

from gs_quant.backtests.generic_engine import GenericEngine

ge = GenericEngine()
ge.supports_strategy(strategy)

Output:

True

As a final step, let's put everything together to run the backtest. The frequency here indicates the frequency of calculations. You can also supply additional risks you may want to calculate as part of your backtest (greeks or dollar price, for example).

backtest = ge.run_backtest(strategy, start=start_date, end=end_date, frequency='B', show_progress=True)
backtest

Output:

Backtest

run_backtest returns an object which can be used in several ways:

  • to view the results of backtest.risks (by default, Price) evaluated on each backtested date: backtest.results
  • to view and further evaluate the portfolio on any given backtested date: backtest.portfolio_dict
  • to view the number of total api calls and number of calculations performed: backtest.calc_calls and backtest.calculations

In this case, we can use backtest.results and backtest.portfolio_dict to construct timeseries for premia paid, payoff collected and the mark-to-market of our strategy.

from collections import defaultdict
import pandas as pd

results = backtest.results
g_premia, g_payoff, g_mtm = defaultdict(int), defaultdict(int), defaultdict(int)
for day, portfolio in backtest.portfolio_dict.items():
    for inst in portfolio:
        if inst.premium_payment_date == day:
            g_premia[day] += results[day][inst]
        if inst.expiration_date == day:
            g_payoff[day] += results[day][inst]
        if inst.expiration_date != day:
            g_mtm[day] += results[day][inst]

g_overview = pd.concat([pd.Series(g_premia).cumsum(), pd.Series(g_payoff).cumsum(), pd.Series(g_mtm)], axis=1,
                     sort=True).fillna(method='ffill').fillna(0)
g_overview.columns = ['Premium Received at Inception', 'Paid at Expiry', 'Mark to Market']
g_overview.plot(figsize=(12, 8), title='Cumulative Payoff, Premium and Mark-to-Market')

Output:


Related Content