Backtest template#

This is a template for Jupyter Notebook for developing algorithmic trading strategies using Trading Strategy framework.

This is an empty backtest that won’t do any trades, because it does not contain any trading logic. For examples that contain strategy logic please see the strategy examples section in the documentation.

✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

How to use this template#

Copy-paste this template to your favorite Jupyter Notebook environment. Some examples below.

Currently Python version 3.9 or higher is required. Google Colab environment is unsupported for this reason.

MyBinder cloud environment#

To get started with MyBinder development:

  • Click Launch Binder at the top of this documentation page

  • Save as

Local development or Visual Studio Code#

To get started with development on your local computer using locally installed Visual Studio Code and Python environment.

Read instructions for setting up an environment for Visual Studio Code.

End of instructions#

The actual trading strategy code starts here. After you have successfully run the notebook in your environment you can delete this section and everything above this.

✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

Strategy parameters#

In this section we set up parameters for the strategy backtest.

For available options see the documentation in relevant Python classes:

Beyond the options above, we have some self-explanatory parameters like.

  • Max position size (of cash, or available total equity)

  • Initial cash at the start of the backtesting period

  • Backtesting start and end dates

[1]:
import datetime
import pandas as pd

from tradingstrategy.chain import ChainId
from tradingstrategy.timebucket import TimeBucket
from tradeexecutor.strategy.cycle import CycleDuration
from tradeexecutor.strategy.strategy_module import StrategyType, TradeRouting, ReserveCurrency

# Tell what trade execution engine version this strategy needs to use
trading_strategy_engine_version = "0.1"

backtest_name = "Your backtest name"

# What kind of strategy type we are running.
trading_strategy_type = StrategyType.managed_positions

# On which chain we are trading
chain_id = ChainId.bsc

# How our trades are routed.
# PancakeSwap basic routing supports two way trades with BUSD
# and three way trades with BUSD-BNB hop.
trade_routing = TradeRouting.pancakeswap_basic

# How often the strategy performs the decide_trades cycle.
# We do it for every 24h.
trading_strategy_cycle = CycleDuration.cycle_24h

# Strategy keeps its cash as BUSD token
reserve_currency = ReserveCurrency.busd

# We use daily candles for this strategy
candle_time_bucket = TimeBucket.d1

# Which exchange we are trading on.
exchange_slug = "pancakeswap-v2"

# Which trading pair we are trading
trading_pair = ("WBNB", "BUSD")

# How much of the cash from the strategy treasury
# we allocate for a single trade
position_size = 0.10

# Backtesting range start
start_at = datetime.datetime(2021, 6, 1)

# Backtesting range end
end_at = datetime.datetime(2022, 1, 1)

# Start with 10,000 USD
initial_deposit = 10_000

#
# Strategy thinking specific parameter
#
# TODO: Add your parameters here.
#

# For backward looking strategies, how many candles we look backwards
candle_batch_size = 90

Strategy logic#

The strategy logic is in decide_trades() Python function.

  • This function is called for each strategy cycle.

  • The return value of the function is a list of trades that the strategy does for the cycle.

  • The logic function takes inputs:

    • Timestamp is the time of the strategy cycle.

    • The function takes the strategy trading universe as an input. This is OHLCV and liquidity data for chosen trading pairs. The additional input includes the current State of the strategy execution.

    • The function also receives the current PricingModel as the input. The pricing model estimates how much it will cost us to open or close positions, in the terms of price impact and transaction fees.

    • The function also receives cycle_debug_data Python dictionary that strategy developers can use to track various diagnostics information for their internal purposes

  • The function constructs PositionManager helper class to generate trades for different actions (open position, close position, close all positions). You can also use PositionManager to query open positions.

  • The strategy can further visualise the indicators it uses using visualisation instance that is part of the state. This is especially useful during backtesting when one is iterating through different trading ideas and want to see how they behave.

If you do not add any logic, the backtest results section will plot the price action for the backtest duration and no positions or trades are made.

[2]:
from typing import List, Dict

from tradeexecutor.state.visualisation import PlotKind
from tradeexecutor.state.trade import TradeExecution
from tradeexecutor.strategy.pricing_model import PricingModel
from tradeexecutor.strategy.pandas_trader.position_manager import PositionManager
from tradeexecutor.state.state import State
from tradingstrategy.universe import Universe


def decide_trades(
        timestamp: pd.Timestamp,
        universe: Universe,
        state: State,
        pricing_model: PricingModel,
        cycle_debug_data: Dict) -> List[TradeExecution]:
    """The brain function to decide the trades on each trading strategy cycle.

    - Reads incoming execution state (positions, past trades)

    - Reads the current universe (candles)

    - Decides what to do next

    - Outputs strategy thinking for visualisation and debug messages

    :param timestamp:
        The Pandas timestamp object for this strategy cycle. Matches
        trading_strategy_cycle division.
        Always truncated to the zero seconds and minutes, never a real-time clock.

    :param universe:
        Trading universe that was constructed earlier.

    :param state:
        The current trade execution state.
        Contains current open positions and all previously executed trades, plus output
        for statistics, visualisation and diangnostics of the strategy.

    :param pricing_model:
        Pricing model can tell the buy/sell price of the particular asset at a particular moment.

    :param cycle_debug_data:
        Python dictionary for various debug variables you can read or set, specific to this trade cycle.
        This data is discarded at the end of the trade cycle.

    :return:
        List of trade instructions in the form of :py:class:`TradeExecution` instances.
        The trades can be generated using `position_manager` but strategy could also hand craft its trades.
    """

    # The pair we are trading
    pair = universe.pairs.get_single()

    # How much cash we have in the hand
    cash = state.portfolio.get_current_cash()

    # How much we can afford to spend on opening a position, USD wise=
    cash_available_for_trade = cash * position_size

    # Get OHLCV candles for our trading pair as Pandas Dataframe.
    candles: pd.DataFrame = universe.candles.get_single_pair_data(timestamp, sample_count=candle_batch_size)

    # We have data for open, high, close, etc.
    # We only operate using candle close values in this strategy.
    # This is a Pandas series object
    # # https://pandas.pydata.org/docs/reference/api/pandas.Series.iat.html
    close = candles["close"]

    # Calculate exponential moving averages based on slow and fast sample numbers.

    # Get the last close price from OHLCV close time series
    current_price = close.iloc[-1]

    # List of any trades we decide on this cycle.
    # Because the strategy is simple, there can be
    # only zero (do nothing) or 1 (open or close) trades
    # decides
    trades = []

    # Create a position manager helper class that allows us easily to create
    # opening/closing trades for different positions
    position_manager = PositionManager(timestamp, universe, state, pricing_model)

    # 💡 💡 💡 💡 💡 💡
    # TODO: Add strategy logic here using position manager
    # For examples see
    # 💡 💡 💡 💡 💡 💡

    # Visualize strategy
    # See Strategy examples how to plot your technical indicators
    # in backtesting output.
    # https://tradingstrategy.ai/docs/programming/strategy-examples/index.html
    visualisation = state.visualisation

    # 💡 💡 💡 💡 💡 💡
    # TODO: plot any extra indicators you want here
    # 💡 💡 💡 💡 💡 💡

    return trades

Trading universe#

Trading universe tells us on what assets our strategy will trade and used data:

  • Blockchains the strategy trades on

  • Exchanges the strategy trades on

  • List of allowed trading pairs

  • OHCLV candles and candle duration, also known as bucket

  • Liquidity samples

In this example, we create a trading universe that contains only a single predefined trading pair, as set in the Strategy parameters section above.

Unless you want to have multipair trading strategy, you do not need to change this function, as it only reads input from Strategy parameters section above.

[3]:
from typing import Optional
from tradeexecutor.strategy.trading_strategy_universe import load_all_data, TradingStrategyUniverse
from tradeexecutor.strategy.execution_context import ExecutionContext
from tradingstrategy.client import Client
import datetime

def create_trading_universe(
        ts: datetime.datetime,
        client: Client,
        execution_context: ExecutionContext,
        candle_time_frame_override: Optional[TimeBucket]=None,
) -> TradingStrategyUniverse:
    """Creates the trading universe where the strategy trades.

    If `execution_context.live_trading` is true then this function is called for
    every execution cycle. If we are backtesting, then this function is
    called only once at the start of backtesting and the `decide_trades`
    need to deal with new and deprecated trading pairs.

    As we are only trading a single pair, load data for the single pair only.

    :param ts:
        The timestamp of the trading cycle. For live trading,
        `create_trading_universe` is called on every cycle.
        For backtesting, it is only called at the start

    :param client:
        Trading Strategy Python client instance.

    :param execution_context:
        Information how the strategy is executed. E.g.
        if we are live trading or not.

    :param candle_timeframe_override:
        Allow the backtest framework override what candle size is used to backtest the strategy
        without editing the strategy Python source code file.

    :return:
        This function must return :py:class:`TradingStrategyUniverse` instance
        filled with the data for exchanges, pairs and candles needed to decide trades.
        The trading universe also contains information about the reserve asset,
        usually stablecoin, we use for the strategy.
    """

    # Load all datas we can get for our candle time bucket
    dataset = load_all_data(client, candle_time_bucket, execution_context)

    # Filter down to the single pair we are interested in
    universe = TradingStrategyUniverse.create_single_pair_universe(
        dataset,
        chain_id,
        exchange_slug,
        trading_pair[0],
        trading_pair[1],
    )

    return universe

Market data client#

The Trading Strategy market data client is the Python library responsible for managing the data feeds needed to run the backtest.None

We set up the market data client with an API key.

When this notebook is run, you will receive a prompt or a dialog box to enter your API key. After entering the API key once, it is saved on your notebook system in your home folder.

[4]:
from tradingstrategy.client import Client

client = Client.create_jupyter_client()
Started Trading Strategy in Jupyter notebook environment, configuration is stored in /Users/moo/.tradingstrategy

Running backtest#

In this section, the backtest is run using giving trading universe and strategy function.

  • Running the backtest outputs state object that contains all the information on the backtesting position and trades.

  • The trade execution engine will download the necessary datasets to run the backtest. The datasets may be large, several gigabytes.

Unless you want to customize how backtests are run, you do not need to change this section, as it is only using parameters defined in Strategy parameters section above.

[5]:
import logging

from tradeexecutor.backtest.backtest_runner import run_backtest_inline

state, universe, debug_dump = run_backtest_inline(
    name=backtest_name,
    start_at=start_at,
    end_at=end_at,
    client=client,
    cycle_duration=trading_strategy_cycle,
    decide_trades=decide_trades,
    create_trading_universe=create_trading_universe,
    initial_deposit=initial_deposit,
    reserve_currency=reserve_currency,
    trade_routing=trade_routing,
    log_level=logging.WARNING,
)

Backtest results#

In this section, we examine and visualise the performance of the trading strategy based on its backtest results.

  • We read state object that contains all backtesting positions and trades created during the backtest run

  • Based on these trades, we create various charts ana analysis to understand the strategy performance

For analysis options see strategy performance analysis section in the documentatio.

Trade and position count#

We print out how many (if any) positions and trades was taken by the strategy.

[6]:
print(f"Backtesting for {backtest_name} complete")
print(f"Positions taken: {len(list(state.portfolio.get_all_positions()))}")
print(f"Trades made: {len(list(state.portfolio.get_all_trades()))}")
Backtesting for Your backtest name complete
Positions taken: 0
Trades made: 0

Price and trade chart#

We plot out a chart that shows - The price action - Any technical indicators we used and added to the visualisation - When the strategy made buys or sells

[7]:
from tradeexecutor.visual.single_pair import visualise_single_pair

figure = visualise_single_pair(
    state,
    universe.universe.candles,
    start_at=start_at,
    end_at=end_at)

figure.show()